git-releaselog 0.6.0 → 0.7.0

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