git-releaselog 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 567008c8838f03f01eef3ab47173418335f66749
4
- data.tar.gz: 5a92522b789dfe71c4418de08d2afada0687e02a
3
+ metadata.gz: 56cebe4847171a0bb6abab7e5cb47adb936f5a5b
4
+ data.tar.gz: dfbaf79e1bd1ccecdc6b3925a1cd213fb9d259ec
5
5
  SHA512:
6
- metadata.gz: 5c04a68c551ec6bf27e8c3cd611686656fb65608a91b1af0580c9de9938b18ed6f9b555841dc8663180843401d6e7e461bacd85f4cbc57a60dbe8ad9bd13a84c
7
- data.tar.gz: 73d5c5d3251ce70bfedb2f8e8bae6f31c6000d14be7552077ba2cb1ef362abd6324de98be3cce9885be841345907ef759077b2a17a285ccb8653eb3e0fbf0523
6
+ metadata.gz: 2b5ac5be3bc031e8c6bee5b9bd0b720f53cf77addb8b4c0a00ca8e06c73269d1ee792398bb921c5729a14625776af4619abc403d04199cdab40d19a5d7459eb5
7
+ data.tar.gz: a24b234e7cfea87d14a9995f1906e17387075aa4e1c386eab032ff23302daa9ae9847a5c6c3290150441ec2f802ec956bd8166a60f8f729721ac44a253ce8cd3
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Markus Chmelar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # git-releaselog
2
+
3
+ This tool generates release log from a git repository.
4
+
5
+ Generally, I don't beliefe that its possible to generate good releaselog
6
+ from an ordinary git log.
7
+ The git log usually contains very detailed, technical information, targeted
8
+ at the maintainers of a project.
9
+
10
+ In contrast, the releaselog should be targetet at the users of a project and
11
+ should describe changes relevant to those users at a higher abstraction.
12
+
13
+ Thats why this tool does not attempt to build a releaselog from normal commit
14
+ messages but instead requires special keywords to mark notes that should
15
+ show up in the releaselog.
16
+
17
+ These [keywords](#markup) can be used in commit messages. See the [Example](#example)
18
+ section.
19
+
20
+ ## Usage
21
+
22
+ The default use is to generate the releaselog starting from the last release
23
+ (the most recent tag) until now.
24
+
25
+ * `git-releaselog`: will look up the most recent tag and will search all commits from this tag to `HEAD`.
26
+
27
+ If you want to controll the range of commits that should be searched, you can
28
+ specify a _commit-hash_ or _tag-name_, e.g.
29
+
30
+ * `git-releaselog v1.0` will look up the commits starting from the tag with name `v1.0` to `HEAD`.
31
+ * `git-releaselog v1.0 7c064bb` will look up the commits starting from the tag with name `v1.0` to the commit `7c064bb`.
32
+
33
+ Alternatively, you can choose the generate the whole releaselog for the whole repo:
34
+
35
+ * `git-releaselog --complete` will search the whole git log and group the changes nicely in sections by existing tags.
36
+
37
+ To control the markup of the output, you can use these options (the default is slack):
38
+
39
+ * `--slack` produces output that looks nice when copy/pasted into slack
40
+ * `--md` produces markdown output in reverse order, e.g this repo's [releaselog]
41
+
42
+ ## Markup
43
+
44
+ Entries that should show up in the releaselog must have a special format:
45
+
46
+ `* <keyword>: [<optional-scope] <description>`
47
+
48
+ The descriptions are extracted from the git history and grouped by keyword.
49
+ Currently, the following keynotes are supported
50
+
51
+ * `fix`
52
+ * `feat`
53
+ * `gui`
54
+ * `refactor`
55
+
56
+ ### Scope
57
+
58
+ The releaselog can be limited to a certain __scope__. This is helpful when multiple projects / targets are in the same git repository (E.g. several targets of an app with a large shared codebase).
59
+
60
+ When a scope is declared for the generation of the releaselog, only entries that are marked with that scope and entries without scope are included into the releaselog.
61
+
62
+ ### Example
63
+
64
+ Given these lines in a commit message:
65
+
66
+ ```
67
+ * feat: [project-x] add a new feature to project-x
68
+ * fix: [project-y] fix a bug in project-y
69
+ * fix: fix a bug that affected both projects
70
+ ```
71
+ running
72
+ ```
73
+ git-releaselog --scope project-x
74
+ ```
75
+ will generate this releaselog:
76
+
77
+ ```
78
+ *Features*
79
+ * add a new feature to project-x
80
+
81
+ *Fixes*
82
+ * fix a bug that affected both projects
83
+ ```
84
+
85
+ ## Usage suggestion
86
+
87
+ This is just what works best for us:
88
+
89
+ * Use guidelines for git commits, e.g. [AngularJS](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit)
90
+ * Use the [Gitflow workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow)
91
+ * Create Merge Requests for merging feature branches back to develop
92
+ * Feature branch / merge request should address specific features / fixes / ...
93
+ * The description of the merge request should contain markup for the releaselog
94
+ * The description of the merge request should be the commit message of the merge commit (done automatically e.g. by gitlab)
95
+
96
+ The only additional step from our normal workflow is to use special markup for the change log in the description of a merge request.
97
+ Doing so enables the generation of change logs between arbitrary commits / releases
98
+
99
+ ## Example
100
+
101
+ The following is an excerpt of the this repos git log:
102
+
103
+ ```
104
+ commit fa40cdb51c674df8b4a564e283a601d50fcdd55f
105
+ Author: MeXx <mexx@devsub.net>
106
+ Date: Tue May 26 14:04:09 2015 +0200
107
+
108
+ fix(repo): back to the local repo
109
+
110
+ commit 1f4abe3399891cfd429e5aa474e6c414f7e2b3b2
111
+ Author: MeXx <mexx@devsub.net>
112
+ Date: Tue May 26 14:02:47 2015 +0200
113
+
114
+ feat(releaselog): new feature to create a complete releaselog
115
+
116
+ * feat: use the `--complete` parameter to generate a complete releaselog over all tags
117
+
118
+ commit 61fe21959bb52ce09eaf1ee995650c8c4c3b073e
119
+ Author: MeXx <mexx@devsub.net>
120
+ Date: Tue May 26 13:18:10 2015 +0200
121
+
122
+ refactor(searchChanges): moved the function to search the git log into a function
123
+
124
+ commit d41dac909757b265d226589ead6a5a57aba5dc87
125
+ Author: MeXx <mexx@devsub.net>
126
+ Date: Tue May 26 12:49:00 2015 +0200
127
+
128
+ feat(printing): nicer printing of the log
129
+ ```
130
+
131
+ Notice, that commit `1f4abe3399891cfd429e5aa474e6c414f7e2b3b2` has an extra line with a `feat` keyword.
132
+ The releaselog for these commits looks like this:
133
+ `git-releaselog fa40cdb d41dac9 --md`
134
+
135
+ ```
136
+ ## Unreleased (_26.05.2015_)
137
+ #### Fixes
138
+ _No new Fixes_
139
+
140
+ #### Features
141
+ * use the `--complete` parameter to generate a complete releaselog over all tags
142
+ ```
143
+
144
+ [releaselog]: CHANGELOG.md
data/bin/git-releaselog CHANGED
@@ -34,13 +34,13 @@ DOCOPT
34
34
 
35
35
  # Parse Commandline Arguments
36
36
  begin
37
- args = Docopt::docopt(doc, version: '0.6.0')
37
+ args = Docopt::docopt(doc, version: Releaselog::VERSION)
38
38
  rescue Docopt::Exit => e
39
39
  puts e.message
40
40
  exit
41
41
  end
42
42
 
43
- puts Releaselog.generate_releaselog(
43
+ puts Releaselog::Releaselog.generate_releaselog(
44
44
  repo_path: ".",
45
45
  from_ref: args["<from-ref>"],
46
46
  to_ref: args["<to-ref>"],
@@ -1,104 +1,115 @@
1
1
  require "rugged"
2
- require "changelog_helpers"
3
- require "changelog"
4
2
  require "logger"
3
+ require "git-releaselog/changelog_helpers"
4
+ require "git-releaselog/changelog"
5
+ require "git-releaselog/version"
5
6
 
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
7
+ module Releaselog
8
+ class Releaselog
9
+ def self.generate_releaselog(options = {})
10
+ repo_path = options.fetch(:repo_path, '.')
11
+ from_ref_name = options.fetch(:from_ref, nil)
12
+ to_ref_name = options.fetch(:to_ref, nil)
13
+ scope = options.fetch(:scope, nil)
14
+ format = options.fetch(:format, 'slack')
15
+ generate_complete = options.fetch(:generate_complete, false)
16
+ verbose = options.fetch(:verbose, false)
28
17
 
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)
18
+ # Initialize Logger
19
+ logger = Logger.new(STDOUT)
20
+ logger.level = verbose ? Logger::DEBUG : Logger::ERROR
33
21
 
34
- if from_ref
35
- logger.info("Found Tag #{from_ref.name} to start from")
36
- end
22
+ # Initialize Repo
23
+ begin
24
+ repo = Rugged::Repository.discover(repo_path)
25
+ rescue Rugged::OSError => e
26
+ puts ("Current directory is not a git repo")
27
+ logger.error(e.message)
28
+ exit
29
+ end
37
30
 
38
- if to_ref
39
- logger.info("Found Tag #{to_ref.name} to end at")
40
- end
31
+ # Find if we're operating on tags
32
+ from_ref = tagWithName(repo, from_ref_name)
33
+ to_ref = tagWithName(repo, to_ref_name)
34
+ latest_tag = latestTagID(repo, logger)
41
35
 
42
- if latest_tag
43
- logger.info("Latest Tag found: #{latest_tag.name}")
44
- end
36
+ if from_ref
37
+ logger.info("Found Tag #{from_ref.name} to start from")
38
+ end
45
39
 
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)]
40
+ if to_ref
41
+ logger.info("Found Tag #{to_ref.name} to end at")
42
+ end
43
+
44
+ if latest_tag
45
+ logger.info("Latest Tag found: #{latest_tag.name}")
46
+ end
47
+
48
+ if generate_complete && repo.tags.count > 0
49
+ sorted_tags = repo.tags.sort { |t1, t2| t1.target.time <=> t2.target.time }
50
+ changeLogs = []
51
+ sorted_tags.each_with_index do |tag, index|
52
+ logger.error("Tag #{tag.name} with date #{tag.target.time}")
53
+
54
+ if index == 0
55
+ # First Interval: Generate from start of Repo to the first Tag
56
+ changes = searchGitLog(repo, nil, tag.target, scope, logger)
57
+ changeLogs += [Changelog.new(changes, nil, tag, nil, nil)]
58
+
59
+ logger.info("Parsing from start of the repo to #{tag.target.oid}")
60
+ logger.info("First Tag: #{tag.name}: #{changes.count} changes")
61
+ else
62
+ # Normal interval: Generate from one Tag to the next Tag
63
+ previousTag = sorted_tags[index-1]
64
+ changes = searchGitLog(repo, previousTag.target, tag.target, scope, logger)
65
+ changeLogs += [Changelog.new(changes, previousTag, tag, nil, nil)]
66
+
67
+ logger.info("Parsing from #{tag.target.oid} to #{previousTag.target.oid}")
68
+ logger.info("Tag #{previousTag.name} to #{tag.name}: #{changes.count} changes")
69
+ end
61
70
  end
