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 +4 -4
- data/LICENSE +22 -0
- data/README.md +144 -0
- data/bin/git-releaselog +2 -2
- data/lib/git-releaselog.rb +100 -89
- data/lib/git-releaselog/change.rb +65 -0
- data/lib/git-releaselog/changelog.rb +118 -0
- data/lib/git-releaselog/changelog_helpers.rb +61 -0
- data/lib/git-releaselog/version.rb +5 -0
- metadata +49 -3
- data/lib/changelog.rb +0 -116
- data/lib/changelog_helpers.rb +0 -123
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56cebe4847171a0bb6abab7e5cb47adb936f5a5b
|
4
|
+
data.tar.gz: dfbaf79e1bd1ccecdc6b3925a1cd213fb9d259ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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>"],
|
data/lib/git-releaselog.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
36
|
+
if from_ref
|
37
|
+
logger.info("Found Tag #{from_ref.name} to start from")
|
38
|
+
end
|
45
39
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
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
|
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.
|
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
|
data/lib/changelog_helpers.rb
DELETED
@@ -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
|