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