historian 0.0.1
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.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +35 -0
- data/LICENSE +20 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +149 -0
- data/Rakefile +2 -0
- data/VERSION +1 -0
- data/autotest/discover.rb +1 -0
- data/bin/historian +12 -0
- data/historian.gemspec +28 -0
- data/lib/historian.rb +12 -0
- data/lib/historian/cli.rb +84 -0
- data/lib/historian/commit_message.rb +40 -0
- data/lib/historian/configuration.rb +10 -0
- data/lib/historian/git.rb +189 -0
- data/lib/historian/history_file.rb +209 -0
- data/lib/historian/version.rb +3 -0
- data/spec/example_history.txt +24 -0
- data/spec/fixtures/after_0_0_2 +16 -0
- data/spec/fixtures/after_0_0_2_changelog +6 -0
- data/spec/fixtures/after_0_0_2_history +17 -0
- data/spec/fixtures/all_types_history +17 -0
- data/spec/fixtures/anonymous_release_log +7 -0
- data/spec/fixtures/arbitrary_text +21 -0
- data/spec/fixtures/courageous_camel_history +13 -0
- data/spec/fixtures/courageous_camel_release_log +7 -0
- data/spec/fixtures/empty +0 -0
- data/spec/fixtures/invalid_significance +17 -0
- data/spec/fixtures/missing_significance +16 -0
- data/spec/fixtures/normal +10 -0
- data/spec/fixtures/normal_changelog_after_major +11 -0
- data/spec/fixtures/normal_changelog_after_minor +8 -0
- data/spec/fixtures/normal_history_after_major +17 -0
- data/spec/fixtures/normal_history_after_minor +14 -0
- data/spec/fixtures/patch_on_nothing +5 -0
- data/spec/fixtures/release_on_nothing +1 -0
- data/spec/fixtures/second_patch_on_nothing +6 -0
- data/spec/historian/cli_spec.rb +210 -0
- data/spec/historian/commit_message_spec.rb +95 -0
- data/spec/historian/git_spec.rb +199 -0
- data/spec/historian/history_file_spec.rb +225 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/fixture_helper.rb +8 -0
- data/spec/support/git_helpers.rb +53 -0
- data/spec/support/history_file_matchers.rb +52 -0
- metadata +189 -0
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
class Historian::Git
|
6
|
+
class Hooker
|
7
|
+
attr_accessor :hook, :repo_dir
|
8
|
+
|
9
|
+
def initialize(repo_dir, hook)
|
10
|
+
self.repo_dir = repo_dir
|
11
|
+
self.hook = hook
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_hook_script
|
15
|
+
File.open(hook_script, "w") do |f|
|
16
|
+
f.puts "#!/bin/bash"
|
17
|
+
f.puts "historian #{hook.to_s} $@"
|
18
|
+
end
|
19
|
+
File.chmod 0755, hook_script
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_hook_scripts_dir
|
23
|
+
Dir.mkdir hook_scripts_dir unless File.directory? hook_scripts_dir
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_hook_wrapper_script
|
27
|
+
File.open(hook_wrapper_script, "w") do |f|
|
28
|
+
script = <<-EOF
|
29
|
+
#!/bin/bash
|
30
|
+
#
|
31
|
+
# Git hook modified by Historian. Do not modify this file.
|
32
|
+
# Add more hooks in the #{hook_to_git}.d directory.
|
33
|
+
#
|
34
|
+
BASE=`dirname $0`
|
35
|
+
for S in $BASE/#{hook_to_git}.d/*
|
36
|
+
do
|
37
|
+
$S $@ || exit $?
|
38
|
+
done
|
39
|
+
EOF
|
40
|
+
f.puts script.gsub(/^\s+/,'')
|
41
|
+
end
|
42
|
+
File.chmod 0755, hook_wrapper_script
|
43
|
+
end
|
44
|
+
|
45
|
+
def other_git_hook_exists?
|
46
|
+
File.exists?(hook_wrapper_script) && !File.directory?(hook_scripts_dir)
|
47
|
+
end
|
48
|
+
|
49
|
+
def hook_script
|
50
|
+
File.join hook_scripts_dir, "historian"
|
51
|
+
end
|
52
|
+
|
53
|
+
def hook_scripts_dir
|
54
|
+
File.join hooks_dir, hook_to_git + ".d"
|
55
|
+
end
|
56
|
+
|
57
|
+
def hook_to_git
|
58
|
+
hook.to_s.gsub "_", "-"
|
59
|
+
end
|
60
|
+
|
61
|
+
def hook_wrapper_script
|
62
|
+
File.join hooks_dir, hook_to_git
|
63
|
+
end
|
64
|
+
|
65
|
+
def hooks_dir
|
66
|
+
File.join repo_dir, ".git", "hooks"
|
67
|
+
end
|
68
|
+
|
69
|
+
def install
|
70
|
+
create_hook_scripts_dir
|
71
|
+
create_hook_script
|
72
|
+
create_hook_wrapper_script
|
73
|
+
end
|
74
|
+
|
75
|
+
def installed?
|
76
|
+
File.exists? hook_script
|
77
|
+
end
|
78
|
+
|
79
|
+
def preserve_original
|
80
|
+
create_hook_scripts_dir
|
81
|
+
FileUtils.cp hook_wrapper_script, File.join(hook_scripts_dir, "original")
|
82
|
+
end
|
83
|
+
|
84
|
+
def update
|
85
|
+
create_hook_script
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
attr_reader :repo_directory, :history
|
91
|
+
|
92
|
+
def bundle_history_file
|
93
|
+
amend_history_changes
|
94
|
+
end
|
95
|
+
|
96
|
+
def initialize(dir, history)
|
97
|
+
@repo_directory = dir
|
98
|
+
@history = history
|
99
|
+
end
|
100
|
+
|
101
|
+
def install_hook(hook)
|
102
|
+
hook = Hooker.new(repo_directory, hook)
|
103
|
+
if hook.other_git_hook_exists?
|
104
|
+
hook.preserve_original
|
105
|
+
hook.install
|
106
|
+
:adapted
|
107
|
+
elsif !hook.installed?
|
108
|
+
hook.install
|
109
|
+
:created
|
110
|
+
else
|
111
|
+
hook.update
|
112
|
+
:exists
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def tag_release
|
117
|
+
commit_history_changes if history_dirty?
|
118
|
+
ensure_history_has_release
|
119
|
+
Tempfile.open("historian") do |file|
|
120
|
+
file.puts commit_message_for_tag
|
121
|
+
file.flush
|
122
|
+
git "tag", "-a", tag, "-F", file.path
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
protected
|
127
|
+
|
128
|
+
def amend_history_changes
|
129
|
+
Tempfile.open("historian") do |file|
|
130
|
+
git "add", history.path
|
131
|
+
git "commit", history.path, "--amend", "-C", "HEAD"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def commit_history_changes
|
136
|
+
Tempfile.open("historian") do |file|
|
137
|
+
file.puts commit_message_for_history
|
138
|
+
file.flush
|
139
|
+
git "add", history.path
|
140
|
+
git "commit", history.path, "-F", file.path
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def history_dirty?
|
145
|
+
git("status", "--porcelain", history.path) =~ /^.M/
|
146
|
+
end
|
147
|
+
|
148
|
+
def hook_invokes_command?(command)
|
149
|
+
return false unless File.exists?(hook_script_for command)
|
150
|
+
lines = File.readlines(hook_script_for command).collect { |l| l.chomp }
|
151
|
+
lines.first == "#!/bin/bash" && lines.grep("historian #{command} $@")
|
152
|
+
end
|
153
|
+
|
154
|
+
def hook_script_for(hook)
|
155
|
+
File.join repo_directory, ".git/hooks", hook.to_s.gsub("_", "-")
|
156
|
+
end
|
157
|
+
|
158
|
+
def tag
|
159
|
+
'v' + history.current_version
|
160
|
+
end
|
161
|
+
|
162
|
+
def release_name
|
163
|
+
history.current_release_name
|
164
|
+
end
|
165
|
+
|
166
|
+
def commit_message_for_history
|
167
|
+
"update history for release"
|
168
|
+
end
|
169
|
+
|
170
|
+
def commit_message_for_tag
|
171
|
+
message = "tagged #{tag}"
|
172
|
+
if release_name
|
173
|
+
message += %{ "#{release_name}"}
|
174
|
+
end
|
175
|
+
message + "\n\n" + history.release_log
|
176
|
+
end
|
177
|
+
|
178
|
+
def ensure_history_has_release
|
179
|
+
if !history.changelog.empty?
|
180
|
+
history.release
|
181
|
+
commit_history_changes
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def git(*args)
|
186
|
+
Dir.chdir repo_directory
|
187
|
+
%x(git #{args.collect { |a| Shellwords.escape a }.join " "})
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module Historian
|
2
|
+
module HistoryFile
|
3
|
+
# Example:
|
4
|
+
#
|
5
|
+
# history = File.open("History.txt", "w+") # (empty file!)
|
6
|
+
# history.extend Historian::HistoryFile
|
7
|
+
# history.next_version # => "0.0.1"
|
8
|
+
# history.update_history :minor => "added whizbangery",
|
9
|
+
# :release => "Awesome Release"
|
10
|
+
# history.rewind
|
11
|
+
# puts history.read
|
12
|
+
# ## 0.1.0 Awesome Release - 2010/12/12
|
13
|
+
#
|
14
|
+
# ### Minor Changes
|
15
|
+
# * added whizbangery
|
16
|
+
#
|
17
|
+
# Note that the "w+" mode is critically important if you're
|
18
|
+
# extending a File object. The IO instance must be both
|
19
|
+
# readable and writeable.
|
20
|
+
|
21
|
+
SECTION_HEADERS = {
|
22
|
+
:major => "Major Changes",
|
23
|
+
:minor => "Minor Changes",
|
24
|
+
:patch => "Bugfixes"
|
25
|
+
}
|
26
|
+
|
27
|
+
# The pending changelog for the next release. See also #release_log.
|
28
|
+
def changelog
|
29
|
+
return "" unless changes? || @release
|
30
|
+
|
31
|
+
log = []
|
32
|
+
if @release
|
33
|
+
release_string = @release === true ? "" : " " + @release
|
34
|
+
date_string = " - " + Time.now.strftime("%Y/%m/%d")
|
35
|
+
log << "== #{next_version}#{release_string}#{date_string}"
|
36
|
+
else
|
37
|
+
log << "== In Git"
|
38
|
+
end
|
39
|
+
[ :major, :minor, :patch ].each do |significance|
|
40
|
+
unless changes[significance].empty?
|
41
|
+
log << "\n=== #{SECTION_HEADERS[significance]}"
|
42
|
+
changes[significance].each { |change| log << "* #{change}" }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
log.join("\n")
|
46
|
+
end
|
47
|
+
|
48
|
+
def changes #:nodoc:
|
49
|
+
@changes ||= {
|
50
|
+
:major => [],
|
51
|
+
:minor => [],
|
52
|
+
:patch => []
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Whether there are changes since the last release
|
57
|
+
def changes?
|
58
|
+
parse unless parsed?
|
59
|
+
changes.find { |(k,v)| !v.empty? }
|
60
|
+
end
|
61
|
+
|
62
|
+
# The current (most-recently released) version number in "x.y.z" format
|
63
|
+
def current_version
|
64
|
+
parse unless parsed?
|
65
|
+
@current_version || "0.0.0"
|
66
|
+
end
|
67
|
+
|
68
|
+
# The next (upcoming release) version number, based on what
|
69
|
+
# pending changes currently exist. Major changes will increment
|
70
|
+
# the major number, else minor changes will increment the minor
|
71
|
+
# number, else the patch number is incremented.
|
72
|
+
def next_version
|
73
|
+
parse unless parsed?
|
74
|
+
(major, minor, patch) = current_version.split(".").collect { |n| n.to_i }
|
75
|
+
if !changes[:major].empty?
|
76
|
+
major += 1
|
77
|
+
patch = minor = 0
|
78
|
+
elsif !changes[:minor].empty?
|
79
|
+
minor += 1
|
80
|
+
patch = 0
|
81
|
+
else
|
82
|
+
patch += 1
|
83
|
+
end
|
84
|
+
"%d.%d.%d" % [ major, minor, patch ]
|
85
|
+
end
|
86
|
+
|
87
|
+
# whether the history file has already been parsed
|
88
|
+
def parsed?
|
89
|
+
@parsed
|
90
|
+
end
|
91
|
+
|
92
|
+
# Parse the entire history file. There's generally no
|
93
|
+
# need to call this method -- it should be called automatically
|
94
|
+
# when needed.
|
95
|
+
def parse
|
96
|
+
rewind
|
97
|
+
@changes = nil
|
98
|
+
@buffer = []
|
99
|
+
@release_log = []
|
100
|
+
state = :gathering_current_history
|
101
|
+
significance = nil
|
102
|
+
each_line do |line|
|
103
|
+
if state == :gathering_current_history
|
104
|
+
case line
|
105
|
+
when /^== ([0-9]+\.[0-9]+\.[0-9]+)(.*)/
|
106
|
+
state = :gathering_previous_release_log
|
107
|
+
@release_log << line
|
108
|
+
@buffer << line
|
109
|
+
@current_version = $1
|
110
|
+
if $2 =~ %r{ (.*) - [0-9]{4}/[0-9]{2}/[0-9]{2}}
|
111
|
+
@current_release_name = $1
|
112
|
+
end
|
113
|
+
when /^=== (.*)$/
|
114
|
+
significance = significance_for $1
|
115
|
+
when /^\* (.+)$/
|
116
|
+
if significance
|
117
|
+
changes[significance] << $1
|
118
|
+
else
|
119
|
+
raise ParseError.new "no significance preceeding history entry '#{$1}'"
|
120
|
+
end
|
121
|
+
when /^\s*$/, /^== In Git$/
|
122
|
+
#noop
|
123
|
+
else
|
124
|
+
raise ParseError.new("invalid history format: #{line}")
|
125
|
+
end
|
126
|
+
elsif state == :gathering_previous_release_log
|
127
|
+
if line =~ /^== ([0-9]+\.[0-9]+\.[0-9]+)(.*)/
|
128
|
+
state = :gathering_remainder
|
129
|
+
else
|
130
|
+
@release_log << line
|
131
|
+
end
|
132
|
+
@buffer << line
|
133
|
+
else
|
134
|
+
@buffer << line
|
135
|
+
end
|
136
|
+
end
|
137
|
+
@release_log = @release_log.join
|
138
|
+
@parsed = true
|
139
|
+
end
|
140
|
+
|
141
|
+
# Alias for update_history :release => true
|
142
|
+
def release
|
143
|
+
update_history :release => true
|
144
|
+
end
|
145
|
+
|
146
|
+
# If a release was just performed, this will return the changelog
|
147
|
+
# of the release. Otherwise, this is always nil.
|
148
|
+
def release_log
|
149
|
+
parse unless parsed?
|
150
|
+
@release_log
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns the name of the current release, either as parsed
|
154
|
+
# from the history file, or as provided if a release was
|
155
|
+
# recently performed.
|
156
|
+
def current_release_name
|
157
|
+
parse unless parsed?
|
158
|
+
@current_release_name
|
159
|
+
end
|
160
|
+
|
161
|
+
# Update the release history with new release information.
|
162
|
+
# Accepts a hash with any (or all) of four key-value pairs.
|
163
|
+
# [:major] a message describing a major change
|
164
|
+
# [:major] a message describing a minor change
|
165
|
+
# [:patch] a message describing a bugfix or other tiny change
|
166
|
+
# [:release] indicates this history should be updated for a new release, if present. If true, the release has no name, but if a string is provided the release will be annotated with the string
|
167
|
+
#
|
168
|
+
# To add multiple history entries of the same type, call
|
169
|
+
# this method mutiple times.
|
170
|
+
#
|
171
|
+
# *Note*: this method rewrites the entire IO instance to
|
172
|
+
# prepend the new history information.
|
173
|
+
def update_history(history)
|
174
|
+
parse unless parsed?
|
175
|
+
@release = history.delete(:release)
|
176
|
+
history.each do |(significance,message)|
|
177
|
+
changes[significance] << message
|
178
|
+
end
|
179
|
+
rewrite
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
protected
|
184
|
+
|
185
|
+
def rewrite #:nodoc:
|
186
|
+
rewind
|
187
|
+
puts changelog
|
188
|
+
puts ["\n"] * 2
|
189
|
+
puts @buffer
|
190
|
+
truncate pos
|
191
|
+
if @release
|
192
|
+
@release_log = changelog
|
193
|
+
@current_release_name = @release if @release.kind_of?(String)
|
194
|
+
parse
|
195
|
+
@release = nil
|
196
|
+
@changes = nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def significance_for(string)
|
201
|
+
@@translation ||= SECTION_HEADERS.invert
|
202
|
+
if @@translation[string]
|
203
|
+
return @@translation[string]
|
204
|
+
else
|
205
|
+
raise ParseError.new("unknown significance '#{string}'")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
This is some text at the top of the history file. It should be left alone.
|
2
|
+
|
3
|
+
== In Git
|
4
|
+
|
5
|
+
This is a release section, representing in-progress (unreleased changes). This
|
6
|
+
paragraph should be left alone.
|
7
|
+
|
8
|
+
=== A Section
|
9
|
+
* this is an entry in a section
|
10
|
+
|
11
|
+
This is more freeform text. A section, like the one above, categorizes the
|
12
|
+
information in the list below it. Common section names might include things
|
13
|
+
like "New Features", "Bugfixes", "Minor Enhancements", etc.
|
14
|
+
|
15
|
+
=== Another Section
|
16
|
+
* this is an entry too
|
17
|
+
* and another entry
|
18
|
+
|
19
|
+
== 1.0.0 2009-01-01
|
20
|
+
|
21
|
+
This is some freeform text that is part of an entirely different release section.
|
22
|
+
Specifically, this is release 1.0.0, which it should be possible to find via
|
23
|
+
'historian status 1.0.0'. The status command should ignore the date.
|
24
|
+
|