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.
Files changed (50) hide show
  1. data/.document +5 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +35 -0
  6. data/LICENSE +20 -0
  7. data/LICENSE.txt +20 -0
  8. data/README.rdoc +149 -0
  9. data/Rakefile +2 -0
  10. data/VERSION +1 -0
  11. data/autotest/discover.rb +1 -0
  12. data/bin/historian +12 -0
  13. data/historian.gemspec +28 -0
  14. data/lib/historian.rb +12 -0
  15. data/lib/historian/cli.rb +84 -0
  16. data/lib/historian/commit_message.rb +40 -0
  17. data/lib/historian/configuration.rb +10 -0
  18. data/lib/historian/git.rb +189 -0
  19. data/lib/historian/history_file.rb +209 -0
  20. data/lib/historian/version.rb +3 -0
  21. data/spec/example_history.txt +24 -0
  22. data/spec/fixtures/after_0_0_2 +16 -0
  23. data/spec/fixtures/after_0_0_2_changelog +6 -0
  24. data/spec/fixtures/after_0_0_2_history +17 -0
  25. data/spec/fixtures/all_types_history +17 -0
  26. data/spec/fixtures/anonymous_release_log +7 -0
  27. data/spec/fixtures/arbitrary_text +21 -0
  28. data/spec/fixtures/courageous_camel_history +13 -0
  29. data/spec/fixtures/courageous_camel_release_log +7 -0
  30. data/spec/fixtures/empty +0 -0
  31. data/spec/fixtures/invalid_significance +17 -0
  32. data/spec/fixtures/missing_significance +16 -0
  33. data/spec/fixtures/normal +10 -0
  34. data/spec/fixtures/normal_changelog_after_major +11 -0
  35. data/spec/fixtures/normal_changelog_after_minor +8 -0
  36. data/spec/fixtures/normal_history_after_major +17 -0
  37. data/spec/fixtures/normal_history_after_minor +14 -0
  38. data/spec/fixtures/patch_on_nothing +5 -0
  39. data/spec/fixtures/release_on_nothing +1 -0
  40. data/spec/fixtures/second_patch_on_nothing +6 -0
  41. data/spec/historian/cli_spec.rb +210 -0
  42. data/spec/historian/commit_message_spec.rb +95 -0
  43. data/spec/historian/git_spec.rb +199 -0
  44. data/spec/historian/history_file_spec.rb +225 -0
  45. data/spec/spec.opts +3 -0
  46. data/spec/spec_helper.rb +21 -0
  47. data/spec/support/fixture_helper.rb +8 -0
  48. data/spec/support/git_helpers.rb +53 -0
  49. data/spec/support/history_file_matchers.rb +52 -0
  50. 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,3 @@
1
+ module Historian
2
+ VERSION = "0.0.1"
3
+ 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
+
@@ -0,0 +1,16 @@
1
+ == In Git
2
+
3
+ === Bugfixes
4
+ * bugfix #1
5
+
6
+
7
+ == 0.0.2 Bubbling Baboon
8
+
9
+ === Bugfixes
10
+ * more fur
11
+
12
+
13
+ == 0.0.1 Addled Adder
14
+
15
+ === Bugfixes
16
+ * more scales
@@ -0,0 +1,6 @@
1
+ == In Git
2
+
3
+ === Bugfixes
4
+ * bugfix #1
5
+ * bugfix #2
6
+