diff --git a/commitizen/changelog.py b/commitizen/changelog.py
index 5e43e2fef9..bf716cb1f7 100644
--- a/commitizen/changelog.py
+++ b/commitizen/changelog.py
@@ -29,7 +29,7 @@
 import re
 from collections import OrderedDict, defaultdict
 from datetime import date
-from typing import Callable, Dict, Iterable, List, Optional, Tuple
+from typing import Callable, Dict, Iterable, List, Optional, Pattern, Tuple
 
 from jinja2 import Environment, PackageLoader
 
@@ -74,72 +74,101 @@ def generate_tree_from_commits(
     unreleased_version: Optional[str] = None,
     change_type_map: Optional[Dict[str, str]] = None,
     changelog_message_builder_hook: Optional[Callable] = None,
+    tag_pattern: Pattern = re.compile(".*"),
 ) -> Iterable[Dict]:
     pat = re.compile(changelog_pattern)
     map_pat = re.compile(commit_parser, re.MULTILINE)
     body_map_pat = re.compile(commit_parser, re.MULTILINE | re.DOTALL)
 
     # Check if the latest commit is not tagged
-    latest_commit = commits[0]
-    current_tag: Optional[GitTag] = get_commit_tag(latest_commit, tags)
+    latest_commit: GitCommit = commits[0]
 
-    current_tag_name: str = unreleased_version or "Unreleased"
-    current_tag_date: str = ""
-    if unreleased_version is not None:
-        current_tag_date = date.today().isoformat()
-    if current_tag is not None and current_tag.name:
-        current_tag_name = current_tag.name
-        current_tag_date = current_tag.date
+    # create the first_tag
+    # Note: Changelog has no date for "Unreleased".
+    if unreleased_version:
+        first_tag = GitTag(
+            unreleased_version, latest_commit.rev, date.today().isoformat()
+        )
+    else:
+        unreleased_tag = GitTag("Unreleased", latest_commit.rev, "")
+        first_tag = get_commit_tag(latest_commit, tags) or unreleased_tag
 
     changes: Dict = defaultdict(list)
-    used_tags: List = [current_tag]
+    used_tags: List = [first_tag]
     for commit in commits:
-        commit_tag = get_commit_tag(commit, tags)
 
-        if commit_tag is not None and commit_tag not in used_tags:
-            used_tags.append(commit_tag)
+        # determine if we found a new matching tag
+        commit_tag = get_commit_tag(commit, tags)
+        is_tag_match = False
+        if commit_tag:
+            matches = tag_pattern.fullmatch(commit_tag.name)
+            if matches and (commit_tag not in used_tags):
+                is_tag_match = True
+
+        # new node if we have a tag match
+        if is_tag_match:
             yield {
-                "version": current_tag_name,
-                "date": current_tag_date,
+                "version": used_tags[-1].name,
+                "date": used_tags[-1].date,
                 "changes": changes,
             }
-            # TODO: Check if tag matches the version pattern, otherwise skip it.
-            # This in order to prevent tags that are not versions.
-            current_tag_name = commit_tag.name
-            current_tag_date = commit_tag.date
+            used_tags.append(commit_tag)
             changes = defaultdict(list)
 
         matches = pat.match(commit.message)
         if not matches:
             continue
 
-        # Process subject from commit message
-        message = map_pat.match(commit.message)
-        if message:
-            parsed_message: Dict = message.groupdict()
-            # change_type becomes optional by providing None
-            change_type = parsed_message.pop("change_type", None)
-
-            if change_type_map:
-                change_type = change_type_map.get(change_type, change_type)
-            if changelog_message_builder_hook:
-                parsed_message = changelog_message_builder_hook(parsed_message, commit)
-            changes[change_type].append(parsed_message)
-
-        # Process body from commit message
-        body_parts = commit.body.split("\n\n")
-        for body_part in body_parts:
-            message_body = body_map_pat.match(body_part)
-            if not message_body:
-                continue
-            parsed_message_body: Dict = message_body.groupdict()
+        update_changes_for_commit(
+            changes,
+            commit,
+            change_type_map,
+            changelog_message_builder_hook,
+            map_pat,
+            body_map_pat,
+        )
+
+    yield {
+        "version": used_tags[-1].name,
+        "date": used_tags[-1].date,
+        "changes": changes,
+    }
+
 
