git-releaselog 0.6.0

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