vershunt 2.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,49 @@
1
+ module MSPRelease
2
+ class CLI::Bump < CLI::Command
3
+
4
+ include CLI::WorkingCopyCommand
5
+
6
+ description "Increase the version number of the project"
7
+
8
+ arg :segment, "One of major, minor or bugfix"
9
+
10
+ opt :force, "Force bumping of version segments other than bugfix " +
11
+ "if you are not on master. By default, you bump minor and major from" +
12
+ " master, and you bump the bugfix version while on a branch.",
13
+ {
14
+ :short => 'f',
15
+ :default => false
16
+ }
17
+
18
+ def run
19
+ segment = arguments[:segment]
20
+
21
+ check_bump_allowed!(segment)
22
+
23
+ new_version, *changed_files = project.bump_version(segment)
24
+
25
+ files = [project.config_file, *changed_files].compact
26
+
27
+ files.each do |file|
28
+ exec "git add #{file}"
29
+ end
30
+ exec "git commit -m 'BUMPED VERSION TO #{new_version}'"
31
+ puts "New version: #{new_version}"
32
+ end
33
+
34
+ def check_bump_allowed!(segment)
35
+ force = options[:force]
36
+ not_on_master = !git.on_master?
37
+
38
+ if not_on_master && segment != 'bugfix' && ! force
39
+ raise CLI::Exit, "You must be on master to bump the #{segment} version" +
40
+ ", or pass --force if you are sure this is what you want to do"
41
+ end
42
+ end
43
+
44
+ def revert_bump(changed_files)
45
+ exec "git checkout -- #{project.config_file} #{changed_files.join(' ')}"
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,196 @@
1
+ require 'fileutils'
2
+
3
+ module MSPRelease
4
+ class CLI::Checkout < CLI::Command
5
+ include Debian::Versions
6
+
7
+ # When cloning repositories, limit to this many commits from each head
8
+ CLONE_DEPTH = 5
9
+
10
+ description """Checkout a specific commit from a git repository suitable
11
+ for building.
12
+
13
+ When no BRANCH_NAME is given, or that branch is not a release branch, the
14
+ latest commit is checked out and the changelog version is adjusted to signify
15
+ this will be a development build.
16
+
17
+ If BRANCH_NAME denotes a release branch (i.e release-1.0.2) then the latest
18
+ /release/ commit is checked out, even if there are commits after it.
19
+ The changelog remains unaltered in this case - the release commit would have
20
+ updated all version information.
21
+ """
22
+
23
+ arg :git_url, "URL used to clone the git repository"
24
+ arg :branch_name, "Name of a branch on master to switch to once checked out",
25
+ :required => false
26
+
27
+ opt :build, "Build a debian package immediately after checking " +
28
+ "out, using the dpkg-buildpackage command",
29
+ {
30
+ :short => 'b',
31
+ :default => false
32
+ }
33
+
34
+ opt :noise, "Print output to stdout",
35
+ {
36
+ :short => 'n',
37
+ :default => true
38
+ }
39
+
40
+ opt :print_files, "Print out built files to stdout",
41
+ {
42
+ :short => 'p',
43
+ :default => false
44
+ }
45
+
46
+ opt :sign, "Pass options to dpkg-buildpackage to tell it whether or not to sign the build products",
47
+ {
48
+ :short => 'S',
49
+ :default => false
50
+ }
51
+
52
+ opt :tar, "Create a tarfile containing all the debian build " +
53
+ "products when using --build",
54
+ {
55
+ :short => 't',
56
+ :default => false
57
+ }
58
+
59
+ opt :shallow, "Only perform a shallow checkout to a depth of five" +
60
+ "commits from each head. See git documentation for more details",
61
+ {
62
+ :short => 's',
63
+ :default => false
64
+ }
65
+
66
+ opt :distribution, "Specify the debian distribution to put in the " +
67
+ "changelog when checking out a development version",
68
+ {
69
+ :short => 'd',
70
+ :long => 'debian-distribution',
71
+ :type => :string
72
+ }
73
+
74
+ def run
75
+ git_url = arguments[:git_url]
76
+ release_spec_arg = arguments[:branch_name]
77
+
78
+ do_build = options[:build]
79
+ tar_it = options[:tar]
80
+ clone_depth = options[:shallow] ? CLONE_DEPTH : nil
81
+
82
+ LOG.verbose if options[:noise]
83
+
84
+ branch_name = release_spec_arg || 'master'
85
+ pathspec = "origin/#{branch_name}"
86
+ branch_is_release_branch = !! /^release-.+$/.match(branch_name)
87
+
88
+ shallow_output = clone_depth.nil?? '' : ' (shallow)'
89
+ if release_spec_arg && branch_is_release_branch
90
+ LOG.debug("Checking out latest release commit from #{pathspec}#{shallow_output}")
91
+ else
92
+ LOG.debug("Checking out latest commit from #{pathspec}#{shallow_output}")
93
+ end
94
+
95
+ tmp_dir = "vershunt-#{Time.now.to_i}.tmp"
96
+ Git.clone(git_url, {:depth => clone_depth, :out_to => tmp_dir,
97
+ :exec => {:quiet => true}})
98
+
99
+ project = Project.new_from_project_file(tmp_dir + "/" + Helpers::PROJECT_FILE)
100
+ distribution = options[:distribution] || project.changelog.distribution
101
+
102
+ src_dir = Dir.chdir(tmp_dir) do
103
+
104
+ if pathspec != "origin/master"
105
+ move_to(pathspec)
106
+ end
107
+
108
+ if branch_is_release_branch
109
+ first_commit_hash, commit_message =
110
+ find_first_release_commit(project)
111
+
112
+ if first_commit_hash.nil?
113
+ raise CLI::Exit, "Could not find a release commit on #{pathspec}"
114
+ end
115
+
116
+ exec "git reset --hard #{first_commit_hash}"
117
+ else
118
+ dev_version = Development.
119
+ new_from_working_directory(branch_name, latest_commit_hash)
120
+
121
+ project.changelog.amend(dev_version, distribution)
122
+ end
123
+ src_dir = project.source_package_name + "-" + project.changelog.version.to_s
124
+ end
125
+
126
+ FileUtils.mv(tmp_dir, src_dir)
127
+ project = Project.new_from_project_file(src_dir + "/" + Helpers::PROJECT_FILE)
128
+ LOG.debug("Checked out to #{src_dir}")
129
+
130
+ if do_build
131
+ LOG.debug("Building package...")
132
+ build = Build.new(src_dir, project, :sign => options[:sign])
133
+
134
+ result = build.perform_from_cli!
135
+ if print_files?
136
+ result.files.each {|f| stdout.puts(f) }
137
+ end
138
+ tar_it_up(project, result) if tar_it
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def tar_it_up(project, result)
145
+ files = result.files
146
+ tarfile = "#{project.source_package_name}-#{project.changelog.version}.tar"
147
+ exec("tar -cf #{tarfile} #{files.join(' ')}")
148
+ LOG.debug("Build products archived in to #{tarfile}")
149
+ stdout.puts(tarfile) if print_files?
150
+ end
151
+
152
+ def print_files?
153
+ options[:print_files]
154
+ end
155
+
156
+ def oneline_pattern
157
+ /^([a-z0-9]+) (.+)$/i
158
+ end
159
+
160
+ def log_command
161
+ "git --no-pager log --no-color --full-index"
162
+ end
163
+
164
+ def latest_commit_hash
165
+ output = exec(log_command + " --pretty=oneline -1").split("\n").first
166
+ oneline_pattern.match(output)[1]
167
+ end
168
+
169
+ def find_first_release_commit(project)
170
+ all_commits = exec(log_command + " --pretty=oneline").
171
+ split("\n")
172
+
173
+ all_commits.map { |commit_line|
174
+ match = oneline_pattern.match(commit_line)
175
+ [match[1], match[2]]
176
+ }.find {|hash, message|
177
+ project.release_name_from_message(message)
178
+ }
179
+ end
180
+
181
+ def move_to(pathspec)
182
+ begin
183
+ exec("git show #{pathspec} --")
184
+ rescue Exec::UnexpectedExitStatus => e
185
+ if /^fatal: bad revision/.match(e.stderr)
186
+ raise CLI::Exit, "Git pathspec '#{pathspec}' does not exist"
187
+ else
188
+ raise
189
+ end
190
+ end
191
+
192
+ exec("git checkout --track #{pathspec}")
193
+ end
194
+ end
195
+
196
+ end
@@ -0,0 +1,7 @@
1
+
2
+
3
+ class MSPRelease::CLI::Distrib < MSPRelease::CLI::Command
4
+ def self.description
5
+ "Distribute a built debian package"
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ module MSPRelease
2
+ class CLI::Help < CLI::Command
3
+
4
+ description "Show help for vershunt and its sub-commands"
5
+
6
+ arg :command_name, "Name of command to see help for", :required => false
7
+
8
+ def run
9
+ command_name = arguments[:command_name]
10
+ command_class = MSPRelease::CLI.commands[command_name]
11
+ raise Climate::HelpNeeded.new(command_class || self)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ module MSPRelease
2
+ class CLI::New < CLI::Command
3
+
4
+ include CLI::WorkingCopyCommand
5
+
6
+ description "Prepare master or a release branch for a release push"
7
+
8
+ opt :distribution, "Specify a new debian distribution to put in the " +
9
+ "changelog for this new version",
10
+ {
11
+ :short => 'd',
12
+ :long => 'debian-distribution',
13
+ :type => :string
14
+ }
15
+
16
+ opt :force, "Force creation of a release commit from a non-release branch", {
17
+ :short => 'f',
18
+ :long => 'force',
19
+ :default => false
20
+ }
21
+
22
+ def run
23
+ fail_if_push_pending
24
+ fail_if_modified_wc
25
+ fail_unless_on_release_branch
26
+
27
+ LOG.verbose
28
+
29
+ new_version = project.next_version_for_release(options)
30
+
31
+ self.data = {:version => new_version}
32
+ save_data
33
+ end
34
+
35
+ def not_on_release_branch_msg
36
+ "You must be on a release branch to create " +
37
+ "release commits, or use --force.\nSwitch to a release branch, or build " +
38
+ "from any branch without creating a release commit for development builds"
39
+ end
40
+
41
+ def fail_unless_on_release_branch
42
+ if git.cur_branch != project.branch_name
43
+ if options[:force]
44
+ LOG.error("Not on a release branch, forcing creation of release " +
45
+ "commit. #{git.cur_branch} != #{project.branch_name}")
46
+ else
47
+ raise CLI::Exit, not_on_release_branch_msg
48
+ end
49
+ end
50
+ end
51
+
52
+ def get_next_release_number(suffix)
53
+ suffix_pattern = /([0-9]+)/
54
+ if suffix.nil? || suffix_pattern.match(suffix)
55
+ suffix.to_i + 1
56
+ else
57
+ raise CLI::Exit, "malformed suffix: #{suffix}\n" +
58
+ "Fix the changelog and try again"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,29 @@
1
+ module MSPRelease
2
+ class CLI::Push < CLI::Command
3
+
4
+ include CLI::WorkingCopyCommand
5
+
6
+ description "Push a new release to origin"
7
+
8
+ def run
9
+ unless data_exists?
10
+ raise CLI::Exit, "You need to stage a new release before you can push it"
11
+ end
12
+
13
+ load_data
14
+ release_name = "#{data[:version]}"
15
+ tagname = "release-#{release_name}"
16
+
17
+ if project.respond_to?(:project_specific_push)
18
+ project.project_specific_push(release_name)
19
+ end
20
+
21
+ exec "git tag #{tagname}"
22
+ exec "git push origin #{git.cur_branch}"
23
+ stdout.puts "Pushing new release tag: #{tagname}"
24
+ exec "git push origin #{tagname}"
25
+
26
+ remove_data
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ module MSPRelease
2
+ class CLI::Reset < CLI::Command
3
+
4
+ include CLI::WorkingCopyCommand
5
+
6
+ description "Reset changes made by vershunt new"
7
+
8
+ def run
9
+
10
+ raise CLI::Exit, "No waiting changes" unless data_exists?
11
+
12
+ exec "git checkout #{project.changelog_path}"
13
+ remove_data
14
+ puts "Reset"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ module MSPRelease
2
+ class CLI::Status < CLI::Command
3
+
4
+ include CLI::WorkingCopyCommand
5
+
6
+ description "Print out discovered release state"
7
+
8
+ def run
9
+ bits = []
10
+
11
+ if data_exists?
12
+ load_data
13
+ puts "Awaiting push. Please update the changelog, then run vershunt push "
14
+ bits.push(["Pending", data[:version]])
15
+ else
16
+ bits.push(["Project says", msp_version]) if msp_version
17
+ bits.push(["Release branch", on_release_branch?? git_version : nil])
18
+
19
+ if changelog
20
+ changelog_version = changelog.version
21
+ bits.push(["Changelog says", changelog_version])
22
+ end
23
+
24
+ end
25
+
26
+ bits.push(["Release commit", release_name_for_output])
27
+
28
+ format_bits(bits)
29
+ end
30
+
31
+ private
32
+
33
+ def format_bits(bits)
34
+ bits = bits.map {|header, value| [header, value.nil?? '<none>' : value.to_s ]}
35
+ widths = bits.transpose.map { |column| column.map {|v| v.length }.max }
36
+ bits.each do |row|
37
+ puts row.zip(widths).map {|val, width| val.ljust(width) }.join(" : ").strip
38
+ end
39
+ end
40
+
41
+ def release_name_for_output
42
+ commit = git.latest_commit(project)
43
+ commit.release_commit? && commit.release_name || nil
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,229 @@
1
+ class Debian
2
+
3
+ module Versions
4
+
5
+ module Versionable
6
+ def to_version
7
+ MSPRelease::Version.new(@major, @minor, @bugfix)
8
+ end
9
+ end
10
+
11
+ class Base
12
+
13
+ def self.new_if_matches(string)
14
+ pattern.match(string) && new_from_string(string)
15
+ end
16
+
17
+ def self.new_from_string(string)
18
+ match = pattern.match(string)
19
+ parts = (1...match.length).map {|idx| match[idx] }
20
+ begin
21
+ new(*parts)
22
+ rescue => e
23
+ raise "Unable to construct #{self} from #{string}\n#{e.to_s}"
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ class Unreleased < Base
30
+
31
+ include Versionable
32
+
33
+ def self.pattern
34
+ /^([0-9]+)\.([0-9]+)\.([0-9]+)$/
35
+ end
36
+
37
+ def self.new_from_version(version)
38
+ new(version.major, version.minor, version.bugfix)
39
+ end
40
+
41
+ def initialize(major, minor, bugfix)
42
+ @major = major
43
+ @minor = minor
44
+ @bugfix = bugfix
45
+ end
46
+
47
+ def bump
48
+ Stable.new(@major, @minor, @bugfix, "1")
49
+ end
50
+
51
+ def to_s
52
+ [@major, @minor, @bugfix].join(".")
53
+ end
54
+ end
55
+
56
+ class Stable < Base
57
+
58
+ include Versionable
59
+
60
+ def self.pattern
61
+ /^([0-9]+)\.([0-9]+)\.([0-9]+)-([0-9]+)$/
62
+ end
63
+
64
+ def initialize(major, minor, bugfix, revision)
65
+ @major = major
66
+ @minor = minor
67
+ @bugfix = bugfix
68
+ @revision = revision
69
+ end
70
+
71
+ def bump
72
+ self.class.new(@major, @minor, @bugfix, @revision.to_i + 1).to_s
73
+ end
74
+
75
+ def to_s
76
+ [@major, @minor, @bugfix].join(".") + "-#{@revision}"
77
+ end
78
+
79
+ end
80
+
81
+ class Development < Base
82
+
83
+ class << self
84
+ include MSPRelease::Helpers
85
+
86
+ def pattern
87
+ /^([0-9]+)-git\+([a-f0-9]+)~([a-z0-9\+\.]+)$/
88
+ end
89
+
90
+ def new_from_working_directory(branch_name, commit_hash)
91
+ safe_branch_name = branch_name.gsub(/[^a-z0-9]/i, ".")
92
+ new(timestamp, commit_hash[0...6], safe_branch_name)
93
+ end
94
+ end
95
+
96
+ def initialize(timestamp, hash, branch_name)
97
+ @timestamp = timestamp
98
+ @hash = hash
99
+ @branch_name = branch_name
100
+ end
101
+
102
+ def to_s
103
+ "#{@timestamp}-git+#{@hash}~#{@branch_name}"
104
+ end
105
+
106
+ def bump
107
+ Stable.new(@major, @minor, @bugfix, "1")
108
+ end
109
+ end
110
+
111
+ class Unrecognized < Base
112
+
113
+ def self.pattern
114
+ /^(.+)$/
115
+ end
116
+
117
+ def initialize(chunk)
118
+ @chunk = chunk
119
+ end
120
+
121
+ def to_s ; @chunk ; end
122
+ def to_version ; nil ; end
123
+ end
124
+
125
+
126
+ end
127
+
128
+ def default_distribution ; "msp" ; end
129
+
130
+ def initialize(basedir, fname)
131
+ @fname = File.join(basedir, fname)
132
+ end
133
+
134
+ attr_reader :fname
135
+
136
+ def read_top_line
137
+ File.open(@fname, 'r') {|f|f.readline}
138
+ end
139
+
140
+ def version_classes
141
+ [Versions::Unreleased, Versions::Stable, Versions::Development]
142
+ end
143
+
144
+ def version
145
+ version_classes.each do |c|
146
+ v = c.new_if_matches(version_string_from_top_line) and
147
+ return v
148
+ end
149
+ Versions::Unrecognized.new(version_string_from_top_line)
150
+ end
151
+
152
+ def distribution_pattern
153
+ /([^ ]+ [^ ]+ )([^;]+)/
154
+ end
155
+
156
+ def distribution
157
+ (m = distribution_pattern.match(read_top_line)) && m[2]
158
+ end
159
+
160
+ def package_name
161
+ /[a-z0-9\-]+/.match(read_top_line)
162
+ end
163
+
164
+ def version_string_from_top_line
165
+ tline = read_top_line
166
+ match = /[a-z\-]+ \(([^\)]+)\)/.match(tline)
167
+
168
+ match && match[1] or
169
+ raise "could not parse version info from #{tline}"
170
+ end
171
+
172
+ def matches(version)
173
+ self.version == version
174
+ end
175
+
176
+ def amend(version, distribution=self.distribution)
177
+ # replace the first line
178
+ tline, *all = File.open(@fname, 'r') {|f|f.readlines}
179
+ cur_version = /^[^\(]+\(([^\)]+).+/.match(tline)[1]
180
+ tline = tline.gsub(cur_version, "#{version}").
181
+ gsub(distribution_pattern) {|m|"#{$1}#{distribution}" }
182
+
183
+ all.unshift(tline)
184
+
185
+ signoff_idx = all.index {|l| /^ -- .+$/.match(l) }
186
+ all[signoff_idx] = create_signoff
187
+
188
+ File.open(@fname, 'w') { |f| all.each {|l|f.puts(l) } }
189
+
190
+ version
191
+ end
192
+
193
+ def add(version, stub, distribution=self.distribution)
194
+ tline = create_top_line(version, distribution)
195
+ all = File.open(@fname, 'r') {|f| f.readlines }
196
+ new =
197
+ [
198
+ tline,
199
+ "",
200
+ " * #{stub}",
201
+ "",
202
+ create_signoff + "\n",
203
+ ""
204
+ ]
205
+
206
+ File.open(@fname, 'w') {|f| f.write(new.join("\n")); f.write(all.join) }
207
+
208
+ version
209
+ end
210
+
211
+ def reset_at(project_version)
212
+ Versions::Unreleased.new_from_version(project_version).bump
213
+ end
214
+
215
+ def create_signoff
216
+ date = MSPRelease.time_rfc
217
+ author = MSPRelease.author
218
+ " -- #{author.name} <#{author.email}> #{date}"
219
+ end
220
+
221
+ def create_top_line(v, distribution)
222
+ tline = "#{package_name} (#{v})"
223
+
224
+ # FIXME, un-hardcode this?
225
+ tline + " #{distribution}; urgency=low"
226
+ end
227
+
228
+
229
+ end