62
- end
63
71
 
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
72
+ if sorted_tags.count > 0
73
+ lastTag = sorted_tags.last
74
+ # Last Interval: Generate from last Tag to HEAD
75
+ changes = searchGitLog(repo, lastTag.target, repo.head.target, scope, logger)
76
+ changeLogs += [Changelog.new(changes, lastTag, nil, nil, nil)]
71
77
 
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
78
+ logger.info("Parsing from #{lastTag.target.oid} to HEAD")
79
+ logger.info("Tag #{lastTag.name} to HEAD: #{changes.count} changes")
80
+ end
81
+
82
+ # Print the changelog
83
+ if format == "md"
84
+ changeLogs.reverse.map { |log| "#{log.to_md}\n" }
85
+ elsif format == "slack"
86
+ changeLogs.reduce("") { |log, version| log + "#{version.to_slack}\n" }
87
+ else
88
+ logger.error("Unknown Format: `#{format}`")
89
+ end
100
90
  else
101
- logger.error("Unknown Format: `#{format}`")
91
+ # From which commit should the log be followed? Will default to the latest tag
92
+ commit_from = (from_ref && from_ref.target) || commit(repo, from_ref, logger) || latest_tag && (latest_tag.target)
93
+
94
+ # To which commit should the log be followed? Will default to HEAD
95
+ commit_to = (to_ref && to_ref.target) || commit(repo, to_ref, logger) || repo.head.target
96
+
97
+
98
+ changes = searchGitLog(repo, commit_from, commit_to, scope, logger)
99
+ # Create the changelog
100
+ log = Changelog.new(changes, from_ref, to_ref || latest_tag, commit_from, commit_to)
101
+
102
+ # Print the changelog
103
+ case format
104
+ when "md"
105
+ log.to_md
106
+ when "slack"
107
+ log.to_slack
108
+ when "raw"
109
+ log
110
+ else
111
+ logger.error("Unknown Format: `#{format}`")
112
+ end
102
113
  end