-            change_type = parsed_message_body.pop("change_type", None)
-            if change_type_map:
-                change_type = change_type_map.get(change_type, change_type)
-            changes[change_type].append(parsed_message_body)
+def update_changes_for_commit(
+    changes: Dict,
+    commit: GitCommit,
+    change_type_map: Optional[Dict[str, str]],
+    changelog_message_builder_hook: Optional[Callable],
+    map_pat: Pattern,
+    body_map_pat: Pattern,
+):
+    """Processes the commit message and will update changes if applicable."""
+    # Process subject from commit message
+    message = map_pat.match(commit.message)
+    if message:
+        parsed_message: Dict = message.groupdict()
+        # change_type becomes optional by providing None
+        change_type = parsed_message.pop("change_type", None)
+
+        if change_type_map:
+            change_type = change_type_map.get(change_type, change_type)
+        if changelog_message_builder_hook:
+            parsed_message = changelog_message_builder_hook(parsed_message, commit)
+        changes[change_type].append(parsed_message)
+
+    # Process body from commit message
+    body_parts = commit.body.split("\n\n")
+    for body_part in body_parts:
+        message_body = body_map_pat.match(body_part)
+        if not message_body:
+            continue
+        parsed_message_body: Dict = message_body.groupdict()
 
-    yield {"version": current_tag_name, "date": current_tag_date, "changes": changes}
+        change_type = parsed_message_body.pop("change_type", None)
+        if change_type_map:
+            change_type = change_type_map.get(change_type, change_type)
+        changes[change_type].append(parsed_message_body)
 
 
 def order_changelog_tree(tree: Iterable, change_type_order: List[str]) -> Iterable:
diff --git a/commitizen/cli.py b/commitizen/cli.py
index 90abc55453..34039e42ea 100644
--- a/commitizen/cli.py
+++ b/commitizen/cli.py
@@ -220,6 +220,13 @@
                             "If not set, it will generate changelog from the start"
                         ),
                     },
