1+ # git-cliff ~ configuration file
2+ # https://git-cliff.org/docs/configuration
3+
4+ [remote .github ]
5+ owner = " space-code"
6+ repo = " concurrency"
7+ # If you are using a token to fetch GitHub usernames, uncomment below:
8+ # token = "${GITHUB_TOKEN}"
9+
10+ [changelog ]
11+ # GUARANTEE: Skip releases if they contain no commits (for tagged versions)
12+ skip_empty_releases = true
13+ # Maximum number of releases to display in the changelog
14+ # number_of_releases = 10
15+
16+ header = """
17+ # Changelog
18+
19+ All notable changes to this project will be documented in this file.
20+
21+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
22+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
23+
24+ {#- LOGIC: Generate table of contents - group versions by major version -#}
25+ {%- set_global major_versions = [] -%}
26+ {%- for release in releases -%}
27+ {%- if release.version -%}
28+ {%- set version_clean = release.version | trim_start_matches(pat="v") -%}
29+ {%- set major = version_clean | split(pat=".") | first -%}
30+ {%- if major not in major_versions -%}
31+ {%- set_global major_versions = major_versions | concat(with=major) -%}
32+ {%- endif -%}
33+ {%- endif -%}
34+ {%- endfor -%}
35+ {%- set sorted_majors = major_versions | sort | reverse -%}
36+
37+ {#- MAIN LOOP: Iterate over major versions -#}
38+ {%- for major in sorted_majors -%}
39+ {#- VISUAL: Add double newline before the header to separate from previous block -#}
40+ {{ "\n\n " }}#### {{ major }}.x Releases
41+
42+ {#- LOGIC: Filter releases for the current major version -#}
43+ {%- set_global major_releases = [] -%}
44+ {%- for release in releases -%}
45+ {%- if release.version -%}
46+ {%- set version_clean = release.version | trim_start_matches(pat="v") -%}
47+ {%- set rel_major = version_clean | split(pat=".") | first -%}
48+ {%- if rel_major == major -%}
49+ {%- set_global major_releases = major_releases | concat(with=version_clean) -%}
50+ {%- endif -%}
51+ {%- endif -%}
52+ {%- endfor -%}
53+
54+ {#- LOGIC: Separate into Stable, RC, and Beta -#}
55+ {%- set_global stable_versions = [] -%}
56+ {%- set_global rc_versions = [] -%}
57+ {%- set_global beta_versions = [] -%}
58+ {%- for version in major_releases -%}
59+ {%- if version is containing("-rc") -%}
60+ {%- set_global rc_versions = rc_versions | concat(with=version) -%}
61+ {%- elif version is containing("-beta") -%}
62+ {%- set_global beta_versions = beta_versions | concat(with=version) -%}
63+ {%- else -%}
64+ {%- set_global stable_versions = stable_versions | concat(with=version) -%}
65+ {%- endif -%}
66+ {%- endfor -%}
67+
68+ {#- LOGIC: Group stable versions by minor version -#}
69+ {%- set_global minor_versions = [] -%}
70+ {%- for version in stable_versions -%}
71+ {%- set parts = version | split(pat=".") -%}
72+ {%- set minor_key = parts | slice(end=2) | join(sep=".") -%}
73+ {%- if minor_key not in minor_versions -%}
74+ {%- set_global minor_versions = minor_versions | concat(with=minor_key) -%}
75+ {%- endif -%}
76+ {%- endfor -%}
77+ {%- set sorted_minors = minor_versions | sort | reverse -%}
78+
79+ {#- OUTPUT: Stable releases -#}
80+ {%- for minor_key in sorted_minors -%}
81+ {%- set_global minor_release_versions = [] -%}
82+ {%- for version in stable_versions -%}
83+ {%- set parts = version | split(pat=".") -%}
84+ {%- set ver_minor = parts | slice(end=2) | join(sep=".") -%}
85+ {%- if ver_minor == minor_key -%}
86+ {%- set_global minor_release_versions = minor_release_versions | concat(with=version) -%}
87+ {%- endif -%}
88+ {%- endfor -%}
89+ {%- set versions_list = minor_release_versions | sort | reverse -%}
90+ {{ "\n " }}- `{{ minor_key }}.x` Releases - {% for version in versions_list -%}
91+ [{{ version }}](#{{ version | replace(from=".", to="") | replace(from="-", to="") | lower }})
92+ {%- if not loop.last %} | {% endif -%}
93+ {%- endfor -%}
94+ {%- endfor -%}
95+
96+ {#- OUTPUT: RC versions -#}
97+ {%- if rc_versions | length > 0 -%}
98+ {%- set rc_versions_sorted = rc_versions | sort | reverse -%}
99+ {%- set rc_base = rc_versions_sorted | first | split(pat="-") | first -%}
100+ {{ "\n " }}- `{{ rc_base }}` Release Candidates - {% for version in rc_versions_sorted -%}
101+ [{{ version }}](#{{ version | replace(from=".", to="") | replace(from="-", to="") | lower }})
102+ {%- if not loop.last %} | {% endif -%}
103+ {%- endfor -%}
104+ {%- endif -%}
105+
106+ {#- OUTPUT: Beta versions -#}
107+ {%- if beta_versions | length > 0 -%}
108+ {%- set beta_versions_sorted = beta_versions | sort | reverse -%}
109+ {%- set beta_base = beta_versions_sorted | first | split(pat="-") | first -%}
110+ {{ "\n " }}- `{{ beta_base }}` Betas - {% for version in beta_versions_sorted -%}
111+ [{{ version }}](#{{ version | replace(from=".", to="") | replace(from="-", to="") | lower }})
112+ {%- if not loop.last %} | {% endif -%}
113+ {%- endfor -%}
114+ {%- endif -%}
115+ {%- endfor -%}{{ "\n " }}
116+ ---
117+ """
118+
119+ body = """
120+ {%- macro remote_url() -%}
121+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
122+ {%- endmacro -%}
123+
124+ {%- set_global renderable_commits = [] -%}
125+ {%- for commit in commits -%}
126+ {# Filter commits that have a Conventional Commit type or a PR number (your rendering condition) #}
127+ {%- if commit.conventional or commit.remote.pr_number -%}
128+ {%- set_global renderable_commits = renderable_commits | concat(with=commit) -%}
129+ {%- endif -%}
130+ {%- endfor -%}
131+
132+ {%- if renderable_commits | length > 0 -%}
133+ {#- LOGIC: Version Header with Link -#}
134+ {%- if version -%}
135+ {{ "\n " }}
136+ ## [{{ version | trim_start_matches(pat="v") }}]({{ self::remote_url() }}/releases/tag/{{ version | trim_start_matches(pat="v") }})
137+ {{ "\n " }}Released on {{ timestamp | date(format="%Y-%m-%d") }}. All issues associated with this milestone can be found using this [filter]({{ self::remote_url() }}/milestones?state=closed&q={{ version }}).
138+ {%- else -%}
139+ ## [Unreleased]
140+ {%- endif -%}
141+
142+ {#- LOGIC: Loop through commit groups -#}
143+ {%- for group, commits_in_group in renderable_commits | group_by(attribute="group") -%}
144+ {%- if group == "Uncategorized Changes" and commits_in_group | length == 0 -%}
145+ {%- continue -%}
146+ {%- endif -%}
147+
148+ {# We also check that it is not the system group 'Other', which might be empty #}
149+ {%- if group == "Other" and commits_in_group | length == 0 -%}
150+ {%- continue -%}
151+ {%- endif -%}
152+
153+ {%- set action_verb = "Contributed by" -%}
154+ {%- if group == "Features" -%}
155+ {%- set action_verb = "Implemented by" -%}
156+ {%- elif group == "Bug Fixes" -%}
157+ {%- set action_verb = "Fixed by" -%}
158+ {%- elif group == "Performance" -%}
159+ {%- set action_verb = "Optimized by" -%}
160+ {%- elif group == "Documentation" -%}
161+ {%- set action_verb = "Documented by" -%}
162+ {%- endif -%}
163+
164+ {{ "\n " }}{{ "\n " }}### {{ group | upper_first }}
165+
166+ {#- THE LOOP NOW USES FILTERED COMMITS AND DOESN'T NEED AN INNER IF -#}
167+ {%- for commit in commits_in_group -%}
168+ {%- set message = commit.message | split(pat="\n ") | first | upper_first | trim -%}
169+
170+ {#- VISUAL: Commit message line -#}
171+ {{ "\n " }}- {{ message }}
172+
173+ {%- if commit.remote.username and commit.remote.pr_number -%}
174+ {#- VISUAL: Dynamic verb line -#}
175+ {{ "\n " }} - {{ action_verb }} [@{{ commit.remote.username }}](https://github.com/{{ commit.remote.username }}) in Pull Request [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}).
176+ {%- endif -%}
177+ {%- endfor -%}
178+ {%- endfor -%}
179+
180+ {#- LOGIC: New Contributors Section -#}
181+ {%- set new_contributors = github.contributors | filter(attribute="is_first_time", value=true) -%}
182+
183+ {%- if new_contributors | length > 0 -%}
184+ {%- set_global real_new_contributors = [] -%}
185+ {%- for contributor in new_contributors -%}
186+ {#- IMPORTANT: Filtering out your login "ns-vasilev" and Renovate -#}
187+ {%- set username_lower = contributor.username | default(value="") | lower | trim -%}
188+ {%- if username_lower != "ns-vasilev" and username_lower != "renovate" -%}
189+ {%- set_global real_new_contributors = real_new_contributors | concat(with=contributor) -%}
190+ {%- endif -%}
191+ {%- endfor -%}
192+
193+ {%- if real_new_contributors | length > 0 -%}
194+ {{ "\n " }}{{ "\n " }}### New Contributors
195+ {%- for contributor in real_new_contributors -%}
196+ {{ "\n " }}* @{{ contributor.username }} made their first contribution in{{ " " }}
197+ {%- if contributor.pr_number -%}
198+ [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }})
199+ {%- endif -%}
200+ {%- endfor -%}
201+ {%- endif -%}
202+ {%- endif -%}
203+ {%- endif -%}
204+ """
205+
206+ footer = """
207+ {%- macro remote_url() -%}
208+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
209+ {%- endmacro -%}
210+
211+ {{ "\n " }}
212+ {% for release in releases -%}
213+ {% if release.version -%}
214+ {% if release.previous.version -%}
215+ [{{ release.version | trim_start_matches(pat="v") }}]: \
216+ {{ self::remote_url() }}/compare/{{ release.previous.version }}..{{ release.version }}
217+ {% endif -%}
218+ {% else -%}
219+ [unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}..HEAD
220+ {% endif -%}
221+ {% endfor %}
222+ """
223+ trim = true
224+ postprocessors = []
225+
226+ [git ]
227+ conventional_commits = true
228+ # SET TO false to include old (unconventional) commits
229+ filter_unconventional = false
230+ split_commits = false
231+ commit_preprocessors = [
232+ { pattern = ' \((\w+\s)?#([0-9]+)\)' , replace = " " },
233+ ]
234+ commit_parsers = [
235+ { message = " ^chore\\ (changelog\\ )" , skip = true },
236+ { message = " ^chore.*changelog" , skip = true },
237+ { message = " ^docs: update CHANGELOG\\ .md \\ [skip ci\\ ]$" , skip = true },
238+ { message = " ^feat" , group = " Features" },
239+ { message = " ^fix" , group = " Bug Fixes" },
240+ { message = " ^doc" , group = " Documentation" },
241+ { message = " ^perf" , group = " Performance" },
242+ { message = " ^refactor" , group = " Refactor" },
243+ { message = " ^style" , group = " Styling" },
244+ { message = " ^test" , group = " Testing" },
245+ { message = " ^chore\\ (spm.*\\ )" , skip = false },
246+ { message = " ^chore\\ (deps.*\\ )" , skip = true },
247+ { message = " ^chore\\ (pr\\ )" , skip = true },
248+ { message = " ^chore\\ (pull\\ )" , skip = true },
249+ { message = " ^chore\\ (release\\ ): prepare for" , skip = true },
250+ { message = " ^chore|^ci" , group = " Miscellaneous Tasks" },
251+ { body = " .*security" , group = " Security" },
252+ # CATCH-ALL PARSER for old (unconventional) commits
253+ { message = " .*" , group = " Uncategorized Changes" },
254+ ]
255+
256+ # SET TO false to avoid filtering out Uncategorized Changes
257+ filter_commits = false
258+ protect_breaking_commits = false
259+ tag_pattern = " ^[0-9].*"
260+ skip_tags = " beta|alpha|cli-.*"
261+ ignore_tags = " rc|web-.*"
262+ topo_order = false
263+ sort_commits = " newest"
264+
265+ [bump ]
266+ breaking_always_bump_major = true
267+ features_always_bump_minor = true
268+ initial_tag = " 0.1.0"
0 commit comments