git-releaselog 0.6.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 567008c8838f03f01eef3ab47173418335f66749
4
+ data.tar.gz: 5a92522b789dfe71c4418de08d2afada0687e02a
5
+ SHA512:
6
+ metadata.gz: 5c04a68c551ec6bf27e8c3cd611686656fb65608a91b1af0580c9de9938b18ed6f9b555841dc8663180843401d6e7e461bacd85f4cbc57a60dbe8ad9bd13a84c
7
+ data.tar.gz: 73d5c5d3251ce70bfedb2f8e8bae6f31c6000d14be7552077ba2cb1ef362abd6324de98be3cce9885be841345907ef759077b2a17a285ccb8653eb3e0fbf0523
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ require "git-releaselog"
3
+ require "docopt"
4
+
5
+ doc = <<DOCOPT
6
+ A script to generate release-notes from a git repository
7
+
8
+ Commit messages are parsed for lines of the following format:
9
+
10
+ `* fix: [<scope(optional)>] <description>`
11
+ `* feat: [<scope(optional)>] <description>`
12
+ `* gui: [<scope(optional)>] <description>`
13
+ `* refactor: [<scope(optional)>] <description>`
14
+
15
+ The descriptions are collected and printed as releaselog.
16
+
17
+ Usage:
18
+ #{__FILE__} [--complete][--debug][--format=<format>][--scope=<scope>]
19
+ #{__FILE__} <from-ref> [--debug][--format=<format>][--scope=<scope>]
20
+ #{__FILE__} <from-ref> <to-ref> [--debug][--format=<format>][--scope=<scope>]
21
+ #{__FILE__} -h | --help
22
+ #{__FILE__} --version
23
+
24
+ Options:
25
+ from-ref Git-Ref from which should the log be generated. Can be a tag-name or commit-hash. Will default to the latest tag
26
+ to-ref Git-Ref to which the log should be generated. Can be a tag-name or commit-hash. Has to be newer than `from-ref`. Will default to head
27
+ --scope=<scope> The scope. Will only include releaselog entries with that scope or without scope.
28
+ --format=<format> The format in which the output should be generated. Currently supports 'slack' and 'md' (for markdown)
29
+ --complete Traverses the whole git history and generates a releaselog for all tags
30
+ -h --help Show this screen.
31
+ --version Show version.
32
+ --debug Show debug output
33
+ DOCOPT
34
+
35
+ # Parse Commandline Arguments
36
+ begin
37
+ args = Docopt::docopt(doc, version: '0.6.0')
38
+ rescue Docopt::Exit => e
39
+ puts e.message
40
+ exit
41
+ end
42
+
43
+ puts Releaselog.generate_releaselog(
44
+ repo_path: ".",
45
+ from_ref: args["<from-ref>"],
46
+ to_ref: args["<to-ref>"],
47
+ scope: args["--scope"],
48
+ format: args["--format"] || "slack",
49
+ generate_complete: args["--complete"],
50
+ verbose: (args["--debug"] ? true : false)
51
+ )
data/lib/changelog.rb ADDED
@@ -0,0 +1,116 @@
1
+ # A class for representing a changelog consisting of several changes
2
+ # over a certain timespan (between two commits)
3
+ class Changelog
4
+ def initialize(changes, tag_from = nil, tag_to = nil, from_commit = nil, to_commit = nil)
5
+ @changes_fix = changes.select { |c| c.type == Change::FIX }
6
+ @changes_feat = changes.select { |c| c.type == Change::FEAT }
7
+ @changes_gui = changes.select { |c| c.type == Change::GUI }
8
+ @changes_refactor = changes.select { |c| c.type == Change::REFACTOR }
9
+ @tag_from = tag_from
10
+ @tag_to = tag_to
11
+ @commit_from = from_commit
12
+ @commit_to = to_commit
13
+ end
14
+
15
+ # Returns a hash of the changes.
16
+ # The changes are grouped by change type into `fix`, `feature`, `gui`, `refactor`
17
+ # Each type is a list of changes where each change is the note of that change
18
+ def changes
19
+ {
20
+ fix: @changes_fix.map(&:note),
21
+ feature: @changes_feat.map(&:note),
22
+ gui: @changes_gui.map(&:note),
23
+ refactor: @changes_refactor.map(&:note)
24
+ }
25
+ end
26
+
27
+ # Display tag information about the tag that the changelog is created for
28
+ def tag_info
29
+ if @tag_to && @tag_to.name
30
+ yield("#{@tag_to.name}")
31
+ else
32
+ yield("Unreleased")
33
+ end
34
+ end
35
+
36
+ # Display tinformation about the commit the changelog is created for
37
+ def commit_info
38
+ if @commit_to
39
+ yield(@commit_to.time.strftime("%d.%m.%Y"))
40
+ else
41
+ yield("")
42
+ end
43
+ end
44
+
45
+ # Format each section from #sections.
46
+ #
47
+ # section_changes ... changes in the format of { section_1: [changes...], section_2: [changes...]}
48
+ # header_style ... is called for styling the header of each section
49
+ # entry_style ... is called for styling each item of a section
50
+ def sections(section_changes, header_style, entry_style)
51
+ str = ""
52
+ section_changes.each do |section_category, section_changes|
53
+ str << section(
54
+ section_changes,
55
+ section_category.to_s,
56
+ entry_style,
57
+ header_style
58
+ )
59
+ end
60
+ str
61
+ end
62
+
63
+ # Format a specific section.
64
+ #
65
+ # section_changes ... changes in the format of { section_1: [changes...], section_2: [changes...]}
66
+ # header ... header of the section
67
+ # entry_style ... is called for styling each item of a section
68
+ # header_style ... optional, since styled header can be passed directly; is called for styling the header of the section
69
+ def section(section_changes, header, entry_style, header_style = nil)
70
+ return "" unless section_changes.size > 0
71
+ str = ""
72
+
73
+ unless header.empty?
74
+ if header_style
75
+ str << header_style.call(header)
76
+ else
77
+ str << header
78
+ end
79
+ end
80
+
81
+ section_changes.each_with_index do |e, i|
82
+ str << entry_style.call(e, i)
83
+ end
84
+ str
85
+ end
86
+
87
+ # Render the Changelog with Slack Formatting
88
+ def to_slack
89
+ str = ""
90
+
91
+ str << tag_info { |t| t }
92
+ str << commit_info { |ci| " (_#{ci}_)\n" }
93
+ str << sections(
94
+ changes,
95
+ -> (header) { "*#{header.capitalize}*\n" },
96
+ -> (field, _index) { "\t- #{field}\n" }
97
+ )
98
+
99
+ str
100
+ end
101
+
102
+ # Render the Changelog with Markdown Formatting
103
+ def to_md
104
+ str = ""
105
+
106
+ str << tag_info { |t| "## #{t}" }
107
+ str << commit_info { |ci| " (_#{ci}_)" }
108
+ str << sections(
109
+ changes,
110
+ -> (header) { "\n*#{header.capitalize}*\n" },
111
+ -> (field, _index) { "* #{field}\n" }
112
+ )
113
+
114
+ str
115
+ end
116
+ end
@@ -0,0 +1,123 @@
1
+ #
2
+ # Helper Functions for git-changelog script
3
+ #
4
+
5
+ # A class for representing a change
6
+ # A change can have a type (fix or feature) and a note describing the change
7
+ class Change
8
+ FIX = 1
9
+ FEAT = 2
10
+ GUI = 3
11
+ REFACTOR = 4
12
+
13
+ TOKEN_FIX = "* fix:"
14
+ TOKEN_FEAT = "* feat:"
15
+ TOKEN_GUI = "* gui:"
16
+ TOKEN_REFACTOR = "* refactor:"
17
+
18
+ def initialize(type, note)
19
+ @type = type
20
+ @note = note.strip
21
+ end
22
+
23
+ def type
24
+ @type
25
+ end
26
+
27
+ def note
28
+ @note
29
+ end
30
+
31
+ # Parse a single line as a `Change` entry
32
+ # If the line is formatte correctly as a change entry, a corresponding `Change` object will be created and returned,
33
+ # otherwise, nil will be returned.
34
+ #
35
+ # The additional scope can be used to skip changes of another scope. Changes without scope will always be included.
36
+ def self.parse(line, scope = nil)
37
+ if line.start_with? Change::TOKEN_FEAT
38
+ self.new(Change::FEAT, line.split(Change::TOKEN_FEAT).last).check_scope(scope)
39
+ elsif line.start_with? Change::TOKEN_FIX
40
+ self.new(Change::FIX, line.split(Change::TOKEN_FIX).last).check_scope(scope)
41
+ elsif line.start_with? Change::TOKEN_GUI
42
+ self.new(Change::GUI, line.split(Change::TOKEN_GUI).last).check_scope(scope)
43
+ elsif line.start_with? Change::TOKEN_REFACTOR
44
+ self.new(Change::REFACTOR, line.split(Change::TOKEN_REFACTOR).last).check_scope(scope)
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ # Checks the scope of the `Change` and the change out if the scope does not match.
51
+ def check_scope(scope = nil)
52
+ # If no scope is requested or the change has no scope include this change unchanged
53
+ return self unless scope
54
+ change_scope = /^\s*\[\w+\]/.match(@note)
55
+ return self unless change_scope
56
+
57
+ # change_scope is a string of format `[scope]`, need to strip the `[]` to compare the scope
58
+ if change_scope[0][1..-2] == scope
59
+ # Change has the scope that is requested, strip the whole scope scope from the change note
60
+ @note = change_scope.post_match.strip
61
+ return self
62
+ else
63
+ # Change has a different scope than requested
64
+ return nil
65
+ end
66
+ end
67
+ end
68
+
69
+ # check if the given refString (tag name or commit-hash) exists in the repo
70
+ def commit(repo, refString, logger)
71
+ return unless refString != nil
72
+ begin
73
+ repo.lookup(refString)
74
+ rescue Rugged::OdbError => e
75
+ puts ("Commit `#{refString}` does not exist in Repo")
76
+ logger.error(e.message)
77
+ exit
78
+ rescue Exception => e
79
+ puts ("`#{refString}` is not a valid OID")
80
+ logger.error(e.message)
81
+ exit
82
+ end
83
+ end
84
+
85
+ # Returns the most recent tag
86
+ def latestTagID(repo, logger)
87
+ return nil unless repo.tags.count > 0
88
+ sorted_tags = repo.tags.sort { |t1, t2| t1.target.time <=> t2.target.time }
89
+ sorted_tags.last
90
+ end
91
+
92
+ # Returns the tag with the given name (if exists)
93
+ def tagWithName(repo, name)
94
+ tags = repo.tags.select { |t| t.name == name }
95
+ return tags.first unless tags.count < 1
96
+ end
97
+
98
+ # Parses a commit message and returns an array of Changes
99
+ def parseCommit(commit, scope, logger)
100
+ logger.debug("Parsing Commit #{commit.oid}")
101
+ # Sepaerate into lines, remove whitespaces and filter out empty lines
102
+ lines = commit.message.lines.map(&:strip).reject(&:empty?)
103
+ # Parse the lines
104
+ lines.map{|line| Change.parse(line, scope)}.reject(&:nil?)
105
+ end
106
+
107
+ # Searches the commit log messages of all commits between `commit_from` and `commit_to` for changes
108
+ def searchGitLog(repo, commit_from, commit_to, scope, logger)
109
+ logger.info("Traversing git tree from commit #{commit_from.oid} to commit #{commit_to && commit_to.oid}")
110
+
111
+ # Initialize a walker that walks through the commits from the <from-commit> to the <to-commit>
112
+ walker = Rugged::Walker.new(repo)
113
+ walker.sorting(Rugged::SORT_DATE)
114
+ walker.push(commit_to)
115
+ commit_from.parents.each do |parent|
116
+ walker.hide(parent)
117
+ end unless commit_from == nil
118
+
119
+ # Parse all commits and extract changes
120
+ changes = walker.map{ |c| parseCommit(c, scope, logger)}.reduce(:+) || []
121
+ logger.debug("Found #{changes.count} changes")
122
+ return changes
123
+ end
@@ -0,0 +1,105 @@
1
+ require "rugged"
2
+ require "changelog_helpers"
3
+ require "changelog"
4
+ require "logger"
5
+
6
+ class Releaselog
7
+ def self.generate_releaselog(options = {})
8
+ repo_path = options.fetch(:repo_path, '.')
9
+ from_ref_name = options.fetch(:from_ref, nil)
10
+ to_ref_name = options.fetch(:to_ref, nil)
11
+ scope = options.fetch(:scope, nil)
12
+ format = options.fetch(:format, 'slack')
13
+ generate_complete = options.fetch(:generate_complete, false)
14
+ verbose = options.fetch(:verbose, false)
15
+
16
+ # Initialize Logger
17
+ logger = Logger.new(STDOUT)
18
+ logger.level = verbose ? Logger::DEBUG : Logger::ERROR
19
+
20
+ # Initialize Repo
21
+ begin
22
+ repo = Rugged::Repository.discover(repo_path)
23
+ rescue Rugged::OSError => e
24
+ puts ("Current directory is not a git repo")
25
+ logger.error(e.message)
26
+ exit
27
+ end
28
+
29
+ # Find if we're operating on tags
30
+ from_ref = tagWithName(repo, from_ref_name)
31
+ to_ref = tagWithName(repo, to_ref_name)
32
+ latest_tag = latestTagID(repo, logger)
33
+
34
+ if from_ref
35
+ logger.info("Found Tag #{from_ref.name} to start from")
36
+ end
37
+
38
+ if to_ref
39
+ logger.info("Found Tag #{to_ref.name} to end at")
40
+ end
41
+
42
+ if latest_tag
43
+ logger.info("Latest Tag found: #{latest_tag.name}")
44
+ end
45
+
46
+ if generate_complete && repo.tags.count > 0
47
+ sorted_tags = repo.tags.sort { |t1, t2| t1.target.time <=> t2.target.time }
48
+ changeLogs = []
49
+ sorted_tags.each_with_index do |tag, index|
50
+ if index == 0
51
+ # First Interval: Generate from start of Repo to the first Tag
52
+ changes = searchGitLog(repo, tag.target, repo.head.target, scope, logger)
53
+ logger.info("First Tag: #{tag.name}: #{changes.count} changes")
54
+ changeLogs += [Changelog.new(changes, tag, nil, nil, nil)]
55
+ else
56
+ # Normal interval: Generate from one Tag to the next Tag
57
+ previousTag = sorted_tags[index-1]
58
+ changes = searchGitLog(repo, tag.target, previousTag.target, scope, logger)
59
+ logger.info("Tag #{previousTag.name} to #{tag.name}: #{changes.count} changes")
60
+ changeLogs += [Changelog.new(changes, tag, previousTag, nil, nil)]
61
+ end
62
+ end
63
+
64
+ if sorted_tags.count > 0
65
+ lastTag = sorted_tags.last
66
+ # Last Interval: Generate from last Tag to HEAD
67
+ changes = searchGitLog(repo, repo.head.target, lastTag.target, scope, logger)
68
+ logger.info("Tag #{lastTag.name} to HEAD: #{changes.count} changes")
69
+ changeLogs += [Changelog.new(changes, nil, lastTag, nil, nil)]
70
+ end
71
+
72
+ # Print the changelog
73
+ if format == "md"
74
+ changeLogs.reverse.map { |log| "#{log.to_md}\n" }
75
+ elsif format == "slack"
76
+ changeLogs.reduce("") { |log, version| log + "1) #{version.to_slack}\n" }
77
+ else
78
+ logger.error("Unknown Format: `#{format}`")
79
+ end
80
+ else
81
+ # From which commit should the log be followed? Will default to the latest tag
82
+ commit_from = (from_ref && from_ref.target) || commit(repo, from_ref, logger) || latest_tag && (latest_tag.target)
83
+
84
+ # To which commit should the log be followed? Will default to HEAD
85
+ commit_to = (to_ref && to_ref.target) || commit(repo, to_ref, logger) || repo.head.target
86
+
87
+
88
+ changes = searchGitLog(repo, commit_from, commit_to, scope, logger)
89
+ # Create the changelog
90
+ log = Changelog.new(changes, from_ref, to_ref || latest_tag, commit_from, commit_to)
91
+
92
+ # Print the changelog
93
+ case format
94
+ when "md"
95
+ log.to_md
96
+ when "slack"
97
+ log.to_slack
98
+ when "raw"
99
+ log
100
+ else
101
+ logger.error("Unknown Format: `#{format}`")
102
+ end
103
+ end
104
+ end
105
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git-releaselog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Markus Chmelar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: docopt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.5'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.5.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.5'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.5.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: rugged
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.23.0
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.23.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 3.3.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 3.3.0
61
+ description: Write your releaselog as part of your usual commit messages. This tool
62
+ generates a useful releaselog from marked lines in your git commit messages
63
+ email: markus.chmelar@innovaptor.com
64
+ executables:
65
+ - git-releaselog
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - bin/git-releaselog
70
+ - lib/changelog.rb
71
+ - lib/changelog_helpers.rb
72
+ - lib/git-releaselog.rb
73
+ homepage: https://github.com/iv-mexx/git-releaselog
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 2.4.8
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: Generate a releaselog from a git repository
97
+ test_files: []