+                    {
+                        "name": "--tag-parser",
+                        "help": (
+                            "regex match for tags represented "
+                            "within the changelog. default: '.*'"
+                        ),
+                    },
                 ],
             },
             {
diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py
index 58c0ceaeef..16a22cd3fe 100644
--- a/commitizen/commands/changelog.py
+++ b/commitizen/commands/changelog.py
@@ -1,4 +1,5 @@
 import os.path
+import re
 from difflib import SequenceMatcher
 from operator import itemgetter
 from typing import Callable, Dict, List, Optional
@@ -51,6 +52,10 @@ def __init__(self, config: BaseConfig, args):
         self.tag_format = args.get("tag_format") or self.config.settings.get(
             "tag_format"
         )
+        tag_parser = args.get("tag_parser")
+        if tag_parser is None:
+            tag_parser = self.config.settings.get("tag_parser", r".*")
+        self.tag_pattern = re.compile(tag_parser)
 
     def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
         """Try to find the 'start_rev'.
@@ -154,6 +159,7 @@ def __call__(self):
             unreleased_version,
             change_type_map=change_type_map,
             changelog_message_builder_hook=changelog_message_builder_hook,
+            tag_pattern=self.tag_pattern,
         )
         if self.change_type_order:
             tree = changelog.order_changelog_tree(tree, self.change_type_order)
diff --git a/docs/changelog.md b/docs/changelog.md
index 6f92bb21cd..01640545c9 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -161,6 +161,24 @@ cz changelog --start-rev="v0.2.0"
 changelog_start_rev = "v0.2.0"
 ```
 
+### `tag-parser`
+
+This value can be set in the `toml` file with the key `tag_parser` under `tools.commitizen`
+
+The default the changelog will capture all git tags (e.g. regex `.*`).
+The user may specify a regex pattern of their own display only
+specific tags within the changelog.
+
+```bash
+cz changelog --tag-parser=".*"
+```
+
+```toml
+[tools.commitizen]
+# ...
+tag_parser = "v[0-9]*\\.[0-9]*\\.[0-9]*"
+```
+
 ## Hooks
 
 Supported hook methods:
diff --git a/docs/config.md b/docs/config.md
index 6a7838e245..e1a3bbd466 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -128,6 +128,7 @@ commitizen:
 | `version`                  | `str`  | `None`                      | Current version. Example: "0.1.2"                                                                                                                                                               |
 | `version_files`            | `list` | `[ ]`                       | Files were the version will be updated. A pattern to match a line, can also be specified, separated by `:` [See more][version_files]                                                            |
 | `tag_format`               | `str`  | `None`                      | Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [See more][tag_format]                                                                                  |
+| `tag_parser `              | `str`  | `.*`                        | Generate changelog using only tags matching the regex pattern (e.g. `"v([0-9.])*"`). [See more][tag_parser]                                                                                  |
 | `update_changelog_on_bump` | `bool` | `false`                     | Create changelog when running `cz bump`                                                                                                                                                         |
 | `annotated_tag`            | `bool` | `false`                     | Use annotated tags instead of lightweight tags. [See difference][annotated-tags-vs-lightweight]                                                                                                 |
 | `bump_message`             | `str`  | `None`                      | Create custom commit message, useful to skip ci. [See more][bump_message]                                                                                                                       |
@@ -142,6 +143,7 @@ commitizen:
 [version_files]: bump.md#version_files
 [tag_format]: bump.md#tag_format
 [bump_message]: bump.md#bump_message
+[tag_parser]: changelog.md#tag_parser
 [allow_abort]: check.md#allow-abort
 [additional-features]: https://github.com/tmbo/questionary#additional-features
 [customization]: customization.md
diff --git a/tests/CHANGELOG_FOR_TEST.md b/tests/CHANGELOG_FOR_TEST.md
index e92ca1ce39..3bf4daf246 100644
--- a/tests/CHANGELOG_FOR_TEST.md
+++ b/tests/CHANGELOG_FOR_TEST.md
@@ -54,6 +54,8 @@
 
 ## v1.0.0b1 (2019-01-17)
 
+## user_def (2019-01-10)
+
 ### feat
 
 - py3 only, tests and conventional commits 1.0
diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py
index 3f90800e7e..e862f0cb16 100644
--- a/tests/commands/test_changelog_command.py
+++ b/tests/commands/test_changelog_command.py
@@ -872,3 +872,50 @@ def test_changelog_from_rev_latest_version_dry_run(
     out, _ = capsys.readouterr()
 
     file_regression.check(out, extension=".md")
+
+
+@pytest.mark.usefixtures("tmp_commitizen_project")
+@pytest.mark.freeze_time("2022-02-13")
+@pytest.mark.parametrize(
+    "cli_args, line, filtered",
+    [
+        ([], r'tag_parser = "v[0-9]*\\.[0-9]*\\.[0-9]*"', True),  # version filter
+        (["--tag-parser", r"v[0-9]*\.[0-9]*\.[0-9]*"], "", True),  # cli arg filter
+        ([], "", False),  # default tag_parser
+    ],
+)
+def test_changelog_tag_parser_config(
+    mocker, config_path, changelog_path, cli_args, line, filtered
+):
+    mocker.patch("commitizen.git.GitTag.date", "2022-02-13")
+
+    with open(config_path, "a") as f:
+        # f.write('tag_format = "$version"\n')
+        f.write(line)
+
+    # create a valid start tag
+    create_file_and_commit("feat: initial")
+    git.tag("v1.0.0")
+
+    # create a tag for this test
+    create_file_and_commit("feat: add new")
+    git.tag("v1.1.0-beta")
+
+    # create a valid end tag
+    create_file_and_commit("feat: add more")
+    git.tag("v1.1.0")
+
+    # call CLI
+    command = ["cz", "changelog"]
+    command.extend(cli_args)
+    mocker.patch.object(sys, "argv", command)
+    cli.main()
+
+    # open CLI output
+    with open(changelog_path, "r") as f:
+        out = f.read()
+
+    # test if cli is handling tag_format
+    assert "v1.0.0" in out
+    assert ("v1.1.0-beta" in out) is not filtered
+    assert "v1.1.0" in out
diff --git a/tests/test_changelog.py b/tests/test_changelog.py
index e68a3abdcf..156f2ff0fe 100644
--- a/tests/test_changelog.py
+++ b/tests/test_changelog.py
@@ -1,3 +1,6 @@
+import re
+from typing import Dict, Iterable, List, Pattern
+
 import pytest
 
 from commitizen import changelog, defaults, git
@@ -458,6 +461,7 @@
     ("v1.0.0", "aa44a92d68014d0da98965c0c2cb8c07957d4362", "2019-03-01"),
     ("1.0.0b2", "aab33d13110f26604fb786878856ec0b9e5fc32b", "2019-01-18"),
     ("v1.0.0b1", "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", "2019-01-17"),
+    ("user_def", "ed830019581c83ba633bfd734720e6758eca6061", "2019-01-10"),
     ("v0.9.11", "c52eca6f74f844ab3ffbde61d98ef96071e132b7", "2018-12-17"),
     ("v0.9.10", "b3f89892222340150e32631ae6b7aab65230036f", "2018-09-22"),
     ("v0.9.9", "684e0259cc95c7c5e94854608cd3dcebbd53219e", "2018-09-22"),
@@ -642,9 +646,10 @@ def test_get_commit_tag_is_None(gitcommits, tags):
         },
     },
     {"version": "1.0.0b2", "date": "2019-01-18", "changes": {}},
+    {"version": "v1.0.0b1", "date": "2019-01-17", "changes": {}},
     {
-        "version": "v1.0.0b1",
-        "date": "2019-01-17",
+        "version": "user_def",
+        "date": "2019-01-10",
         "changes": {
             "feat": [
                 {
@@ -790,14 +795,57 @@ def test_get_commit_tag_is_None(gitcommits, tags):
 )
 
 
-def test_generate_tree_from_commits(gitcommits, tags):
+def _filter_tree(tag_pattern: Pattern, tree: Iterable[Dict]) -> List[Dict[str, str]]:
+    """filters the tree. commits with invalid tags are kept within the current node"""
+
+    current = None
+    out = []
+    for node in tree:
+        if not current or tag_pattern.fullmatch(node["version"]) or not out:
+            current = node.copy()
+            out.append(current)
+        else:
+            changes = current["changes"]
+            for key, value in node["changes"].items():
+                if key in changes:
+                    changes[key].extend(value)
+                else:
+                    changes[key] = value
+
+    return out
+
+
+@pytest.mark.parametrize(
+    "tag_parser",
+    [
+        (None),  # backwards compatibility check
+        (".*"),  # default tag_parser
+        (r"v[0-9]*\.[0-9]*\.[0-9]*"),  # version filter
+    ],
+)
+def test_generate_tree_from_commits(gitcommits, tags, tag_parser):
     parser = defaults.commit_parser
     changelog_pattern = defaults.bump_pattern
-    tree = changelog.generate_tree_from_commits(
-        gitcommits, tags, parser, changelog_pattern
-    )
 
-    assert tuple(tree) == COMMITS_TREE
+    # generate the tree and expected_tree
+    if tag_parser is None:
+        tree = changelog.generate_tree_from_commits(
+            gitcommits, tags, parser, changelog_pattern
+        )
+        # commits tree is unfiltered
+        expected_tree = COMMITS_TREE
+    else:
+        tag_pattern = re.compile(tag_parser)
+        tree = changelog.generate_tree_from_commits(
+            gitcommits, tags, parser, changelog_pattern, tag_pattern=tag_pattern
+        )
+        # filter the COMMITS_TREE to what we expect it to be
+        expected_tree = _filter_tree(tag_pattern, COMMITS_TREE)
+
+    # compare the contents of each tree
+    tree = list(tree)
+    for outcome, expected in zip(tree, expected_tree):
+        assert outcome == expected
 
 
 @pytest.mark.parametrize(