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.
@@ -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