historian 0.0.1

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