├── lib ├── history.yml ├── history │ └── release.rb └── history.rb ├── var ├── CREATED ├── NAME ├── TITLE ├── VERSION ├── created ├── ORGANIZATION ├── SUMMARY ├── AUTHORS ├── COPYRIGHTS ├── REPOSITORIES ├── REQUIREMENTS ├── RESOURCES └── DESCRIPTION ├── pkg ├── .gitignore └── history.gemspec ├── demo ├── applique │ ├── history.rb │ ├── ae.rb │ └── fixtures.rb ├── 00_intro.md └── 01_history.md ├── .ruby ├── Gemfile ├── .gitignore ├── .travis.yml ├── MANIFEST ├── Assembly ├── HISTORY.md ├── LICENSE.txt ├── .index ├── work └── history_file.rb └── README.md /lib/history.yml: -------------------------------------------------------------------------------- 1 | ../.ruby -------------------------------------------------------------------------------- /var/CREATED: -------------------------------------------------------------------------------- 1 | 2010-02-19 -------------------------------------------------------------------------------- /var/NAME: -------------------------------------------------------------------------------- 1 | history 2 | -------------------------------------------------------------------------------- /var/TITLE: -------------------------------------------------------------------------------- 1 | History 2 | -------------------------------------------------------------------------------- /var/VERSION: -------------------------------------------------------------------------------- 1 | 0.3.1 2 | -------------------------------------------------------------------------------- /var/created: -------------------------------------------------------------------------------- 1 | 2006-05-09 -------------------------------------------------------------------------------- /pkg/.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | -------------------------------------------------------------------------------- /var/ORGANIZATION: -------------------------------------------------------------------------------- 1 | Rubyworks 2 | -------------------------------------------------------------------------------- /demo/applique/history.rb: -------------------------------------------------------------------------------- 1 | require 'history' 2 | -------------------------------------------------------------------------------- /var/SUMMARY: -------------------------------------------------------------------------------- 1 | HISTORY and CHANGELOG parser 2 | -------------------------------------------------------------------------------- /var/AUTHORS: -------------------------------------------------------------------------------- 1 | --- 2 | - trans 3 | -------------------------------------------------------------------------------- /demo/applique/ae.rb: -------------------------------------------------------------------------------- 1 | require 'ae' 2 | require 'ae/should' 3 | -------------------------------------------------------------------------------- /var/COPYRIGHTS: -------------------------------------------------------------------------------- 1 | --- 2 | - 2012 Rubyworks (BSD-2-Clause) 3 | 4 | -------------------------------------------------------------------------------- /.ruby: -------------------------------------------------------------------------------- 1 | ruby 1.9.3p327 (2012-11-10 revision 37606) [x86_64-linux] 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec :path=>'pkg' 3 | -------------------------------------------------------------------------------- /var/REPOSITORIES: -------------------------------------------------------------------------------- 1 | --- 2 | upstream: git://github.com/rubyworks/history.git 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .fire/digest 2 | .yardocs 3 | log 4 | tmp 5 | web 6 | work/sandbox 7 | DEMO.md 8 | -------------------------------------------------------------------------------- /var/REQUIREMENTS: -------------------------------------------------------------------------------- 1 | --- 2 | - detroit (build) 3 | - qed (test) 4 | - ae (test) 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | script: "bundle exec qed" 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - rbx 8 | - rbx-19mode 9 | - jruby 10 | - jruby-19mode 11 | -------------------------------------------------------------------------------- /var/RESOURCES: -------------------------------------------------------------------------------- 1 | --- 2 | home: http://rubyworks.github.com/history 3 | code: http://github.com/rubyworks/history 4 | bugs: http://github.com/rubyworks/history/issues 5 | mail: http://groups.google.com/groups/rubyworks-mailinglist 6 | chat: irc://us.chat.freenode.net/rubyworks 7 | 8 | -------------------------------------------------------------------------------- /var/DESCRIPTION: -------------------------------------------------------------------------------- 1 | History is a HISTORY file parser. It can parse common HISTORY file layouts 2 | and provide the contents in a structured model. This can be useful for 3 | a number of things, in particular it can be used to generate tag messages 4 | and add pre-release change lists to release announcements. 5 | -------------------------------------------------------------------------------- /demo/00_intro.md: -------------------------------------------------------------------------------- 1 | # History Gem 2 | 3 | History is a HISTORY file parser. It can parse common HISTORY file layouts 4 | and provide the contents in a structured model. This can be useful for 5 | a number of things, in particular it can be used to generate tag messages 6 | and add pre-release change lists to release announcements. 7 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | #!mast -x *.lock .index .ruby .yardopts bin data demo man lib share spec test [A-Z]*.* 2 | .index 3 | .ruby 4 | demo/00_intro.md 5 | demo/01_history.md 6 | demo/applique/ae.rb 7 | demo/applique/fixtures.rb 8 | demo/applique/history.rb 9 | lib/history/release.rb 10 | lib/history.rb 11 | lib/history.yml 12 | LICENSE.txt 13 | HISTORY.md 14 | README.md 15 | DEMO.md 16 | -------------------------------------------------------------------------------- /Assembly: -------------------------------------------------------------------------------- 1 | --- 2 | email: 3 | file: ~ 4 | subject: ~ 5 | mailto: 6 | - ruby-talk@ruby-lang.org 7 | - rubyworks-mailinglist@googlegroups.com 8 | #parts: [readme] 9 | 10 | gem: 11 | gemspec: pkg/history.gemspec 12 | active: true 13 | 14 | github: 15 | gh_pages: web 16 | 17 | dnote: 18 | title: Source Notes 19 | labels: ~ 20 | output: log/notes.html 21 | 22 | yard: 23 | active: false 24 | yardopts: true 25 | priority: 2 26 | 27 | qed: 28 | files: demo 29 | 30 | qedoc: 31 | title: History Demonstrations 32 | files: demo 33 | output: DEMO.md 34 | 35 | vclog: 36 | active: false 37 | output: 38 | - log/history.html 39 | - log/changes.html 40 | 41 | -------------------------------------------------------------------------------- /demo/applique/fixtures.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | FIXTURE_DIR = 'tmp/example/' 4 | 5 | # Remove the example project if it exists. 6 | Before :all do 7 | FileUtils.rm_r(FIXTURE_DIR) if File.exist?(FIXTURE_DIR) 8 | File.open(FIXTURE_DIR + '/.ruby', 'w'){ |f| f << "" } 9 | end 10 | 11 | When 'Given an empty project directory' do 12 | FileUtils.rm_r(FIXTURE_DIR) if File.exist?(FIXTURE_DIR) 13 | end 14 | 15 | When 'iven a ((([\.\w]+))) project file' do |name, text| 16 | FileUtils.mkdir_p(FIXTURE_DIR) 17 | File.open(FIXTURE_DIR + name, 'w') do |f| 18 | f << text 19 | end 20 | end 21 | 22 | When 'no ((([\.\w]+))) file in a project' do |name| 23 | FileUtils.rm(FIXTURE_DIR + name) 24 | end 25 | 26 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # RELEASE HISTORY 2 | 3 | ## 0.3.1 / 2012-12-19 4 | 5 | This release makes a couple of small improvements. First, the 6 | `Changes` marker no long requires a colon. Second, numerically 7 | index change lists will be serach for from the bottom up, making 8 | their parsing a tad more robust. 9 | 10 | Changes: 11 | 12 | * The `changes` marker no longer needs a colon. 13 | * Numerically index changes are parsed from the bottom up. 14 | 15 | 16 | ## 0.3.0 / 2012-05-27 17 | 18 | New release makes the constructor interface more flexible, in part 19 | by making use of the Pathname library. 20 | 21 | Changes: 22 | 23 | * Improves constructor interface. 24 | 25 | 26 | ## 0.2.0 / 2012-05-25 27 | 28 | This the first usable release. There's actual code now ;) 29 | 30 | Changes: 31 | 32 | * Port code from POM project for initial implementation. 33 | * Place variant from VCLog project in work directory for reference. 34 | 35 | 36 | ## 0.1.0 / 2012-05-24 37 | 38 | This is a place holder release until the first working addition 39 | is ready. 40 | 41 | Changes: 42 | 43 | * Happy (Mock) Birthday! 44 | 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD-2-Clause License 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY Thomas Sawyer ``AS IS'' AND ANY EXPRESS 14 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 15 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 16 | NO EVENT SHALL Thomas Sawyer OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 17 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 18 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 19 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 20 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 22 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | -------------------------------------------------------------------------------- /.index: -------------------------------------------------------------------------------- 1 | --- 2 | type: ruby 3 | revision: 2013 4 | sources: 5 | - var 6 | authors: 7 | - name: trans 8 | email: transfire@gmail.com 9 | organizations: [] 10 | requirements: 11 | - groups: 12 | - build 13 | development: true 14 | name: detroit 15 | - groups: 16 | - test 17 | development: true 18 | name: qed 19 | - groups: 20 | - test 21 | development: true 22 | name: ae 23 | conflicts: [] 24 | alternatives: [] 25 | resources: 26 | - type: home 27 | uri: http://rubyworks.github.com/history 28 | label: Homepage 29 | - type: code 30 | uri: http://github.com/rubyworks/history 31 | label: Source Code 32 | - type: bugs 33 | uri: http://github.com/rubyworks/history/issues 34 | label: Issue Tracker 35 | - type: mail 36 | uri: http://groups.google.com/groups/rubyworks-mailinglist 37 | label: Mailing List 38 | - type: chat 39 | uri: irc://us.chat.freenode.net/rubyworks 40 | label: IRC Channel 41 | repositories: 42 | - name: upstream 43 | scm: git 44 | uri: git://github.com/rubyworks/history.git 45 | categories: [] 46 | paths: 47 | load: 48 | - lib 49 | copyrights: 50 | - holder: Rubyworks 51 | year: '2012' 52 | license: BSD-2-Clause 53 | name: history 54 | title: History 55 | created: '2010-02-19' 56 | summary: HISTORY and CHANGELOG parser 57 | version: 0.3.1 58 | description: ! 'History is a HISTORY file parser. It can parse common HISTORY file 59 | layouts 60 | 61 | and provide the contents in a structured model. This can be useful for 62 | 63 | a number of things, in particular it can be used to generate tag messages 64 | 65 | and add pre-release change lists to release announcements.' 66 | date: '2012-12-18' 67 | -------------------------------------------------------------------------------- /work/history_file.rb: -------------------------------------------------------------------------------- 1 | module VCLog 2 | 3 | # The HistoryFile class will parse a history into an array 4 | # of release tags. Of course to do this, it assumes a specific 5 | # file format. 6 | # 7 | class HistoryFile 8 | 9 | FILE = '{HISTORY,HISTORY.*}' 10 | 11 | LINE = /^[=#]/ 12 | VERS = /(\d+[._])+\d+/ 13 | DATE = /(\d+[-])+\d+/ 14 | 15 | # Alias for `File::FNM_CASEFOLD`. 16 | CASEFOLD = File::FNM_CASEFOLD 17 | 18 | # Release tags. 19 | attr :tags 20 | 21 | # Setup new HistoryFile instance. 22 | def initialize(source=nil) 23 | if File.file?(source) 24 | @file = source 25 | @root = File.dirname(source) 26 | elsif File.directory?(source) 27 | @file = Dir.glob(File.join(source,FILE), CASEFOLD).first 28 | @root = source 29 | else 30 | @file = Dir.glob(FILE).first 31 | @root = Dir.pwd 32 | end 33 | raise "no history file" unless @file 34 | 35 | @tags = extract_tags 36 | end 37 | 38 | # Parse history file. 39 | def extract_tags 40 | tags = [] 41 | desc = '' 42 | text = File.read(@file) 43 | text.lines.each do |line| 44 | if LINE =~ line 45 | vers = (VERS.match(line) || [])[0] 46 | date = (DATE.match(line) || [])[0] 47 | next unless vers 48 | tags << [vers, date, desc = ''] 49 | else 50 | desc << line 51 | end 52 | end 53 | 54 | tags.map do |vers, date, desc| 55 | index = desc.index(/^Changes:/) || desc.index(/^\*/) || desc.size 56 | desc = desc[0...index].strip.fold 57 | #[vers, date, desc] 58 | Tag.new(:name=>vers, :date=>date, :msg=>desc) 59 | end 60 | end 61 | 62 | # 63 | def news 64 | tags.first.message 65 | end 66 | 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Homepage](http://rubyworks.github.com/history) | 2 | [Report Issue](http://github.com/rubyworks/history/issues) | 3 | [Source Code](http://github.com/rubyworks/history) 4 | ( [![Build Status](https://secure.travis-ci.org/rubyworks/indexer.png)](http://travis-ci.org/rubyworks/indexer) ) 5 | 6 | 7 | # History 8 | 9 | History is a HISTORY file parser. It can parse common HISTORY file layouts 10 | and provide the contents in a structured model. This can be useful for 11 | a number of things, in particular it can be used to generate tag messages 12 | and add pre-release change lists to release announcements. 13 | 14 | 15 | ## Usage 16 | 17 | Basic usage is fairly straight forward. Load a history file using 18 | the `History.at` method. 19 | 20 | ```ruby 21 | history = History.at('HISTORY.rdoc') 22 | ``` 23 | 24 | Then you have access the the current release with: 25 | 26 | ```ruby 27 | history.release.header 28 | history.release.notes 29 | history.release.version 30 | history.release.date 31 | history.release.codename 32 | history.release.changes 33 | ``` 34 | 35 | And all releases with with an index: 36 | 37 | ```ruby 38 | history.releases[i].header 39 | history.releases[i].notes 40 | history.releases[i].version 41 | history.releases[i].date 42 | history.releases[i].codename 43 | history.releases[i].changes 44 | ``` 45 | 46 | 47 | ## Contact 48 | 49 | Need help, want to make a suggestion or just shoot the shit about this whole idea, 50 | try one of these channels. 51 | 52 | * [Issue Tracker](http://groups.google.com/groups/rubyworks-mailinglist) 53 | * [Mailing List](http://github.com/rubyworks/history/issues) 54 | * [IRC Channel](irc://us.chat.freenode.net/rubyworks) 55 | 56 | 57 | ## Copyrights 58 | 59 | HIstory is copyrighted open source software. 60 | 61 | Copyright (c) 2012 Rubyworks 62 | 63 | It is distributable and modifiable in accordance with the 64 | [BSD-2-Clause](http://spdx.org/license/bsd-2-clause) license. 65 | 66 | See LICENSE.txt file for details. 67 | -------------------------------------------------------------------------------- /lib/history/release.rb: -------------------------------------------------------------------------------- 1 | class History 2 | 3 | # History release entry. 4 | # 5 | class Release 6 | 7 | # The full text of the release note. 8 | attr :text 9 | 10 | # The header. 11 | attr :header 12 | 13 | # The description. 14 | attr :notes 15 | 16 | # The list of changes. 17 | attr :changes 18 | 19 | # Version number (as a string). 20 | attr :version 21 | 22 | # Release date. 23 | attr :date 24 | 25 | # Nick name of the release, if any. 26 | attr :nickname 27 | 28 | # 29 | def initialize(text) 30 | @text = text.strip 31 | parse 32 | end 33 | 34 | # Returns the complete text. 35 | def to_s 36 | text 37 | end 38 | 39 | private 40 | 41 | # Parse the release text into +header+, +notes+ 42 | # and +changes+ components. 43 | def parse 44 | lines = text.lines.to_a 45 | 46 | @header = lines.shift.strip 47 | 48 | parse_release_stamp(@header) 49 | 50 | # remove blank lines from top 51 | lines.shift until lines.first !~ /^\s+$/ 52 | 53 | idx = find_changes(lines) 54 | 55 | if idx.nil? 56 | @notes = lines.join 57 | @changes = '' 58 | elsif idx > 0 59 | @notes = lines[0...idx].join 60 | @changes = lines[idx..-1].join 61 | else # hmmm... is this ever used? 62 | gap = lines.index{ |line| /^\s*$/ =~ line } 63 | @changes = lines[0...gap].join 64 | @notes = lines[gap..-1].join 65 | end 66 | end 67 | 68 | # Parse out the different components of the header, such 69 | # as `version`, release `date` and release `nick name`. 70 | def parse_release_stamp(text) 71 | # version 72 | if md = /\b(\d+\.\d.*?)(\s|$)/.match(text) 73 | @version = md[1] 74 | end 75 | # date 76 | if md = /\b(\d+\-\d+\-.*?\d)(\s|\W|$)/.match(text) 77 | @date = md[1] 78 | end 79 | # nickname 80 | if md = /\"(.*?)\"/.match(text) 81 | @nickname = md[1] 82 | end 83 | end 84 | 85 | # Find line that looks like the start of a list of changes. 86 | def find_changes(lines) 87 | # look for a `changes` marker 88 | if idx = lines.index{ |line| /^changes\:?\s*$/i =~ line } 89 | return idx 90 | end 91 | 92 | # look for an enumerated list in reverse order 93 | if idx = lines.reverse.index{ |line| /^1\.\ / =~ line } 94 | idx = lines.size - idx - 1 95 | return idx 96 | end 97 | 98 | # look for first outline bullet 99 | if idx = lines.index{ |line| /^\*\ / =~ line } 100 | return idx 101 | end 102 | 103 | return nil 104 | end 105 | 106 | end 107 | 108 | end 109 | -------------------------------------------------------------------------------- /lib/history.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'history/release' 3 | 4 | # The History class is a HISTORY file parser. It parses HISTORY files 5 | # into a structure of individual release sections. 6 | # 7 | # The file is expected to be in RDoc or simple Markdown format with 8 | # each section beginning with a secondary header (`==` or `##`) giving 9 | # *version* and *date* of release, then a *note* followed by a point by 10 | # point outline of *changes*. 11 | # 12 | # For example: 13 | # 14 | # == 1.0.0 / 2009-10-07 15 | # 16 | # Say something about this version. 17 | # 18 | # Changes: 19 | # 20 | # * outline oimportant changelog items 21 | # 22 | # `Changes:` is used as a parsing marker. While optional, it helps the 23 | # parser find the list of changes, rather than looking for an asterisk 24 | # or digit, so that ordered and unordered lists can be used in the note 25 | # section too. 26 | # 27 | # Ideally, this class will be continuely imporved to handle greater 28 | # variety of layout. 29 | # 30 | class History 31 | 32 | # File glob for finding the HISTORY file. 33 | DEFAULT_FILE = '{History}{,.*}' 34 | 35 | # Match against version number string. 36 | HEADER_RE = /^[=#]+\s*v?\d+\.\S+/ 37 | 38 | # Convenience constant for `File::FNM_CASEFOLD`. 39 | CASEFOLD = File::FNM_CASEFOLD 40 | 41 | # Parse history from given text. 42 | def self.parse(text) 43 | new(text.to_s) 44 | end 45 | 46 | # Read and parse history from given file. 47 | def self.read(file) 48 | new(Pathname.new(file)) 49 | end 50 | 51 | # Lookup history file given a project root directory. 52 | # If a history file is not present, assume a default 53 | # file name of `HISTORY`. 54 | def self.at(root=Dir.pwd) 55 | if file = Dir.glob(File.join(root, DEFAULT_FILE), CASEFOLD).first 56 | new(Pathname.new(file)) 57 | else 58 | file = File.join(root, 'HISTORY') 59 | new(:file=>file) 60 | end 61 | end 62 | 63 | # Alias for #at. 64 | def self.find(root=Dir.pwd) 65 | at(root) 66 | end 67 | 68 | # Does a HISTORY file exist? 69 | def self.exist?(path=Dir.pwd) 70 | if File.directory?(path) 71 | Dir.glob(File.join(path, DEFAULT_FILE), CASEFOLD).first 72 | else 73 | File.exist?(path) ? path : false 74 | end 75 | end 76 | 77 | # HISTORY file's path. 78 | attr :file 79 | 80 | # HISTORY file's raw contents. 81 | attr :text 82 | 83 | # List of release entries. 84 | attr :releases 85 | 86 | # New History. 87 | def initialize(io=nil, opts={}) 88 | if Hash === io 89 | opts = io 90 | io = nil 91 | end 92 | 93 | @releases = [] 94 | 95 | case io 96 | when String 97 | parse(io) 98 | when Pathname 99 | @file = io 100 | parse(io.read) 101 | when File 102 | @file = io.path 103 | parse(io.read) 104 | else 105 | parse(io.read) if io 106 | end 107 | 108 | # file can be overidden 109 | @file = opts[:file] if opts.key?(:file) 110 | end 111 | 112 | # Does history file exist? 113 | def exist? 114 | File.file?(@file) 115 | end 116 | 117 | # Parse History text. 118 | def parse(text) 119 | return unless text 120 | releases, entry = [], nil 121 | text.each_line do |line| 122 | if HEADER_RE =~ line 123 | releases << Release.new(entry) if entry 124 | entry = line 125 | else 126 | next unless entry 127 | entry << line 128 | end 129 | end 130 | releases << Release.new(entry) if entry 131 | @releases = releases 132 | end 133 | 134 | # Lookup release by version. 135 | def [](version) 136 | releases.find{ |r| r.version == version } 137 | end 138 | 139 | # Returns first entry in release list. 140 | def release 141 | releases.first 142 | end 143 | 144 | end #class History 145 | -------------------------------------------------------------------------------- /demo/01_history.md: -------------------------------------------------------------------------------- 1 | # History Class 2 | 3 | The history class encapsulates this list of release 4 | made by a project. It parses a text file by the 5 | name of HISTORY into it indivdual relase entries. 6 | 7 | Given a HISTORY project file containing: 8 | 9 | = RELEASE HISTORY 10 | 11 | == 1.2.0 / 2010-10-18 12 | 13 | Some Dandy description of the 1.2.0 release. 14 | This is multiline description. 15 | 16 | Changes: 17 | 18 | * This is change 1. 19 | * This is change 2. 20 | * This is change 3. 21 | 22 | 23 | == 1.1.0 | 2010-06-06 | "Happy Days" 24 | 25 | Some Dandy description of the 1.1.0 release. 26 | This is multiline description. Notice the 27 | header varies from the first. 28 | 29 | The description can even have multiple paragraphs. 30 | 31 | Changes: 32 | 33 | * This is change 1. 34 | * This is change 2. 35 | * This is change 3. 36 | 37 | 38 | == 1.0.0 / 2010-04-30 39 | 40 | Some Dandy description of the 1.0.0 release. 41 | This is multiline description. Notice that 42 | the "changes:" label isn't strictly needed. 43 | 44 | * This is change 1. 45 | * This is change 2. 46 | * This is change 3. 47 | 48 | 49 | == 0.9.0 / 2010-04-10 50 | 51 | Some Dandy description of the 0.9.0 release. 52 | Notice this time that the changes are numerically enumerated. 53 | 54 | 1. This is change 1. 55 | 2. This is change 2. 56 | 3. This is change 3. 57 | 58 | 59 | == 0.8.0 / 2010-02-10 60 | 61 | 1. This is change 1. 62 | 2. This is change 2. 63 | 3. This is change 3. 64 | 65 | Some Dandy description of the 0.8.0 release. 66 | Notice this time that the changes are listed 67 | first and are numerically enumerated. 68 | 69 | 70 | The History class provides an interface to this information. 71 | The initializer takes the root directory for the project 72 | and looks for a file called +HISTORY+, optionally ending 73 | in an extension such as +.txt+ or +.rdoc+, etc. 74 | 75 | history = History.at('tmp/example') 76 | 77 | Now we should have an enumeration of each release entry in 78 | the HISTORY file. 79 | 80 | history.releases.size.assert == 5 81 | 82 | The non-plurual #release method will give us the first entry. 83 | And we can see that it has been parsed into its component 84 | attributes. 85 | 86 | history.release.header.assert == '== 1.2.0 / 2010-10-18' 87 | history.release.notes.assert.index('description of the 1.2.0') 88 | 89 | The header is further parsed into version, date and nickname if given. 90 | 91 | history.release.version.assert == '1.2.0' 92 | history.release.date.assert == '2010-10-18' 93 | 94 | We should see like results for the other release entries. 95 | 96 | history.releases[2].version.assert == '1.0.0' 97 | history.releases[2].date.assert == '2010-04-30' 98 | 99 | history.releases[2].header.assert == '== 1.0.0 / 2010-04-30' 100 | history.releases[2].notes.assert.index('description of the 1.0.0') 101 | history.releases[2].changes.assert.index('This is change 1') 102 | 103 | Even though there are variations in the formats of each entry they are 104 | still parsed correctly. For example the second release has a nick name. 105 | 106 | history.releases[1].nickname.assert == 'Happy Days' 107 | 108 | The next to last entry has it's changes listed before the description. 109 | 110 | history.releases[3].header.assert == '== 0.9.0 / 2010-04-10' 111 | history.releases[3].notes.assert.index('description of the 0.9.0') 112 | history.releases[3].changes.assert.index('This is change 1') 113 | 114 | And the last entry has it's changes listed before the description. 115 | 116 | history.releases[4].header.assert == '== 0.8.0 / 2010-02-10' 117 | history.releases[4].notes.assert.index('description of the 0.8.0') 118 | history.releases[4].changes.assert.index('This is change 1') 119 | 120 | The history parser is farily simplistic, but it is flexibile enough 121 | to parse the most common HISTORY file formats. 122 | 123 | -------------------------------------------------------------------------------- /pkg/history.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | require 'pathname' 5 | 6 | module Indexer 7 | 8 | # Convert index data into a gemspec. 9 | # 10 | # Notes: 11 | # * Assumes all executables are in bin/. 12 | # * Does not yet handle default_executable setting. 13 | # * Does not yet handle platform setting. 14 | # * Does not yet handle required_ruby_version. 15 | # * Support for rdoc entries is weak. 16 | # 17 | class GemspecExporter 18 | 19 | # File globs to include in package --unless a manifest file exists. 20 | FILES = ".index .yardopts alt bin data demo ext features lib man spec test try* [A-Z]*.*" unless defined?(FILES) 21 | 22 | # File globs to omit from FILES. 23 | OMIT = "Config.rb" unless defined?(OMIT) 24 | 25 | # Standard file patterns. 26 | PATTERNS = { 27 | :root => '{.index,Gemfile}', 28 | :bin => 'bin/*', 29 | :lib => 'lib/{**/}*', #.rb', 30 | :ext => 'ext/{**/}extconf.rb', 31 | :doc => '*.{txt,rdoc,md,markdown,tt,textile}', 32 | :test => '{test,spec}/{**/}*.rb' 33 | } unless defined?(PATTERNS) 34 | 35 | # For which revision of indexer spec is this converter intended? 36 | REVISION = 2013 unless defined?(REVISION) 37 | 38 | # 39 | def self.gemspec 40 | new.to_gemspec 41 | end 42 | 43 | # 44 | attr :metadata 45 | 46 | # 47 | def initialize(metadata=nil) 48 | @root_check = false 49 | 50 | if metadata 51 | root_dir = metadata.delete(:root) 52 | if root_dir 53 | @root = root_dir 54 | @root_check = true 55 | end 56 | metadata = nil if metadata.empty? 57 | end 58 | 59 | @metadata = metadata || YAML.load_file(root + '.index') 60 | 61 | if @metadata['revision'].to_i != REVISION 62 | warn "This gemspec exporter was not designed for this revision of index metadata." 63 | end 64 | end 65 | 66 | # 67 | def has_root? 68 | root ? true : false 69 | end 70 | 71 | # 72 | def root 73 | return @root if @root || @root_check 74 | @root_check = true 75 | @root = find_root 76 | end 77 | 78 | # 79 | def manifest 80 | return nil unless root 81 | @manifest ||= Dir.glob(root + 'manifest{,.txt}', File::FNM_CASEFOLD).first 82 | end 83 | 84 | # 85 | def scm 86 | return nil unless root 87 | @scm ||= %w{git hg}.find{ |m| (root + ".#{m}").directory? }.to_sym 88 | end 89 | 90 | # 91 | def files 92 | return [] unless root 93 | @files ||= \ 94 | if manifest 95 | File.readlines(manifest). 96 | map{ |line| line.strip }. 97 | reject{ |line| line.empty? || line[0,1] == '#' } 98 | else 99 | list = [] 100 | Dir.chdir(root) do 101 | FILES.split(/\s+/).each do |pattern| 102 | list.concat(glob(pattern)) 103 | end 104 | OMIT.split(/\s+/).each do |pattern| 105 | list = list - glob(pattern) 106 | end 107 | end 108 | list 109 | end.select{ |path| File.file?(path) }.uniq 110 | end 111 | 112 | # 113 | def glob_files(pattern) 114 | return [] unless root 115 | Dir.chdir(root) do 116 | Dir.glob(pattern).select do |path| 117 | File.file?(path) && files.include?(path) 118 | end 119 | end 120 | end 121 | 122 | def patterns 123 | PATTERNS 124 | end 125 | 126 | def executables 127 | @executables ||= \ 128 | glob_files(patterns[:bin]).map do |path| 129 | File.basename(path) 130 | end 131 | end 132 | 133 | def extensions 134 | @extensions ||= \ 135 | glob_files(patterns[:ext]).map do |path| 136 | File.basename(path) 137 | end 138 | end 139 | 140 | def name 141 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 142 | end 143 | 144 | def homepage 145 | page = ( 146 | metadata['resources'].find{ |r| r['type'] =~ /^home/i } || 147 | metadata['resources'].find{ |r| r['name'] =~ /^home/i } || 148 | metadata['resources'].find{ |r| r['name'] =~ /^web/i } 149 | ) 150 | page ? page['uri'] : false 151 | end 152 | 153 | def licenses 154 | metadata['copyrights'].map{ |c| c['license'] }.compact 155 | end 156 | 157 | def require_paths 158 | paths = metadata['paths'] || {} 159 | paths['load'] || ['lib'] 160 | end 161 | 162 | # 163 | # Convert to gemnspec. 164 | # 165 | def to_gemspec 166 | if has_root? 167 | Gem::Specification.new do |gemspec| 168 | to_gemspec_data(gemspec) 169 | to_gemspec_paths(gemspec) 170 | end 171 | else 172 | Gem::Specification.new do |gemspec| 173 | to_gemspec_data(gemspec) 174 | to_gemspec_paths(gemspec) 175 | end 176 | end 177 | end 178 | 179 | # 180 | # Convert pure data settings. 181 | # 182 | def to_gemspec_data(gemspec) 183 | gemspec.name = name 184 | gemspec.version = metadata['version'] 185 | gemspec.summary = metadata['summary'] 186 | gemspec.description = metadata['description'] 187 | 188 | metadata['authors'].each do |author| 189 | gemspec.authors << author['name'] 190 | 191 | if author.has_key?('email') 192 | if gemspec.email 193 | gemspec.email << author['email'] 194 | else 195 | gemspec.email = [author['email']] 196 | end 197 | end 198 | end 199 | 200 | gemspec.licenses = licenses 201 | 202 | requirements = metadata['requirements'] || [] 203 | requirements.each do |req| 204 | next if req['optional'] 205 | next if req['external'] 206 | 207 | name = req['name'] 208 | groups = req['groups'] || [] 209 | 210 | version = gemify_version(req['version']) 211 | 212 | if groups.empty? or groups.include?('runtime') 213 | # populate runtime dependencies 214 | if gemspec.respond_to?(:add_runtime_dependency) 215 | gemspec.add_runtime_dependency(name,*version) 216 | else 217 | gemspec.add_dependency(name,*version) 218 | end 219 | else 220 | # populate development dependencies 221 | if gemspec.respond_to?(:add_development_dependency) 222 | gemspec.add_development_dependency(name,*version) 223 | else 224 | gemspec.add_dependency(name,*version) 225 | end 226 | end 227 | end 228 | 229 | # convert external dependencies into gemspec requirements 230 | requirements.each do |req| 231 | next unless req['external'] 232 | gemspec.requirements << ("%s-%s" % req.values_at('name', 'version')) 233 | end 234 | 235 | gemspec.homepage = homepage 236 | gemspec.require_paths = require_paths 237 | gemspec.post_install_message = metadata['install_message'] 238 | end 239 | 240 | # 241 | # Set gemspec settings that require a root directory path. 242 | # 243 | def to_gemspec_paths(gemspec) 244 | gemspec.files = files 245 | gemspec.extensions = extensions 246 | gemspec.executables = executables 247 | 248 | if Gem::VERSION < '1.7.' 249 | gemspec.default_executable = gemspec.executables.first 250 | end 251 | 252 | gemspec.test_files = glob_files(patterns[:test]) 253 | 254 | unless gemspec.files.include?('.document') 255 | gemspec.extra_rdoc_files = glob_files(patterns[:doc]) 256 | end 257 | end 258 | 259 | # 260 | # Return a copy of this file. This is used to generate a local 261 | # .gemspec file that can automatically read the index file. 262 | # 263 | def self.source_code 264 | File.read(__FILE__) 265 | end 266 | 267 | private 268 | 269 | def find_root 270 | root_files = patterns[:root] 271 | if Dir.glob(root_files).first 272 | Pathname.new(Dir.pwd) 273 | elsif Dir.glob("../#{root_files}").first 274 | Pathname.new(Dir.pwd).parent 275 | else 276 | #raise "Can't find root of project containing `#{root_files}'." 277 | warn "Can't find root of project containing `#{root_files}'." 278 | nil 279 | end 280 | end 281 | 282 | def glob(pattern) 283 | if File.directory?(pattern) 284 | Dir.glob(File.join(pattern, '**', '*')) 285 | else 286 | Dir.glob(pattern) 287 | end 288 | end 289 | 290 | def gemify_version(version) 291 | case version 292 | when /^(.*?)\+$/ 293 | ">= #{$1}" 294 | when /^(.*?)\-$/ 295 | "< #{$1}" 296 | when /^(.*?)\~$/ 297 | "~> #{$1}" 298 | else 299 | version 300 | end 301 | end 302 | 303 | end 304 | 305 | end 306 | 307 | Indexer::GemspecExporter.gemspec --------------------------------------------------------------------------------