103
114
  end
104
115
  end
@@ -0,0 +1,65 @@
1
+ # A class for representing a change
2
+ # A change can have a type (fix or feature) and a note describing the change
3
+ module Releaselog
4
+ class Change
5
+ FIX = 1
6
+ FEAT = 2
7
+ GUI = 3
8
+ REFACTOR = 4
9
+
10
+ TOKEN_FIX = "* fix:"
11
+ TOKEN_FEAT = "* feat:"
12
+ TOKEN_GUI = "* gui:"
13
+ TOKEN_REFACTOR = "* refactor:"
14
+
15
+ def initialize(type, note)
16
+ @type = type
17
+ @note = note.strip
18
+ end
19
+
20
+ def type
21
+ @type
22
+ end
23
+
24
+ def note
25
+ @note
26
+ end
27
+
28
+ # Parse a single line as a `Change` entry
29
+ # If the line is formatte correctly as a change entry, a corresponding `Change` object will be created and returned,
30
+ # otherwise, nil will be returned.
31
+ #
32
+ # The additional scope can be used to skip changes of another scope. Changes without scope will always be included.
33
+ def self.parse(line, scope = nil)
34
+ if line.start_with? Change::TOKEN_FEAT
35
+ self.new(Change::FEAT, line.split(Change::TOKEN_FEAT).last).check_scope(scope)
36
+ elsif line.start_with? Change::TOKEN_FIX
37
+ self.new(Change::FIX, line.split(Change::TOKEN_FIX).last).check_scope(scope)
38
+ elsif line.start_with? Change::TOKEN_GUI
39
+ self.new(Change::GUI, line.split(Change::TOKEN_GUI).last).check_scope(scope)
40
+ elsif line.start_with? Change::TOKEN_REFACTOR
41
+ self.new(Change::REFACTOR, line.split(Change::TOKEN_REFACTOR).last).check_scope(scope)
42
+ else
43
+ nil
44
+ end
45
+ end
46
+
47
+ # Checks the scope of the `Change` and the change out if the scope does not match.
48
+ def check_scope(scope = nil)
49
+ # If no scope is requested or the change has no scope include this change unchanged
50
+ return self unless scope
51
+ change_scope = /^\s*\[\w+\]/.match(@note)
52
+ return self unless change_scope
53
+
54
+ # change_scope is a string of format `[scope]`, need to strip the `[]` to compare the scope
55
+ if change_scope[0][1..-2] == scope
56
+ # Change has the scope that is requested, strip the whole scope scope from the change note
57
+ @note = change_scope.post_match.strip
58
+ return self
59
+ else
60
+ # Change has a different scope than requested
61
+ return nil
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,118 @@
1
+ # A class for representing a changelog consisting of several changes
2
+ # over a certain timespan (between two commits)
3
+ module Releaselog
4
+ class Changelog
5
+ def initialize(changes, tag_from = nil, tag_to = nil, from_commit = nil, to_commit = nil)
6
+ @changes_fix = changes.select { |c| c.type == Change::FIX }
7
+ @changes_feat = changes.select { |c| c.type == Change::FEAT }
8
+ @changes_gui = changes.select { |c| c.type == Change::GUI }
9
+ @changes_refactor = changes.select { |c| c.type == Change::REFACTOR }
10
+ @tag_from = tag_from
11
+ @tag_to = tag_to
12
+ @commit_from = from_commit
13
+ @commit_to = to_commit
14
+ end
15
+
16
+ # Returns a hash of the changes.
17
+ # The changes are grouped by change type into `fix`, `feature`, `gui`, `refactor`
18
+ # Each type is a list of changes where each change is the note of that change
19
+ def changes
20
+ {
21
+ fix: @changes_fix.map(&:note),
22
+ feature: @changes_feat.map(&:note),
23
+ gui: @changes_gui.map(&:note),
24
+ refactor: @changes_refactor.map(&:note)
25
+ }
26
+ end
27
+
28
+ # Display tag information about the tag that the changelog is created for
29
+ def tag_info
30
+ if @tag_to && @tag_to.name
31
+ yield("#{@tag_to.name}\n")
32
+ else
33
+ yield("Unreleased\n")
34
+ end
35
+ end
36
+
37
+ # Display tinformation about the commit the changelog is created for
38
+ def commit_info
39
+ if @commit_to
40
+ yield(@commit_to.time.strftime("%d.%m.%Y"))
41
+ else
42
+ yield("")
43
+ end
44
+ end
45
+
46
+ # Format each section from #sections.
47
+ #
48
+ # section_changes ... changes in the format of { section_1: [changes...], section_2: [changes...]}
49
+ # header_style ... is called for styling the header of each section
50
+ # entry_style ... is called for styling each item of a section
51
+ def sections(section_changes, header_style, entry_style)
52
+ str = ""
53
+ section_changes.each do |section_category, section_changes|
54
+ str << section(
55
+ section_changes,
56
+ section_category.to_s,
57
+ entry_style,
58
+ header_style
59
+ )
60
+ end
61
+ str
62
+ end
63
+
64
+ # Format a specific section.
65
+ #
66
+ # section_changes ... changes in the format of { section_1: [changes...], section_2: [changes...]}
67
+ # header ... header of the section
68
+ # entry_style ... is called for styling each item of a section
69
+ # header_style ... optional, since styled header can be passed directly; is called for styling the header of the section
70
+ def section(section_changes, header, entry_style, header_style = nil)
71
+ return "" unless section_changes.size > 0
72
+ str = ""
73
+
74
+ unless header.empty?
75
+ if header_style
76
+ str << header_style.call(header)
77
+ else
78
+ str << header
79
+ end
80
+ end
81
+
82
+ section_changes.each_with_index do |e, i|
83
+ str << entry_style.call(e, i)
84
+ end
85
+ str
86
+ end
87
+
88
+ # Render the Changelog with Slack Formatting
89
+ def to_slack
90
+ str = ""
91
+
92
+ str << tag_info { |t| t }
93
+ str << commit_info { |ci| ci.empty? ? "" : "(_#{ci}_)\n" }
94
+ str << sections(
95
+ changes,
96
+ -> (header) { "*#{header.capitalize}*\n" },
97
+ -> (field, _index) { "\t- #{field}\n" }
98
+ )
99
+
100
+ str
101
+ end
102
+
103
+ # Render the Changelog with Markdown Formatting
104
+ def to_md
105
+ str = ""
106
+
107
+ str << tag_info { |t| "## #{t}" }
108
+ str << commit_info { |ci| ci.empty? ? "" : "(_#{ci}_)\n" }
109
+ str << sections(
110
+ changes,
111
+ -> (header) { "\n*#{header.capitalize}*\n" },
112
+ -> (field, _index) { "* #{field}\n" }
113
+ )
114
+
115
+ str
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,61 @@
1
+ #
2
+ # Helper Functions for git-changelog script
3
+ #
4
+ require "git-releaselog/change"
5
+ include Releaselog
6
+
7
+ # check if the given refString (tag name or commit-hash) exists in the repo
8
+ def commit(repo, refString, logger)
9
+ return unless refString != nil
10
+ begin
11
+ repo.lookup(refString)
12
+ rescue Rugged::OdbError => e
13
+ puts ("Commit `#{refString}` does not exist in Repo")
14
+ logger.error(e.message)
15
+ exit
16
+ rescue Exception => e
17
+ puts ("`#{refString}` is not a valid OID")
18
+ logger.error(e.message)
19
+ exit
20
+ end
21
+ end
22
+
23
+ # Returns the most recent tag
24
+ def latestTagID(repo, logger)
25
+ return nil unless repo.tags.count > 0
26
+ sorted_tags = repo.tags.sort { |t1, t2| t1.target.time <=> t2.target.time }
27
+ sorted_tags.last
28
+ end
29
+
30
+ # Returns the tag with the given name (if exists)
31
+ def tagWithName(repo, name)
32
+ tags = repo.tags.select { |t| t.name == name }
33
+ return tags.first unless tags.count < 1
34
+ end
35
+
36
+ # Parses a commit message and returns an array of Changes
37
+ def parseCommit(commit, scope, logger)
38
+ logger.debug("Parsing Commit #{commit.oid}")
39
+ # Sepaerate into lines, remove whitespaces and filter out empty lines
40
+ lines = commit.message.lines.map(&:strip).reject(&:empty?)
41
+ # Parse the lines
42
+ lines.map{|line| Change.parse(line, scope)}.reject(&:nil?)
43
+ end
44
+
45
+ # Searches the commit log messages of all commits between `commit_from` and `commit_to` for changes
46
+ def searchGitLog(repo, commit_from, commit_to, scope, logger)
47
+ # logger.info("Traversing git tree from commit #{commit_from.oid} to commit #{commit_to && commit_to ? commit_to.oid : '(no oid)'}")
48
+
49
+ # Initialize a walker that walks through the commits from the <from-commit> to the <to-commit>
50
+ walker = Rugged::Walker.new(repo)
51
+ walker.sorting(Rugged::SORT_DATE)
52
+ walker.push(commit_to) unless commit_to == nil
53
+ commit_from.parents.each do |parent|
54
+ walker.hide(parent)
55
+ end unless commit_from == nil
56
+
57
+ # Parse all commits and extract changes
58
+ changes = walker.map{ |c| parseCommit(c, scope, logger)}.reduce(:+) || []
59
+ logger.debug("Found #{changes.count} changes")
60
+ return changes
61
+ end
@@ -0,0 +1,5 @@
1
+ module Releaselog
2
+ VERSION = "0.7.0"
3
+ SUMMARY = "Generate a releaselog from a git repository"
4
+ DESCRIPTION = "Write your releaselog as part of your usual commit messages. This tool generates a useful releaselog from marked lines in your git commit messages"
5
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-releaselog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Markus Chmelar
@@ -44,6 +44,48 @@ dependencies:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
46
  version: 0.23.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: pry
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
47
89
  - !ruby/object:Gem::Dependency
48
90
  name: rspec
49
91
  requirement: !ruby/object:Gem::Requirement
@@ -66,10 +108,14 @@ executables:
66
108
  extensions: []
67
109
  extra_rdoc_files: []
68
110
  files:
111
+ - LICENSE
112
+ - README.md
69
113
  - bin/git-releaselog
70
- - lib/changelog.rb
71
- - lib/changelog_helpers.rb
72
114
  - lib/git-releaselog.rb
115
+ - lib/git-releaselog/change.rb
116
+ - lib/git-releaselog/changelog.rb
117
+ - lib/git-releaselog/changelog_helpers.rb
118
+ - lib/git-releaselog/version.rb
73
119
  homepage: https://github.com/iv-mexx/git-releaselog
74
120
  licenses:
75
121
  - MIT
data/lib/changelog.rb DELETED
@@ -1,116 +0,0 @@
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
@@ -1,123 +0,0 @@
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