Repository: iterative/gto (language: python)

Issue:
Unclear demotion behavior
Initial idea to introduce `gto demote $MODEL $VERSION $ENV` was based on the assumption that user may want to undo the action of promotion. E.g. if he mistakenly promoted a experimental version to production.

One corner case I found out for this `demote` cmd is this.
1. Let's suppose that you have a `rf` with `v1` and `v2`. Then you promote to production `v2`, then `v1`, then again `v2`.
2. Now you `demote` what's in `production` right now. As a result, `v1` now in `production`.
3. Now you `demote` again. As a result, `v2` now in `production`. But you already tried to `undo` promotion of `v2` to `production`, so it's confusing.

I see two options here:
1. Keep things as they are ^. Maybe there are some user scenarios in which this makes sense.
2. Make another assumption. If you `demote` `v2` from `production`, that means that it shouldn't appear there until you `promote` it again. Then the example above will end up without any version of `rf` promoted to `production`.
3. Disallow `demote` instruction and require to `promote` things instead.
4. Use option 3 + still allow to have `demote` shortcut cmd which will actually execute `promote $PREV_VERSION_IN_THAT_ENV`, but that doesn't look very intuitive in complex scenarios also. As you can't distinguish between `promote` made on purpose and `promote` made because user called `demote`, if you run `demote` 2+ times, you'll stuck between two last versions in that env forever.

I'm leaning towards option 3 right now because it's simpler. Later we can introduce `demote` command as in option 1 or 2, however we decide.

WDYT?
CC @dmpetrov @mike0sv @dberenbaum @omesser @shcheklein 


Discussion / hints:
5. Another variation of option 2 could be removing `demote` command and asking to `deprecate` things:
    - e.g. `gto deprecate rf v2` will deprecate `v2` completely, so it gets automatically `demoted` from all envs.
    - `gto deprecate rf v2 production` will deprecate `v2` from using in `production` env. So it gets automatically `demoted` from `production` env. That sounds clearer to me than using `gto demote rf production` - because in the latter you doesn't specify which version you're demoting (but we still can print demoted version in a cmd output FYI).

How git tags will look like in this option:
`rf@v1.0.1` - version registration
`rf@!v1.0.1` - version deprecation (should you be able to undo this action any other way except for git tag removal?)
`rf@v1.0.1!production` - version deprecation for particular env (should you be able to undo this action any other way except for git tag removal?)

> 3\. Disallow `demote` instruction and require to `promote` things instead.

I think this seems like the most straightforward approach. I don't think the convenience is worth it here where users probably want to be very explicit about what is currently active in each environment.

By the way, can a version that's active in any environment be unregistered? How does/should that work?
> By the way, can a version that's active in any environment be unregistered? How does/should that work?

Good question! Right now it works like this. If you `unregister` a version which is present in any environment, it gets `demoted` from them automatically. That means (among other) when you'll run `gto show`, you'll won't see this version promoted, or `gto audit` will show that it got demoted at the moment you unregistered the version.

As `unregister` is undoable, to promote model from the same commit once more, you'll have to register new version in the same commit (assuming that registration is a requirement for promotion, which is the default behavior).

Fix patch:
diff --git a/README.md b/README.md
index f8d37302..74ad93d3 100644
--- a/README.md
+++ b/README.md
@@ -43,15 +43,15 @@ $ git clone https://github.com/iterative/example-gto.git
 $ cd example-gto
 ```
 
-### Versioning
+### Versioning an artifact
 
-To register a new artifact or a new version, use `gto register`. This is usually
-done to mark significant changes to the artifact (such as a release or a
-deprecation).
+Registering a version is usually done to mark significant changes to the
+artifact. To release a new version (including the very first one), use
+`gto register`.
 
 ```console
-$ gto register awesome-model
-Created git tag 'awesome-model@v0.0.1' that registers a new version
+$ gto register awesome-model HEAD --version v0.0.1
+Created git tag 'awesome-model@v0.0.1' that registers a version
 ```
 
 <details summary="What happens under the hood?">
@@ -59,20 +59,22 @@ Created git tag 'awesome-model@v0.0.1' that registers a new version
 GTO creates a special Git tag for the artifact version, in a standard format:
 `{artifact_name}@{version_number}`.
 
-The version is now associated to the current Git commit (`HEAD`). You can have
-several versions in a given commit, ordered by their automatic version numbers.
+The version is now associated to the current Git commit (`HEAD`). You can use
+another Git commit if you provide it's hexsha as an additional argument, like
+`$ gto register awesome-model abc1234`.
 
 </details>
 
-### Assing a stage
+### Assigning a stage to version
 
-Assing an actionable stage for a specific artifact version with `gto assign`.
-Stages can mark it's readiness for a specific consumer. You can plug in a real
-downsteam system via CI/CD or web hooks. For example: redeploy an ML model.
+To assign an actionable stage for a specific artifact version use the same
+`gto assign` command. Stages can mark the artifact readiness for a specific
+consumer. You can plug in a real downsteam system via CI/CD or web hooks, e.g.
+to redeploy an ML model.
 
 ```console
-$ gto assign awesome-model prod
-Created git tag 'awesome-model#prod#1' that adds stage 'prod' to 'v0.0.1'
+$ gto assign awesome-model --version v0.0.1 --stage prod
+Created git tag 'awesome-model#prod#1' that assigns a stage to 'v0.0.1'
 ```
 
 <details summary="What happens under the hood?">
@@ -91,46 +93,12 @@ require deleting the existing tag.
 
 </details>
 
-### Unassign a stage
-
-Note: this functionality is in development and will be introduced soon.
-
-Sometimes you need to mark an artifact version no longer ready for a specific
-consumer, and maybe signal a downstream system about this. You can use
-`gto unassign` for that:
-
-```console
-$ gto unassign awesome-model prod
-Created git tag 'awesome-model#prod#2!' that unassigns stage 'prod' from 'v0.0.1'
-```
-
-<details summary="Some details and options">
-
-GTO creates a special Git tag in a standard format:
-`{artifact_name}#{stage}#{e}!`.
-
-Note, that later you can create this stage again, if you need to, by calling
-`$ gto assign`.
-
-You also may want to delete the git tag instead of creating a new one. This is
-useful if you don't want to keep extra tags in you Git repo, don't need history
-and don't want to trigger a CI/CD or another downstream system. For that, you
-can use:
-
-```console
-$ gto unassign --delete
-Deleted git tag 'awesome-model#prod#1' that assigned 'prod' to 'v0.0.1'
-To push the changes upsteam, run:
-git push origin awesome-model#prod#1 --delete
-```
-
-</details>
-
 ### Annotating
 
-So far we've seen how to register and assign a stage to an artifact versions,
-but we still don't have much information about them. What about the type of
-artifact (dataset, model, etc.) or the file path to find it in the working tree?
+So far we've seen how to register a new version and assign a stage to an
+artifact versions, but we still don't have much information about them. What
+about the type of artifact (dataset, model, etc.) or the file path to find it in
+the working tree?
 
 For simple projects (e.g. single artifact) we can assume the details in a
 downstream system. But for more advanced cases, we should codify them in the
@@ -158,7 +126,7 @@ GTO the artifact file is committed to Git.
 
 <details summary="Virtual vs. Physical artifacts">
 
-- Physical files/directories are committed to the repo. When you register a new
+- Physical files/directories are committed to the repo. When you create a new
   version or assign a stage to it, Git guarantees that it's immutable -- you can
   return a year later and get the same artifact by providing a version.
 
@@ -176,6 +144,99 @@ GTO the artifact file is committed to Git.
 
 </details>
 
+### Unassigning a stage
+
+Sometimes you need to mark an artifact version no longer ready for a specific
+consumer, and maybe signal a downstream system about this. You can use
+`gto deprecate` for that:
+
+```console
+$ gto deprecate awesome-model v0.0.1 prod
+Created git tag 'awesome-model#prod#2!' that unassigns a stage from 'v0.0.1'
+```
+
+<details summary="Some details and options">
+
+GTO creates a special Git tag in a standard format:
+`{artifact_name}#{stage}#{e}!`.
+
+Note, that later you can create this stage again, if you need to, by calling
+`$ gto assign` again.
+
+You also may want to delete the git tag instead of creating a new one. This is
+useful if you don't want to keep extra tags in you Git repo, don't need history
+and don't want to trigger a CI/CD or another downstream system. For that, you
+can use:
+
+```console
+$ gto deprecate awesome-model v0.0.1 prod --delete
+Deleted git tag 'awesome-model#prod#1' that assigned a stage to 'v0.0.1'
+To push the changes upstream, run:
+git push origin awesome-model#prod#1 --delete
+```
+
+</details>
+
+### Deregister a version
+
+Sometimes you need mark a specific artifact version as a no longer ready for
+usage. You could just delete a git tag, but if you want to preserve a history of
+the actions, you may again use `gto deprecate`.
+
+```console
+$ gto deprecate awesome-model v0.0.1
+Created git tag 'awesome-model@v0.0.1!' that deregistered a version.
+```
+
+<details summary="Some details and options">
+
+If you want to deregister the version by deleting the Git tags itself, you could
+use
+
+```console
+$ gto deprecate awesome-model v0.0.1 --delete
+Deleted git tag 'awesome-model@v0.0.1' that registered a version.
+Deleted git tag 'awesome-model#prod#1' that assigned a stage to 'v0.0.1'.
+Deleted git tag 'awesome-model#prod#2!' that unassigned a stage to 'v0.0.1'.
+To push the changes upstream, run:
+git push origin awesome-model@v0.0.1 awesome-model#prod#1 awesome-model#prod#2! --delete
+```
+
+This includes all Git tags related to the version: a tag that registered it and
+all tags that assigned stages to it.
+
+</details>
+
+### Deprecating an artifact
+
+Sometimes you need to need to mark the artifact as "deprecated", usually meaning
+it's outdated and will no longer be developed. To do this, you could run:
+
+```console
+$ gto deprecate awesome-model
+Created Git tag 'awesome-model@deprecated' that deprecates an artifact.
+```
+
+<details summary="Some details and options">
+
+With `awesome-model@deprecated` Git tag the artifact will be considered
+deprecated until you register a new version or assign a new stage to it after
+the deprecation.
+
+If you want to deprecate an artifact by deleting git tags, you'll need to delete
+all of them for the artifact. You could do that with
+
+```console
+$ gto deprecate awesome-model --delete
+Deleted git tag 'awesome-model@v0.0.1' that registered a version.
+Deleted git tag 'awesome-model#prod#1' that assigned a stage to 'v0.0.1'.
+Deleted git tag 'awesome-model#prod#2!' that unassigned a stage to 'v0.0.1'.
+To push the changes upstream, run:
+git push origin awesome-model@v0.0.1 awesome-model#prod#1 awesome-model#prod#2! --delete
+```
+
+</details>
+
 ## Using the registry locally
 
 Let's look at the usage of the `gto show` and `gto history`.
@@ -190,7 +251,7 @@ $ gto show
 ╒═══════════════╤══════════╤════════╤═════════╤════════════╕
 │ name          │ latest   │ #dev   │ #prod   │ #staging   │
 ╞═══════════════╪══════════╪════════╪═════════╪════════════╡
-│ churn         │ v3.1.0   │ v3.0.0 │ v3.0.0  │ v3.1.0     │
+│ churn         │ v3.1.0   │ v3.1.0 │ v3.0.0  │ v3.1.0     │
 │ segment       │ v0.4.1   │ v0.4.1 │ -       │ -          │
 │ cv-class      │ v0.1.13  │ -      │ -       │ -          │
 │ awesome-model │ v0.0.1   │ -      │ v0.0.1  │ -          │
@@ -205,36 +266,72 @@ Add an artifact name to print all of its versions instead:
 
 ```console
 $ gto show churn
-╒════════════╤═══════════╤═══════════╤═════════════════════╤═══════════════════╤══════════════╕
-│ artifact   │ version   │ stage     │ created_at          │ author            │ ref          │
-╞════════════╪═══════════╪═══════════╪═════════════════════╪═══════════════════╪══════════════╡
-│ churn      │ v3.1.0    │ staging   │ 2022-07-13 15:25:41 │ Alexander Guschin │ churn@v3.1.0 │
-│ churn      │ v3.0.0    │ prod, dev │ 2022-07-09 00:19:01 │ Alexander Guschin │ churn@v3.0.0 │
-╘════════════╧═══════════╧═══════════╧═════════════════════╧═══════════════════╧══════════════╛
+╒════════════╤═══════════╤══════════════╤═════════════════════╤══════════════╕
+│ artifact   │ version   │ stage        │ created_at          │ ref          │
+╞════════════╪═══════════╪══════════════╪═════════════════════╪══════════════╡
+│ churn      │ v3.1.0    │ staging, dev │ 2022-07-14 10:33:53 │ churn@v3.1.0 │
+│ churn      │ v3.0.0    │ prod         │ 2022-07-09 19:27:13 │ churn@v3.0.0 │
+╘════════════╧═══════════╧══════════════╧═════════════════════╧══════════════╛
 ```
 
+#### Enabling multiple versions in the same Stage workflow
+
+<details summary="Details">
+
+Note: this functionality is experimental and subject to change. If you find it
+useful, please share your feedback in GH issues to help us make it stable.
+
+If you would like to see more than a single version assigned in a stage, use
+`--av` (short for `--assignments-per-version`), e.g. `-1` to show all versions.
+
+```console
+$ gto show churn --av -1
+╒════════════╤═══════════╤══════════════╤═════════════════════╤══════════════╕
+│ artifact   │ version   │ stage        │ created_at          │ ref          │
+╞════════════╪═══════════╪══════════════╪═════════════════════╪══════════════╡
+│ churn      │ v3.1.0    │ staging, dev │ 2022-07-14 10:33:53 │ churn@v3.1.0 │
+│ churn      │ v3.0.0    │ dev, prod    │ 2022-07-09 19:27:13 │ churn@v3.0.0 │
+╘════════════╧═══════════╧══════════════╧═════════════════════╧══════════════╛
+```
+
+To enable this workflow, you need to supply the `--av` argument to `gto show`
+and `gto which` commands. Other commands behave the same way regardless of the
+approach you choose.
+
+</details>
+
 #### Enabling Kanban workflow
 
-In some cases, you would like to have a latest stage for an artifact version to
-replace all the previous stages. In this case the version will have a single
-stage. This resembles Kanban workflow, when you "move" your artifact version
-from one column ("stage-1") to another ("stage-2"). This is how MLFlow and some
-other Model Registries work.
+<details summary="Details">
+
+Note: this functionality is experimental and subject to change. If you find it
+useful, please share your feedback in GH issues to help us make it stable.
 
-To achieve this, you can use `--last-stage` flag (or `--ls` for short):
+If you would like the latest stage to replace all the previous stages for an
+artifact version, use `--vs` flag (or `--versions-per-stage` for short) combined
+with `--av`:
 
 ```console
-$ gto show --ls
-╒═══════════════╤══════════╤════════╤═════════╤════════════╕
-│ name          │ latest   │ #dev   │ #prod   │ #staging   │
-╞═══════════════╪══════════╪════════╪═════════╪════════════╡
-│ churn         │ v3.1.0   │ -      │ v3.0.0  │ v3.1.0     │
-│ segment       │ v0.4.1   │ v0.4.1 │ -       │ -          │
-│ cv-class      │ v0.1.13  │ -      │ -       │ -          │
-│ awesome-model │ v0.0.1   │ -      │ v0.0.1  │ -          │
-╘═══════════════╧══════════╧════════╧═════════╧════════════╛
+$ gto show churn --av 1 --vs -1
+╒════════════╤═══════════╤═════════╤═════════════════════╤══════════════╕
+│ artifact   │ version   │ stage   │ created_at          │ ref          │
+╞════════════╪═══════════╪═════════╪═════════════════════╪══════════════╡
+│ churn      │ v3.1.0    │ staging │ 2022-07-14 10:33:53 │ churn@v3.1.0 │
+│ churn      │ v3.0.0    │ dev     │ 2022-07-09 19:27:13 │ churn@v3.0.0 │
+╘════════════╧═══════════╧═════════╧═════════════════════╧══════════════╛
 ```
 
+In this case the version will always have a single stage (or have no stage at
+all). This resembles Kanban workflow, when you "move" your artifact version from
+one column ("stage-1") to another ("stage-2"). This is how MLFlow and some other
+Model Registries work.
+
+To enable this workflow, you need to supply the `--vs` and `--av` arguments to
+`gto show` and `gto which` commands. Other commands behave the same way
+regardless of the approach you choose.
+
+</details>
+
 ### See the history of an artifact
 
 `gto history` will print a journal of the events that happened to an artifact.
@@ -242,17 +339,19 @@ This allows you to audit the changes.
 
 ```console
 $ gto history churn
-╒═════════════════════╤════════════╤══════════════╤═══════════╤═════════╤══════════╤═══════════════════╕
-│ timestamp           │ artifact   │ event        │ version   │ stage   │ commit   │ author            │
-╞═════════════════════╪════════════╪══════════════╪═══════════╪═════════╪══════════╪═══════════════════╡
-│ 2022-07-17 02:45:41 │ churn      │ assignment    │ v3.0.0    │ prod    │ 631520b  │ Alexander Guschin │
-│ 2022-07-15 22:59:01 │ churn      │ assignment    │ v3.1.0    │ staging │ be340cc  │ Alexander Guschin │
-│ 2022-07-14 19:12:21 │ churn      │ assignment    │ v3.0.0    │ dev     │ 631520b  │ Alexander Guschin │
-│ 2022-07-13 15:25:41 │ churn      │ registration │ v3.1.0    │ -       │ be340cc  │ Alexander Guschin │
-│ 2022-07-12 11:39:01 │ churn      │ commit       │ -         │ -       │ be340cc  │ Alexander Guschin │
-│ 2022-07-09 00:19:01 │ churn      │ registration │ v3.0.0    │ -       │ 631520b  │ Alexander Guschin │
-│ 2022-07-07 20:32:21 │ churn      │ commit       │ -         │ -       │ 631520b  │ Alexander Guschin │
-╘═════════════════════╧════════════╧══════════════╧═══════════╧═════════╧══════════╧═══════════════════╛
+╒═════════════════════╤════════════╤══════════════╤═══════════╤═════════╤══════════╤═════════════════╕
+│ timestamp           │ artifact   │ event        │ version   │ stage   │ commit   │ ref             │
+╞═════════════════════╪════════════╪══════════════╪═══════════╪═════════╪══════════╪═════════════════╡
+│ 2022-07-29 14:50:10 │ churn      │ assignment   │ v3.1.0    │ dev     │ 8e4b8e9  │ churn#dev#4     │
+│ 2022-07-17 21:53:53 │ churn      │ assignment   │ v3.0.0    │ prod    │ 0d4e471  │ churn#prod#3    │
+│ 2022-07-16 18:07:13 │ churn      │ assignment   │ v3.1.0    │ staging │ 8e4b8e9  │ churn#staging#2 │
+│ 2022-07-15 14:20:33 │ churn      │ assignment   │ v3.0.0    │ dev     │ 0d4e471  │ churn#dev#1     │
+│ 2022-07-14 10:33:53 │ churn      │ registration │ v3.1.0    │ -       │ 8e4b8e9  │ churn@v3.1.0    │
+│ 2022-07-13 06:47:13 │ churn      │ commit       │ v3.1.0    │ -       │ 8e4b8e9  │ 8e4b8e9         │
+│ 2022-07-13 06:47:13 │ churn      │ commit       │ v3.1.0    │ -       │ 8e4b8e9  │ 8e4b8e9         │
+│ 2022-07-09 19:27:13 │ churn      │ registration │ v3.0.0    │ -       │ 0d4e471  │ churn@v3.0.0    │
+│ 2022-07-08 15:40:33 │ churn      │ commit       │ v3.0.0    │ -       │ 0d4e471  │ 0d4e471         │
+╘═════════════════════╧════════════╧══════════════╧═══════════╧═════════╧══════════╧═════════════════╛
 ```
 
 ## Consuming the registry downstream
@@ -316,26 +415,12 @@ To get the version that is currently assigned to an environment (stage), use
 
 ```console
 $ gto which churn dev
-v3.0.0
+v3.1.0
 
 $ gto which churn dev --ref
-churn#dev#1
+churn#dev#4
 ```
 
-<details summary="Kanban workflow">
-
-If you prefer Kanban workflow described above, you could use `--last` flag. This
-will take into account the last stage for a version only.
-
-```console
-$ gto which churn dev --last
-```
-
-Since the last stage for version `v3.0.0` was `prod`, this doesn't return
-anything.
-
-</details>
-
 To get details about an artifact (from `artifacts.yaml`) use `gto describe`:
 
 ```console
diff --git a/gto/api.py b/gto/api.py
index ceeb481a..60d5b466 100644
--- a/gto/api.py
+++ b/gto/api.py
@@ -1,12 +1,19 @@
 from collections import OrderedDict
-from datetime import datetime
 from typing import List, Optional, Union
 
 from funcy import distinct
 from git import Repo
 
-from gto.constants import NAME, STAGE, VERSION, Event
-from gto.exceptions import NoRepo, WrongArgs
+from gto.constants import (
+    ARTIFACT,
+    ASSIGNMENTS_PER_VERSION,
+    COMMIT,
+    NAME,
+    STAGE,
+    VERSION,
+    VERSIONS_PER_STAGE,
+)
+from gto.exceptions import NoRepo, NotImplementedInGTO, WrongArgs
 from gto.ext import EnrichmentInfo
 from gto.index import (
     EnrichmentManager,
@@ -80,6 +87,8 @@ def register(
     ref: str,
     version: str = None,
     message: str = None,
+    simple: bool = None,
+    force: bool = False,
     bump_major: bool = False,
     bump_minor: bool = False,
     bump_patch: bool = False,
@@ -93,6 +102,8 @@ def register(
         ref=ref,
         version=version,
         message=message,
+        simple=simple if simple is not None else True,
+        force=force,
         bump_major=bump_major,
         bump_minor=bump_minor,
         bump_patch=bump_patch,
@@ -106,10 +117,10 @@ def assign(
     repo: Union[str, Repo],
     name: str,
     stage: str,
-    version: str = None,
-    ref: str = None,
-    name_version: str = None,
-    message: str = None,
+    version: Optional[str] = None,
+    ref: Optional[str] = None,
+    name_version: Optional[str] = None,
+    message: Optional[str] = None,
     simple: bool = False,
     force: bool = False,
     skip_registration: bool = False,
@@ -119,11 +130,11 @@ def assign(
 ):
     """Assign stage to specific artifact version"""
     return GitRegistry.from_repo(repo).assign(
-        name,
-        stage,
-        version,
-        ref,
-        name_version,
+        name=name,
+        stage=stage,
+        version=version,
+        ref=ref,
+        name_version=name_version,
         message=message,
         simple=simple,
         force=force,
@@ -134,6 +145,85 @@ def assign(
     )
 
 
+def unassign(
+    repo: Union[str, Repo],
+    name: str,
+    ref: str = None,
+    version: str = None,
+    stage: str = None,
+    message: str = None,
+    stdout: bool = False,
+    simple: Optional[bool] = None,
+    force: bool = False,
+    delete: bool = False,
+    author: Optional[str] = None,
+    author_email: Optional[str] = None,
+):
+    return GitRegistry.from_repo(repo).unassign(
+        name=name,
+        stage=stage,
+        ref=ref,
+        version=version,
+        message=message,
+        stdout=stdout,
+        simple=simple if simple is not None else False,
+        force=force,
+        delete=delete,
+        author=author,
+        author_email=author_email,
+    )
+
+
+def unregister(
+    repo: Union[str, Repo],
+    name: str,
+    ref: str = None,
+    version: str = None,
+    message: str = None,
+    stdout: bool = False,
+    simple: Optional[bool] = None,
+    force: bool = False,
+    delete: bool = False,
+    author: Optional[str] = None,
+    author_email: Optional[str] = None,
+):
+    return GitRegistry.from_repo(repo).unregister(
+        name=name,
+        ref=ref,
+        version=version,
+        message=message,
+        stdout=stdout,
+        simple=simple if simple is not None else True,
+        force=force,
+        delete=delete,
+        author=author,
+        author_email=author_email,
+    )
+
+
+def deprecate(
+    repo: Union[str, Repo],
+    name: str,
+    message: str = None,
+    stdout: bool = False,
+    simple: Optional[bool] = None,
+    force: bool = False,
+    delete: bool = False,
+    author: Optional[str] = None,
+    author_email: Optional[str] = None,
+):
+    return GitRegistry.from_repo(repo).deprecate(
+        name=name,
+        message=message,
+        stdout=stdout,
+        simple=simple if simple is not None else True,
+        force=force,
+        delete=delete,
+        author=author,
+        author_email=author_email,
+    )
+
+
 def parse_tag(name: str):
     return parse_tag_name(name)
 
@@ -152,12 +242,18 @@ def find_versions_in_stage(
     repo: Union[str, Repo],
     name: str,
     stage: str,
-    all: bool = False,
+    assignments_per_version=ASSIGNMENTS_PER_VERSION,
+    versions_per_stage=VERSIONS_PER_STAGE,
     registered_only: bool = False,
 ):
     """Return version of artifact with specific stage active"""
     return GitRegistry.from_repo(repo).which(
-        name, stage, raise_if_not_found=False, all=all, registered_only=registered_only
+        name,
+        stage,
+        raise_if_not_found=False,
+        assignments_per_version=assignments_per_version,
+        versions_per_stage=versions_per_stage,
+        registered_only=registered_only,
     )
 
 
@@ -174,7 +270,8 @@ def show(
     all_commits=False,
     truncate_hexsha=False,
     registered_only=False,
-    last_stage=False,
+    assignments_per_version=ASSIGNMENTS_PER_VERSION,
+    versions_per_stage=1,
     table: bool = False,
 ):
     return (
@@ -184,7 +281,8 @@ def show(
             all_branches=all_branches,
             all_commits=all_commits,
             registered_only=registered_only,
-            last_stage=last_stage,
+            assignments_per_version=assignments_per_version,
+            versions_per_stage=versions_per_stage,
             table=table,
             truncate_hexsha=truncate_hexsha,
         )
@@ -194,7 +292,8 @@ def show(
             all_branches=all_branches,
             all_commits=all_commits,
             registered_only=registered_only,
-            last_stage=last_stage,
+            assignments_per_version=assignments_per_version,
+            versions_per_stage=versions_per_stage,
             table=table,
             truncate_hexsha=truncate_hexsha,
         )
@@ -206,7 +305,8 @@ def _show_registry(
     all_branches=False,
     all_commits=False,
     registered_only=False,
-    last_stage: bool = False,
+    assignments_per_version: int = ASSIGNMENTS_PER_VERSION,
+    versions_per_stage: int = VERSIONS_PER_STAGE,
     table: bool = False,
     truncate_hexsha: bool = False,
 ):
@@ -218,19 +318,26 @@ def format_hexsha(hexsha):
     reg = GitRegistry.from_repo(repo)
     stages = list(reg.get_stages())
     models_state = {
-        o.name: {
-            "version": format_hexsha(o.get_latest_version(registered_only=True).name)
+        o.artifact: {
+            "version": format_hexsha(o.get_latest_version(registered_only=True).version)
             if o.get_latest_version(registered_only=True)
             else None,
             "stage": {
-                name: format_hexsha(
-                    o.get_assignments(
-                        registered_only=registered_only, last_stage=last_stage
-                    )[name][0].version
+                name: ", ".join(
+                    [
+                        format_hexsha(s.version)
+                        for s in o.get_vstages(
+                            registered_only=registered_only,
+                            assignments_per_version=assignments_per_version,
+                            versions_per_stage=versions_per_stage,
+                        )[name]
+                    ]
                 )
                 if name
-                in o.get_assignments(
-                    registered_only=registered_only, last_stage=last_stage
+                in o.get_vstages(
+                    registered_only=registered_only,
+                    assignments_per_version=assignments_per_version,
+                    versions_per_stage=versions_per_stage,
                 )
                 else None
                 for name in stages
@@ -259,7 +366,8 @@ def _show_versions(  # pylint: disable=too-many-locals
     all_branches=False,
     all_commits=False,
     registered_only=False,
-    last_stage: bool = False,
+    assignments_per_version: int = None,
+    versions_per_stage: int = None,
     table: bool = False,
     truncate_hexsha: bool = False,
 ):
@@ -271,45 +379,50 @@ def format_hexsha(hexsha):
     reg = GitRegistry.from_repo(repo)
     if raw:
         return reg.find_artifact(name).versions
-    versions = [
-        v.dict_status()
-        for v in reg.find_artifact(
-            name,
-            all_branches=all_branches,
-            all_commits=all_commits,
-        ).get_versions(
-            include_non_explicit=not registered_only, include_discovered=True
-        )
-    ]
+
+    artifact = reg.find_artifact(
+        name,
+        all_branches=all_branches,
+        all_commits=all_commits,
+    )
+    stages = artifact.get_vstages(
+        registered_only=registered_only,
+        assignments_per_version=assignments_per_version,
+        versions_per_stage=versions_per_stage,
+    )
+    versions = []
+    for v in artifact.get_versions(
+        include_non_explicit=not registered_only, include_discovered=True
+    ):
+        v = v.dict_state()
+        v["stages"] = [
+            vstage.dict_state()
+            for vstages in stages.values()
+            for vstage in vstages
+            if vstage.version == v["version"]
+        ]
+        versions.append(v)
+
     if not table:
         return versions
 
     first_keys = ["artifact", "version", "stage"]
     versions_ = []
     for v in versions:
-        v["version"] = format_hexsha(v["name"])
+        v["version"] = format_hexsha(v["version"])
         v["stage"] = ", ".join(
-            distinct(
-                s["stage"]
-                for s in (
-                    v["assignments"][::-1][:1] if last_stage else v["assignments"][::-1]
-                )
+            distinct(  # TODO: remove? no longer necessary
+                s["stage"] for s in v["stages"]
             )
         )
         v["commit_hexsha"] = format_hexsha(v["commit_hexsha"])
-        v["ref"] = v["tag"] or v["commit_hexsha"]
-        for key in (
-            "enrichments",
-            "discovered",
-            "tag",
-            "commit_hexsha",
-            "name",
-            "message",
-            "author_email",
-            "assignments",
-        ):
-            v.pop(key)
-        # v["enrichments"] = [e["source"] for e in v["enrichments"]]
+        if len(v["registrations"]) > 1:
+            raise NotImplementedInGTO(
+                "Multiple registrations are not supported currently. How you got in here?"
+            )
+        for key in list(v.keys()):
+            if key not in first_keys + ["created_at", "ref"]:
+                del v[key]
         v = OrderedDict(
             [(key, v[key]) for key in first_keys]
             + [(key, v[key]) for key in v if key not in first_keys]
@@ -332,7 +445,7 @@ def describe(
     raise NotImplementedError
 
 
-def history(  # pylint: disable=too-many-locals
+def history(
     repo: Union[str, Repo],
     artifact: str = None,
     # action: str = None,
@@ -352,62 +465,27 @@ def history(  # pylint: disable=too-many-locals
     def format_hexsha(hexsha):
         return hexsha[:7] if truncate_hexsha else hexsha
 
-    commits = [
-        OrderedDict(
-            timestamp=datetime.fromtimestamp(
-                reg.repo.commit(v.commit_hexsha).committed_date
-            ),
-            artifact=o.name,
-            event=Event.COMMIT,
-            commit=format_hexsha(v.commit_hexsha),
-            author=reg.repo.commit(v.commit_hexsha).author.name,
-            author_email=reg.repo.commit(v.commit_hexsha).author.email,
-            message=reg.repo.commit(v.commit_hexsha).message,
-        )
-        for o in artifacts.values()
-        for v in o.get_versions(include_non_explicit=True, include_discovered=True)
-    ]
-
-    registration = [
-        OrderedDict(
-            timestamp=v.created_at,
-            artifact=o.name,
-            event=Event.REGISTRATION,
-            version=format_hexsha(v.name),
-            commit=format_hexsha(v.commit_hexsha),
-            author=v.author,
-            author_email=v.author_email,
-            message=v.message,
-            # enrichments=[e.source for e in v.enrichments],
-        )
-        for o in artifacts.values()
-        for v in o.get_versions()
-    ]
-
-    assignment = [
+    events = [
         OrderedDict(
-            timestamp=p.created_at,
-            artifact=o.name,
-            event=Event.ASSIGNMENT,
-            version=format_hexsha(p.version),
-            stage=p.stage,
-            commit=format_hexsha(p.commit_hexsha),
-            author=p.author,
-            author_email=p.author_email,
-            message=p.message,
+            timestamp=e.created_at,
+            artifact=e.artifact,
+            event=type(e).__name__.lower(),
+            priority=e.priority,
+            version=format_hexsha(e.version) if hasattr(e, "version") else None,
+            stage=getattr(e, "stage", None),
+            commit=format_hexsha(e.commit_hexsha),
+            author=e.author,
+            author_email=e.author_email,
+            message=e.message,
+            ref=format_hexsha(e.ref) if e.ref == e.commit_hexsha else e.ref,
         )
         for o in artifacts.values()
-        for p in o.stages
+        for e in o.get_events()
     ]
 
-    events_order = {
-        Event.COMMIT: 1,
-        Event.REGISTRATION: 2,
-        Event.ASSIGNMENT: 3,
-    }
     events = sorted(
-        commits + registration + assignment,
-        key=lambda x: (x["timestamp"], events_order[x["event"]]),
+        events,
+        key=lambda x: (x["timestamp"], x["priority"]),
     )
     if not ascending:
         events.reverse()
@@ -417,13 +495,12 @@ def format_hexsha(hexsha):
         return events
     keys_order = [
         "timestamp",
-        "artifact",
+        ARTIFACT,
         "event",
         VERSION,
         STAGE,
-        # "enrichments",
-        "commit",
-        "author",
+        COMMIT,
+        "ref",
     ]
     keys_order = [c for c in keys_order if any(c in event for event in events)]
     events = [
diff --git a/gto/base.py b/gto/base.py
index 466c0050..3e3701eb 100644
--- a/gto/base.py
+++ b/gto/base.py
@@ -1,61 +1,340 @@
 from datetime import datetime
-from typing import Dict, FrozenSet, List, Optional, Union
+from typing import Any, Dict, FrozenSet, List, Optional, Sequence, Union
 
 import git
 from pydantic import BaseModel
 
 from gto.config import RegistryConfig
-from gto.constants import Action, VersionSort
-from gto.ext import Enrichment
+from gto.constants import (
+    ASSIGNMENTS_PER_VERSION,
+    VERSIONS_PER_STAGE,
+    Action,
+    VersionSort,
+)
 from gto.versions import SemVer
 
-from .exceptions import ArtifactNotFound, ManyVersions, VersionRequired
-
-
-class BaseAssignment(BaseModel):
+from .exceptions import (
+    ArtifactNotFound,
+    ManyVersions,
+    NoStageForVersion,
+    NotImplementedInGTO,
+    VersionRequired,
+    WrongArgs,
+)
+
+
+# EVENTS: deprecation, registration, deregistration, assignment, unassignment
+class BaseEvent(BaseModel):
+    priority: int
+    addition: bool
+    # some fields here are implementation details of tag-based
+    # if we add other approaches beside tags, we'll need to fix this
     artifact: str
-    version: str
-    stage: str
     created_at: datetime
     author: str
-    author_email: Optional[str]  # remove as optional later
-    message: Optional[str]  # remove as optional later
+    author_email: str
+    message: str
     commit_hexsha: str
+
+    @property
+    def event(self):
+        return self.__class__.__name__.lower()
+
+    def dict_state(self, exclude=None):
+        state = self.dict(exclude=exclude)
+        state["event"] = self.event
+        return state
+
+    @property
+    def ref(self):
+        return getattr(self, "tag", self.commit_hexsha)
+
+
+class Commit(BaseEvent):
+    priority = 0
+    addition = True
+    version: str
+    enrichments: List[Any]
+    committer: str
+    committer_email: str
+
+    def __str__(self):
+        return f'Artifact "{self.artifact}" is annotated'
+
+
+class Creation(BaseEvent):
+    priority = 1
+    addition = True
     tag: str
 
+    def __str__(self):
+        return f'Artifact "{self.artifact}" was created'
+
+
+class Deprecation(BaseEvent):
+    priority = 2
+    addition = False
+    tag: str
+
+    def __str__(self):
+        return f'Artifact "{self.artifact}" was deprecated'
+
+
+class Registration(BaseEvent):
+    priority = 3
+    addition = True
+    tag: str
+    version: str
+
+    def __str__(self):
+        return f'Version "{self.version}" of artifact "{self.artifact}" was registered'
+
+
+class Deregistration(BaseEvent):
+    priority = 4
+    addition = False
+    tag: str
+    version: str
+
+    def __str__(self):
+        return (
+            f'Version "{self.version}" of artifact "{self.artifact}" was deregistered'
+        )
 
-class BaseVersion(BaseModel):
+
+class Assignment(BaseEvent):
+    priority = 5
+    addition = True
+    tag: str
+    version: str
+    stage: str
+
+    def __str__(self) -> str:
+        return f'Stage "{self.stage}" was assigned to version "{self.version}" of artifact "{self.artifact}"'
+
+
+class Unassignment(BaseEvent):
+    priority = 6
+    addition = False
+    tag: str
+    version: str
+    stage: str
+
+    def __str__(self) -> str:
+        return f'Stage "{self.stage}" was unassigned from version "{self.version}" of artifact "{self.artifact}"'
+
+
+# ENTITIES: Artifact, Version, Stage
+class BaseObject(BaseModel):
     artifact: str
-    name: str
-    created_at: datetime
-    author: str
-    author_email: Optional[str]  # remove as optional later
-    message: Optional[str]  # remove as optional later
+
+    def add_event(self, event: BaseEvent):
+        raise NotImplementedError()
+
+    def get_events(
+        self, direct=True, indirect=True, ascending: bool = False
+    ) -> Sequence[BaseEvent]:
+        raise NotImplementedError()
+
+    @property
+    def is_active(self):
+        raise NotImplementedError()
+
+    @property
+    def activated_at(self):
+        raise NotImplementedError()
+
+    @property
+    def authoring_event(self):
+        addition_events = [
+            e for e in self.get_events(direct=True, indirect=False) if e.addition
+        ]
+        if addition_events:
+            return addition_events[0]
+        events = self.get_events(direct=False, indirect=True)
+        except_enrichment = [e for e in events if e.event != "enrichment"]
+        if except_enrichment:
+            return except_enrichment[0]
+        return events[0]
+
+    @property
+    def created_at(self):
+        return self.authoring_event.created_at
+
+    @property
+    def author(self):
+        return self.authoring_event.author
+
+    @property
+    def author_email(self):
+        return self.authoring_event.author_email
+
+    @property
+    def message(self):
+        return self.authoring_event.message
+
+    @property
+    def ref(self):
+        return self.authoring_event.ref
+
+    def dict_state(self, exclude=None):
+        version = self.dict(exclude=exclude)
+        version["is_active"] = self.is_active
+        version["activated_at"] = self.activated_at
+        version["created_at"] = self.created_at
+        version["author"] = self.author
+        version["author_email"] = self.author_email
+        version["message"] = self.message
+        version["ref"] = self.ref
+        return version
+
+
+class VStage(BaseObject):
+    commit_hexsha: str
+    version: str
+    stage: str
+    assignments: List[Assignment] = []
+    unassignments: List[Unassignment] = []
+
+    def add_event(self, event: BaseEvent):
+        if event in self.get_events():
+            return event
+        if isinstance(event, Assignment):
+            self.assignments.append(event)
+            self.assignments.sort(key=lambda e: e.created_at)
+        elif isinstance(event, Unassignment):
+            self.unassignments.append(event)
+            self.unassignments.sort(key=lambda e: e.created_at)
+        else:
+            raise NotImplementedInGTO(f"Unknown event {event} of class {type(event)}")
+        return event
+
+    def get_events(
+        self, direct=True, indirect=True, ascending=False
+    ) -> Sequence[BaseEvent]:  # pylint: disable=unused-argument
+        return sorted(
+            self.assignments + self.unassignments if direct else [], key=lambda e: e.created_at  # type: ignore
+        )[:: 1 if ascending else -1]
+
+    @property
+    def is_active(self):
+        return isinstance(self.get_events()[0], Assignment)
+
+    @property
+    def activated_at(self):
+        if isinstance(self.get_events()[0], Assignment):
+            return self.get_events()[0].created_at
+        return None
+
+    def dict_state(self, exclude=None):
+        state = super().dict_state(exclude=exclude)
+        state["assignments"] = [a.dict_state() for a in self.assignments]
+        state["unassignments"] = [a.dict_state() for a in self.unassignments]
+        return state
+
+
+class Version(BaseObject):
     commit_hexsha: str
-    discovered: bool = False
-    tag: Optional[str] = None
-    assignments: List[BaseAssignment] = []
-    enrichments: List[Enrichment] = []
+    version: str
+    enrichments: List[Commit] = []
+    registrations: List[Registration] = []
+    deregistrations: List[Deregistration] = []
+    stages: Dict[str, VStage] = {}
+
+    def add_event(self, event: BaseEvent):
+        if event in self.get_events():
+            return event
+        if isinstance(event, Registration):
+            self.registrations.append(event)
+            self.registrations.sort(key=lambda e: e.created_at)
+        elif isinstance(event, Deregistration):
+            self.deregistrations.append(event)
+            self.deregistrations.sort(key=lambda e: e.created_at)
+        elif isinstance(event, Commit):
+            self.enrichments.append(event)
+            self.enrichments.sort(key=lambda e: e.created_at)
+        elif isinstance(event, (Assignment, Unassignment)):
+            self.get_vstage(event.stage, create_new=True).add_event(event)
+        else:
+            raise NotImplementedInGTO(f"Unknown event {event} of class {type(event)}")
+        return event
+
+    def get_events(self, direct=True, indirect=True, ascending=False):
+        return sorted(
+            (self.registrations + self.deregistrations if direct else [])
+            + (
+                self.enrichments
+                + [e for s in self.stages.values() for e in s.get_events()]
+                if indirect
+                else []
+            ),
+            key=lambda e: e.created_at,
+        )[:: 1 if ascending else -1]
+
+    @property
+    def is_active(self):
+        direct_events = self.get_events(direct=True, indirect=False)
+        if direct_events:
+            return isinstance(direct_events[0], Registration)
+        indirect_events = self.get_events(direct=False, indirect=True)
+        if indirect_events:
+            return True
+        return False
+
+    @property
+    def activated_at(self):
+        # TODO: handle the case with deregistration?
+        if self.registrations:
+            return self.registrations[0].created_at
+        return self.get_events(ascending=True)[0].created_at
 
     @property
     def is_registered(self):
         """Tells if this is an explicitly registered version"""
-        return self.tag is not None  # SemVer.is_valid(self.name)
+        return len(self.registrations) > 0  # SemVer.is_valid(self.name)
 
     @property
-    def version(self):
-        # TODO: this should be read from config, how to pass it down here?
-        return SemVer(self.name)
-
-    def add_assignment(self, assignment: BaseAssignment):
-        self.assignments.append(assignment)
-        self.assignments.sort(key=lambda p: p.created_at)
-
-    def dict_status(self):
-        version = self.dict(exclude={"assignments"})
-        version["assignments"] = (
-            [a.dict() for a in self.assignments] if self.assignments else None
-        )
+    def discovered(self):
+        return len(self.get_events(direct=True, indirect=False)) == 0
+
+    @property
+    def get_enrichments_info(self):
+        if len(self.enrichments) > 1:
+            raise NotImplementedInGTO(
+                "Multiple enrichments for a single version are not supported"
+            )
+        return self.enrichments[0].enrichments
+
+    @property
+    def semver(self):
+        return SemVer(self.version)
+
+    def get_vstage(self, stage, create_new=False):
+        if create_new and stage not in self.stages:
+            self.stages[stage] = VStage(
+                artifact=self.artifact,
+                version=self.version,
+                stage=stage,
+                commit_hexsha=self.commit_hexsha,
+            )
+        if stage in self.stages:
+            return self.stages[stage]
+        raise NoStageForVersion(self.artifact, self.version, stage)
+
+    def get_vstages(self, active_only=True, ascending=False):
+        return sorted(
+            [s for s in self.stages.values() if not active_only or s.is_active],
+            key=lambda s: s.activated_at,
+        )[:: 1 if ascending else -1]
+
+    def dict_state(self, exclude=None, assignments_per_version=ASSIGNMENTS_PER_VERSION):
+        if assignments_per_version < -1:
+            raise WrongArgs("'assignments_per_version' must be >= -1")
+        version = super().dict_state(exclude=exclude)
+        version["discovered"] = self.discovered
+        version["stages"] = [stage.dict_state() for stage in self.get_vstages()]
+        if assignments_per_version >= 0:
+            version["stages"] = version["stages"][:assignments_per_version]
         return version
 
 
@@ -63,9 +342,11 @@ def sort_versions(
     versions,
     sort=VersionSort.SemVer,
     ascending=False,
-    version="name",
+    version="version",
     timestamp="created_at",
 ):
+    """This function is used both in Studio and in GTO"""
+
     def get(obj, key):
         if isinstance(obj, dict):
             return obj[key]
@@ -92,29 +373,63 @@ def get(obj, key):
             versions,
             key=lambda x: get(x, timestamp),
         )[:: 1 if ascending else -1]
-    # if ascending:
-    #     sorted_versions.reverse()
     return sorted_versions
 
 
-class BaseArtifact(BaseModel):
-    name: str
-    versions: List[BaseVersion]
+class Artifact(BaseObject):
+    versions: List[Version]
+    creations: List[Creation] = []
+    deprecations: List[Deprecation] = []
+
+    def add_event(self, event: BaseEvent):
+        if event in self.get_events():
+            return event
+        if isinstance(event, Creation):
+            self.creations.append(event)
+            self.creations.sort(key=lambda e: e.created_at)
+        elif isinstance(event, Deprecation):
+            self.deprecations.append(event)
+            self.deprecations.sort(key=lambda e: e.created_at)
+        elif isinstance(
+            event, (Registration, Deregistration, Assignment, Unassignment, Commit)
+        ):
+            self.find_version(
+                event.version, commit_hexsha=event.commit_hexsha, create_new=True
+            ).add_event(  # type: ignore
+                event
+            )
+        else:
+            raise NotImplementedInGTO(f"Unknown event {event} of class {type(event)}")
+        return event
+
+    def get_events(
+        self, direct=True, indirect=True, ascending=False
+    ) -> Sequence[BaseEvent]:
+        return sorted(
+            (self.creations + self.deprecations if direct else [])  # type: ignore
+            + ([e for v in self.versions for e in v.get_events()] if indirect else []),
+            key=lambda e: e.created_at,
+        )[:: 1 if ascending else -1]
 
     @property
-    def stages(self):
-        return [
-            assignment
-            for version in self.versions
-            for assignment in version.assignments
-        ]
+    def is_active(self):
+        if len(self.get_events()) == 0:
+            return False
+        return not isinstance(self.get_events()[0], Deprecation)
+
+    @property
+    def activated_at(self):
+        # TODO: handle the case with deprecation
+        if self.creations:
+            return self.creations[0].created_at
+        return self.get_events(ascending=True)[0].created_at
 
     @property
     def unique_stages(self):
-        return {assignment.stage for assignment in self.stages}
+        return set(self.get_vstages())
 
     def __repr__(self) -> str:
-        versions = ", ".join(f"'{v.name}'" for v in self.versions)
+        versions = ", ".join(f"'{v.version}'" for v in self.versions)
         stages = ", ".join(f"'{p}'" for p in self.unique_stages)
         return f"Artifact(versions=[{versions}], stages=[{stages}])"
 
@@ -124,19 +439,22 @@ def get_versions(
         include_discovered=False,
         sort=VersionSort.SemVer,
         ascending=False,
-    ) -> List[BaseVersion]:
+    ) -> List[Version]:
         versions = [
             v
             for v in self.versions
-            if (v.is_registered and not v.discovered)
-            or (include_discovered and v.discovered)
-            or (include_non_explicit and not v.is_registered)
+            if v.is_active
+            and (
+                (v.is_registered and not v.discovered)
+                or (include_discovered and v.discovered)
+                or (include_non_explicit and not v.is_registered)
+            )
         ]
         return sort_versions(versions, sort=sort, ascending=ascending)
 
     def get_latest_version(
         self, registered_only=False, sort=VersionSort.SemVer
-    ) -> Optional[BaseVersion]:
+    ) -> Optional[Version]:
         versions = self.get_versions(
             include_non_explicit=not registered_only, sort=sort
         )
@@ -144,12 +462,17 @@ def get_latest_version(
             return versions[0]
         return None
 
-    def get_assignments(
+    def get_vstages(
         self,
         registered_only=False,
-        last_stage=False,
+        assignments_per_version=ASSIGNMENTS_PER_VERSION,
+        versions_per_stage=VERSIONS_PER_STAGE,
         sort=VersionSort.SemVer,
-    ) -> Dict[str, Union[BaseAssignment, List[BaseAssignment]]]:
+    ):
+        if assignments_per_version < -1:
+            raise WrongArgs("'assignments_per_version' must be >= -1")
+        if versions_per_stage < -1:
+            raise WrongArgs("'versions_per_stage' must be >=-1")
         versions = self.get_versions(
             include_non_explicit=not registered_only, sort=sort
         )
@@ -157,62 +480,28 @@ def get_assignments(
             # for this sort we need to sort not versions, as above ^
             # but assignments themselves
             raise NotImplementedError("Sorting by timestamp is not implemented yet")
-        stages: Dict[str, List[BaseAssignment]] = {}
+        stages: Dict[str, List[VStage]] = {}
         for version in versions:
             for a in (
-                version.assignments[::-1][:1] if last_stage else version.assignments
+                version.get_vstages(ascending=True)[:assignments_per_version]
+                if assignments_per_version > -1
+                else version.get_vstages(ascending=True)
             ):
                 if a.stage not in stages:
                     stages[a.stage] = []
+                if (
+                    versions_per_stage > -1  # pylint: disable=chained-comparison
+                    and len(stages[a.stage]) >= versions_per_stage
+                ):
+                    continue
                 if a.version not in [i.version for i in stages[a.stage]]:
                     stages[a.stage].append(a)
-        return stages  # type: ignore
-
-    def add_version(self, version: BaseVersion):
-        self.versions.append(version)
-
-    def add_assignment(self, assignment: BaseAssignment):
-        self.find_version(name=assignment.version).add_assignment(assignment)  # type: ignore
-
-    def update_enrichments(self, version, enrichments):
-        self.find_version(
-            name=version, include_discovered=True
-        ).enrichments = enrichments
+        return stages
 
     @property
     def discovered(self):
         return any(not v.discovered for v in self.versions)
 
-    # @overload
-    # def find_version(
-    #     self,
-    #     name: str = None,
-    #     commit_hexsha: str = None,
-    #     raise_if_not_found: Literal[True] = ...,
-    #     allow_multiple: Literal[False] = ...,
-    # ) -> BaseVersion:
-    #     ...
-
-    # @overload
-    # def find_version(
-    #     self,
-    #     name: str = None,
-    #     commit_hexsha: str = None,
-    #     raise_if_not_found: Literal[False] = ...,
-    #     allow_multiple: Literal[False] = ...,
-    # ) -> Optional[BaseVersion]:
-    #     ...
-
-    # @overload
-    # def find_version(
-    #     self,
-    #     name: str = None,
-    #     commit_hexsha: str = None,
-    #     raise_if_not_found: Literal[False] = ...,
-    #     allow_multiple: Literal[True] = ...,
-    # ) -> List[BaseVersion]:
-    #     ...
-
     def find_version(
         self,
         name: str = None,
@@ -220,23 +509,31 @@ def find_version(
         raise_if_not_found: bool = False,
         allow_multiple=False,
         include_discovered=False,
-    ) -> Union[None, BaseVersion, List[BaseVersion]]:
+        create_new=False,
+    ) -> Union[None, Version, List[Version]]:
         versions = [
             v
             for v in self.versions
-            if (v.name == name if name else True)
+            if (v.version == name if name else True)
             and (v.commit_hexsha == commit_hexsha if commit_hexsha else True)
             and (True if include_discovered else not v.discovered)
         ]
-
-        if allow_multiple:
+        if create_new and not versions:
+            v = Version(
+                artifact=self.artifact,
+                version=name or commit_hexsha,
+                commit_hexsha=commit_hexsha,
+            )
+            self.versions.append(v)
+            versions = [v]
+        if allow_multiple and versions:
             return versions
         if raise_if_not_found and not versions:
-            raise VersionRequired(name=self.name)
+            raise VersionRequired(name=self.artifact)
         if len(versions) > 1:
             raise ManyVersions(
-                name=self.name,
-                versions=[v.name for v in versions],
+                name=self.artifact,
+                versions=[v.version for v in versions],
             )
         return versions[0] if versions else None
 
@@ -250,31 +547,29 @@ def find_version_at_commit(
                 raise_if_not_found=True,
                 allow_multiple=True,
             )
-            if (v.created_at <= latest_datetime if latest_datetime else True)  # type: ignore
+            if (v.activated_at <= latest_datetime if latest_datetime else True)  # type: ignore
         ][-1]
 
 
 class BaseRegistryState(BaseModel):
-    artifacts: Dict[str, BaseArtifact] = {}
+    artifacts: Dict[str, Artifact] = {}
 
     class Config:
         arbitrary_types_allowed = True
 
     def add_artifact(self, name):
-        self.artifacts[name] = BaseArtifact(name=name, versions=[])
+        self.artifacts[name] = Artifact(artifact=name, versions=[])
 
-    def update_artifact(self, artifact: BaseArtifact):
-        self.artifacts[artifact.name] = artifact
+    def update_artifact(self, artifact: Artifact):
+        self.artifacts[artifact.artifact] = artifact
 
     def get_artifacts(self):
         return self.artifacts
 
-    def find_artifact(self, name: str, create_new=False) -> BaseArtifact:
-        if not name:
-            raise ValueError("Artifact name is required")
+    def find_artifact(self, name: str, create_new=False) -> Artifact:
         if name not in self.artifacts:
             if create_new:
-                self.artifacts[name] = BaseArtifact(name=name, versions=[])
+                self.artifacts[name] = Artifact(artifact=name, versions=[])
             else:
                 raise ArtifactNotFound(name)
         return self.artifacts[name]
@@ -291,25 +586,26 @@ def find_commit(self, name, version):
         )
 
     def which(
-        self, name, stage, raise_if_not_found=True, all=False, registered_only=False
+        self,
+        name,
+        stage,
+        raise_if_not_found=True,
+        assignments_per_version=None,
+        versions_per_stage=None,
+        registered_only=False,
     ):
         """Return stage active in specific stage"""
-        assigned = self.find_artifact(name).get_assignments(
-            registered_only=registered_only
+        assigned = self.find_artifact(name).get_vstages(
+            registered_only=registered_only,
+            assignments_per_version=assignments_per_version,
+            versions_per_stage=versions_per_stage,
         )
         if stage in assigned:
-            return assigned[stage] if all else assigned[stage][0]
+            return assigned[stage]
         if raise_if_not_found:
             raise ValueError(f"Stage {stage} not found for {name}")
         return None
 
-    def sort(self):
-        for name in self.artifacts:  # pylint: disable=consider-using-dict-items
-            self.artifacts[name].versions.sort(key=lambda x: (x.created_at, x.name))
-            self.artifacts[name].stages.sort(
-                key=lambda x: (x.created_at, x.version, x.stage)
-            )
-
 
 class BaseManager(BaseModel):
     repo: git.Repo
@@ -323,14 +619,3 @@ def update_state(
         self, state: BaseRegistryState
     ) -> BaseRegistryState:  # pylint: disable=no-self-use
         raise NotImplementedError
-
-    # better to uncomment this for typing, but it breaks the pylint
-
-    # def register(self, name, version, ref, message):
-    #     raise NotImplementedError
-
-    # def assign(self, name, stage, ref, message):
-    #     raise NotImplementedError
-
-    def check_ref(self, ref: str, state: BaseRegistryState):
-        raise NotImplementedError
diff --git a/gto/cli.py b/gto/cli.py
index 6b9e5e51..3843efae 100644
--- a/gto/cli.py
+++ b/gto/cli.py
@@ -12,6 +12,7 @@
 from typer.core import TyperCommand, TyperGroup
 
 import gto
+from gto.constants import ASSIGNMENTS_PER_VERSION, VERSIONS_PER_STAGE
 from gto.exceptions import GTOException, NotImplementedInGTO, WrongArgs
 from gto.ui import EMOJI_FAIL, EMOJI_GTO, EMOJI_OK, bold, cli_echo, color, echo
 from gto.utils import format_echo, make_ready_to_serialize
@@ -181,10 +182,24 @@ def GTOGroupSection(section):
     add_completion=False,
 )
 
+# General Typer arguments and options
 arg_name = Argument(..., help="Artifact name")
 arg_version = Argument(..., help="Artifact version")
 arg_stage = Argument(..., help="Stage to assign")
+option_version = Option(None, "--version", help="Version to register")
+option_stage = Option(None, "--stage", help="Stage to assign")
+option_to_version = Option(
+    None, "--to-version", help="Version to use for stage assignment"
+)
+option_delete = Option(
+    False,
+    "-d",
+    "--delete",
+    is_flag=True,
+    help="Delete the git tag(s) instead of creating the new one",
+)
 
+# Typer options to control git-related operations
 option_rev = Option(None, "--rev", help="Repo revision to use", show_default=True)
 option_repo = Option(".", "-r", "--repo", help="Repository to use", show_default=True)
 option_all_branches = Option(
@@ -197,14 +212,45 @@ def GTOGroupSection(section):
 option_all_commits = Option(
     False, "-A", "--all-commits", is_flag=True, help="Read all commits"
 )
+option_message = Option(
+    None, "--message", "-m", help="Message to annotate git tag with"
+)
+option_force = Option(
+    False,
+    "--force",
+    is_flag=True,
+    help="Create a git tag even if it already exists and is in effect",
+)
+
+
+def name_callback(  # pylint: disable=inconsistent-return-statements
+    ctx: typer.Context,
+    param: typer.CallbackParam,  # pylint: disable=unused-argument
+    value: str,
+):
+    if ctx.resilient_parsing:
+        return
+    allowed_values = ["auto", "true", "false"]
+    if value not in allowed_values:
+        raise typer.BadParameter(f"Only one of {allowed_values} is allowed")
+    return {"auto": None, "true": True, "false": False}[value]
+
+
+option_simple = Option(
+    "auto",
+    "--simple",
+    help="[auto, true, false] Use simple notation, e.g. rf#prod instead of rf#prod-5",
+    callback=name_callback,
+)
+
+# Typer options to control and filter the output
 option_all = Option(False, "--all", "-a", help="Return all versions sorted")
-option_name = Option(None, "--name", "-n", help="Artifact name", show_default=True)
-option_ascending = Option(
+option_registered_only = Option(
     False,
-    "--ascending",
-    "--asc",
-    help="Show new first",
-    show_default=True,
+    "--registered-only",
+    "--ro",
+    is_flag=True,
+    help="Show only registered versions",
 )
 option_expected = Option(
     False,
@@ -214,16 +260,43 @@ def GTOGroupSection(section):
     help="Return exit code 1 if no result",
     show_default=True,
 )
-option_type_bool = Option(
+option_assignments_per_version = Option(
+    ASSIGNMENTS_PER_VERSION,
+    "--av",
+    "--assignments-per-version",
+    help="Show N last stages for each version. -1 for all",
+)
+option_versions_per_stage = Option(
+    VERSIONS_PER_STAGE,
+    "--vs",
+    "--versions-per-stage",
+    help="Show N last versions for each stage. -1 for all. Applied after 'assignments-per-version'",
+)
+
+# Typer options to format the output
+option_ascending = Option(
+    False,
+    "--ascending",
+    "--asc",
+    help="Show new first",
+    show_default=True,
+)
+option_show_name = Option(False, "--name", is_flag=True, help="Output artifact name")
+option_show_version = Option(
+    False, "--version", is_flag=True, help="Output artifact version"
+)
+option_show_event = Option(False, "--event", is_flag=True, help="Output the event")
+option_show_stage = Option(False, "--stage", is_flag=True, help="Output artifact stage")
+option_show_type = Option(
     False, "--type", is_flag=True, help="Show type", show_default=True
 )
-option_path_bool = Option(
+option_show_path = Option(
     False, "--path", is_flag=True, help="Show path", show_default=True
 )
-option_ref_bool = Option(
+option_show_ref = Option(
     False, "--ref", is_flag=True, help="Show ref", show_default=True
 )
-option_description_bool = Option(
+option_show_description = Option(
     False, "--description", is_flag=True, help="Show description", show_default=True
 )
 option_json = Option(
@@ -254,13 +327,6 @@ def GTOGroupSection(section):
     help="Print output in table format",
     show_default=True,
 )
-option_registered_only = Option(
-    False,
-    "--registered-only",
-    "--ro",
-    is_flag=True,
-    help="Show only registered versions",
-)
 
 
 @app.callback("gto", invoke_without_command=True, no_args_is_help=True)
@@ -340,7 +406,7 @@ def inner(ctx, *iargs, **ikwargs):
                         )
                     )
                     echo(
-                        "Please report it here: <https://github.com/iterative/gto/issues>"
+                        "Please report it here running with '--traceback' flag: <https://github.com/iterative/gto/issues>"
                     )
                 raise typer.Exit(1) from e
             finally:
@@ -407,9 +473,7 @@ def register(
     version: Optional[str] = Option(
         None, "--version", "--ver", help="Version name in SemVer format"
     ),
-    message: Optional[str] = Option(
-        None, "--message", "-m", help="Message to annotate git tag with"
-    ),
+    message: Optional[str] = option_message,
     bump_major: bool = Option(
         False, "--bump-major", is_flag=True, help="Bump major version"
     ),
@@ -459,18 +523,9 @@ def assign(
         "--version",
         help="If you provide REF, this will be used to name new version",
     ),
-    message: Optional[str] = Option(
-        None, "--message", "-m", help="Message to annotate git tag with"
-    ),
-    simple: bool = Option(
-        False,
-        "--simple",
-        is_flag=True,
-        help="Use simple notation, e.g. rf#prod instead of rf#prod-5",
-    ),
-    force: bool = Option(
-        False, help="Assign even if version is already in required Stage"
-    ),
+    message: Optional[str] = option_message,
+    simple: str = option_simple,
+    force: bool = option_force,
     skip_registration: bool = Option(
         False,
         "--sr",
@@ -479,7 +534,7 @@ def assign(
         help="Don't register a version at specified commit",
     ),
 ):
-    """Move an artifact version to a specific stage
+    """Assign stage to specific artifact version
 
     Examples:
         Assign "nn" to "prod" at specific ref:
@@ -511,18 +566,76 @@ def assign(
         ref,
         name_version,
         message=message,
-        simple=simple,
+        simple=simple,  # type: ignore
         force=force,
         skip_registration=skip_registration,
         stdout=True,
     )
 
 
+@gto_command(section=CommandGroups.modifying)
+def deprecate(
+    repo: str = option_repo,
+    name: str = arg_name,
+    version: str = Argument(None, help="Artifact version"),
+    stage: str = Argument(None, help="Stage to unassign"),
+    message: Optional[str] = option_message,
+    simple: str = option_simple,
+    force: bool = option_force,
+    delete: bool = option_delete,
+):
+    """Deprecate artifact, deregister a version, or unassign a stage
+
+    Examples:
+        Deprecate an artifact:
+        $ gto deprecate nn
+
+        Deprecate a version:
+        $ gto deprecate nn v0.0.1
+
+        Unassign a stage:
+        $ gto deprecate nn v0.0.1 prod
+    """
+    if stage:
+        gto.api.unassign(
+            repo=repo,
+            name=name,
+            version=version,
+            stage=stage,
+            message=message,
+            simple=simple,  # type: ignore
+            force=force,
+            delete=delete,
+            stdout=True,
+        )
+    elif version:
+        gto.api.unregister(
+            repo=repo,
+            name=name,
+            version=version,
+            message=message,
+            simple=simple,  # type: ignore
+            force=force,
+            delete=delete,
+            stdout=True,
+        )
+    else:
+        gto.api.deprecate(
+            repo=repo,
+            name=name,
+            message=message,
+            simple=simple,  # type: ignore
+            force=force,
+            delete=delete,
+            stdout=True,
+        )
+
+
 @gto_command(section=CommandGroups.querying)
 def latest(
     repo: str = option_repo,
     name: str = arg_name,
-    ref: bool = option_ref_bool,
+    ref: bool = option_show_ref,
 ):
     """Find the latest version of artifact
 
@@ -532,9 +645,9 @@ def latest(
     latest_version = gto.api.find_latest_version(repo, name)
     if latest_version:
         if ref:
-            echo(latest_version.tag or latest_version.commit_hexsha)
+            echo(latest_version.ref or latest_version.commit_hexsha)
         else:
-            echo(latest_version.name)
+            echo(latest_version.version)
 
 
 @gto_command(section=CommandGroups.querying)
@@ -542,8 +655,9 @@ def which(
     repo: str = option_repo,
     name: str = arg_name,
     stage: str = arg_stage,
-    ref: bool = option_ref_bool,
-    all: bool = option_all,
+    ref: bool = option_show_ref,
+    assignments_per_version: int = option_assignments_per_version,
+    versions_per_stage: int = option_versions_per_stage,
     registered_only: bool = option_registered_only,
     # ascending: bool = option_ascending,
 ):
@@ -555,18 +669,19 @@ def which(
         Print git tag that did the assignment:
         $ gto which nn prod --ref
     """
-    version = gto.api.find_versions_in_stage(
-        repo, name, stage, all=all, registered_only=registered_only
+    versions = gto.api.find_versions_in_stage(
+        repo,
+        name,
+        stage,
+        assignments_per_version=assignments_per_version,
+        versions_per_stage=versions_per_stage,
+        registered_only=registered_only,
     )
-    if version:
-        if all:
-            # if ascending:
-            #     version.reverse()
-            format_echo([v.version for v in version], "lines")
-        elif ref:
-            echo(version.tag or version.commit_hexsha)
+    if versions:
+        if ref:
+            format_echo([v.ref for v in versions], "lines")
         else:
-            echo(version.version)
+            format_echo([v.version for v in versions], "lines")
 
 
 @gto_command(hidden=True)
@@ -591,17 +706,10 @@ def check_ref(
     repo: str = option_repo,
     ref: str = Argument(..., help="Git reference to analyze"),
     json: bool = option_json,
-    registration: bool = Option(
-        False, "--registration", is_flag=True, help="Check if ref registers a version"
-    ),
-    assignment: bool = Option(
-        False, "--assignment", is_flag=True, help="Check if ref assigns a stage"
-    ),
-    name: bool = Option(False, "--name", is_flag=True, help="Output artifact name"),
-    version: bool = Option(
-        False, "--version", is_flag=True, help="Output artifact version"
-    ),
-    stage: bool = Option(False, "--stage", is_flag=True, help="Output artifact stage"),
+    name: bool = option_show_name,
+    version: bool = option_show_version,
+    event: bool = option_show_event,
+    stage: bool = option_show_stage,
 ):
     """Find out the artifact version registered/assigned with ref
 
@@ -611,44 +719,28 @@ def check_ref(
         $ gto check-ref rf#prod --version
     """
     assert (
-        sum(bool(i) for i in (json, registration, assignment, name, version, stage))
-        <= 1
+        sum(bool(i) for i in (json, event, name, version, stage)) <= 1
     ), "Only one output formatting flags is allowed"
-    result = {
-        action: {name: version.dict() for name, version in found.items()}
-        for action, found in gto.api.check_ref(repo, ref).items()
-    }
-    version_dict = list(result["version"].values())[0] if result["version"] else {}
-    stage_dict = list(result["stage"].values())[0] if result["stage"] else {}
+    result = gto.api.check_ref(repo, ref)
+    if len(result) > 1:
+        NotImplementedInGTO("Checking refs that created 1+ events is not supported")
+    if len(result) == 0:
+        return
+    found_event = result[0]
     if json:
-        format_echo(result, "json")
+        format_echo(found_event.dict_state(exclude={"priority", "addition"}), "json")
     elif name:
-        if version_dict:
-            echo(version_dict["artifact"])
-        if stage_dict:
-            echo(stage_dict["artifact"])
+        format_echo(found_event.artifact, "line")
     elif version:
-        if version_dict:
-            echo(version_dict["name"])
-        if stage_dict:
-            echo(stage_dict["version"])
+        if hasattr(found_event, "version"):
+            format_echo(found_event.version, "line")
     elif stage:
-        if version_dict:
-            raise NotImplementedInGTO(
-                "--stage is not implemented for version assignment git tag"
-            )
-        if stage_dict:
-            echo(stage_dict["stage"])
-    elif (registration or not assignment) and version_dict:
-        echo(
-            f"""{EMOJI_OK} Version "{version_dict["name"]}" of artifact """
-            f""""{version_dict["artifact"]}" was registered"""
-        )
-    elif (assignment or not registration) and stage_dict:
-        echo(
-            f"""{EMOJI_OK} Stage "{stage_dict["stage"]}" was assigned to version"""
-            f'''"{stage_dict["version"]}" of artifact "{stage_dict["artifact"]}"'''
-        )
+        if hasattr(found_event, "stage"):
+            format_echo(found_event.stage, "line")
+    elif event:
+        format_echo(found_event.event, "line")
+    else:
+        echo(f"{EMOJI_OK} {found_event}")
 
 
 @gto_command(section=CommandGroups.querying)
@@ -661,9 +753,8 @@ def show(
     plain: bool = option_plain,
     name_only: bool = option_name_only,
     registered_only: bool = option_registered_only,
-    last_stage: bool = Option(
-        False, "--ls", "--last-stage", help="Show only the last stage for each version"
-    ),
+    assignments_per_version: int = option_assignments_per_version,
+    versions_per_stage: int = option_versions_per_stage,
 ):
     """Show the registry state
 
@@ -688,7 +779,8 @@ def show(
             all_branches=all_branches,
             all_commits=all_commits,
             registered_only=registered_only,
-            last_stage=last_stage,
+            assignments_per_version=assignments_per_version,
+            versions_per_stage=versions_per_stage,
             table=False,
         )
         if name_only:
@@ -703,7 +795,8 @@ def show(
                 all_branches=all_branches,
                 all_commits=all_commits,
                 registered_only=registered_only,
-                last_stage=last_stage,
+                assignments_per_version=assignments_per_version,
+                versions_per_stage=versions_per_stage,
                 table=True,
                 truncate_hexsha=True,
             ),
@@ -713,16 +806,6 @@ def show(
         )
 
 
-# Actions = StrEnum("Actions", ALIAS.REGISTER + ALIAS.PROMOTE, type=str)  # type: ignore
-# action: List[Actions] = Argument(None),  # type: ignore
-# @click.option(
-#     "--action",
-#     required=False,
-#     type=click.Choice(ALIAS.COMMIT + ALIAS.REGISTER + ALIAS.PROMOTE),
-#     nargs=-1,
-# )
-
-
 @gto_command(section=CommandGroups.querying)
 def history(
     repo: str = option_repo,
@@ -834,9 +917,9 @@ def describe(
     repo: str = option_repo,
     name: str = arg_name,
     rev: str = option_rev,
-    type: Optional[bool] = option_type_bool,
-    path: Optional[bool] = option_path_bool,
-    description: Optional[bool] = option_description_bool,
+    type: Optional[bool] = option_show_type,
+    path: Optional[bool] = option_show_path,
+    description: Optional[bool] = option_show_description,
 ):
     """Display enrichments for an artifact
 
diff --git a/gto/config.py b/gto/config.py
index 1eaa4b18..3dfd841d 100644
--- a/gto/config.py
+++ b/gto/config.py
@@ -8,13 +8,14 @@
 from pydantic.env_settings import InitSettingsSource
 from ruamel.yaml import YAML
 
+from gto.constants import name_regexp
 from gto.exceptions import (
     UnknownStage,
     UnknownType,
     ValidationError,
     WrongConfig,
 )
-from gto.ext import Enrichment, find_enrichment_types, find_enrichments
+from gto.ext import EnrichmentReader, find_enrichment_types, find_enrichments
 
 yaml = YAML(typ="safe", pure=True)
 yaml.default_flow_style = False
@@ -23,7 +24,7 @@
 
 
 def check_name_is_valid(name):
-    return bool(re.match(r"[a-z][a-z0-9-/]*[a-z0-9]$", name))
+    return bool(re.search(name_regexp, name))
 
 
 def assert_name_is_valid(name):
@@ -38,7 +39,7 @@ class EnrichmentConfig(BaseModel):
     type: str
     config: Dict = {}
 
-    def load(self) -> Enrichment:
+    def load(self) -> EnrichmentReader:
         return find_enrichment_types()[self.type](**self.config)
 
 
@@ -67,7 +68,7 @@ def assert_stage(self, name):
             raise UnknownStage(name, self.STAGES)
 
     @property
-    def enrichments(self) -> Dict[str, Enrichment]:
+    def enrichments(self) -> Dict[str, EnrichmentReader]:
         res = {e.source: e for e in (e.load() for e in self.ENRICHMENTS)}
         if self.AUTOLOAD_ENRICHMENTS:
             return {**find_enrichments(), **res}
diff --git a/gto/constants.py b/gto/constants.py
index 0182814e..0ea51d99 100644
--- a/gto/constants.py
+++ b/gto/constants.py
@@ -1,31 +1,42 @@
+import re
 from enum import Enum
 
+COMMIT = "commit"
 REF = "ref"
-# COMMIT = "commit"
 TAG = "tag"
-# BRANCH = "branch"
-# FILE = "file"
 
+ARTIFACT = "artifact"
 ACTION = "action"
 TYPE = "type"
 NAME = "name"
 PATH = "path"
 VERSION = "version"
 STAGE = "stage"
-NUMBER = "number"
+COUNTER = "counter"
 
 
 class Action(Enum):
+    CREATE = "create"
+    DEPRECATE = "deprecate"
+    DEREGISTER = "deregister"
     REGISTER = "register"
     ASSIGN = "assign"
+    UNASSIGN = "unassign"
 
 
-class Event(Enum):
-    COMMIT = "commit"
-    REGISTRATION = "registration"
-    ASSIGNMENT = "assignment"
+name = "[a-z][a-z0-9-/]*[a-z0-9]"
+semver = r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"
+counter = "?P<counter>[0-9]+"
+name_regexp = re.compile(f"^{name}$")
+tag_regexp = re.compile(
+    f"^(?P<artifact>{name})(((#(?P<stage>{name})|@(?P<version>v{semver}))(?P<cancel>!?))|@((?P<deprecated>deprecated)|(?P<created>created)))(#({counter}))?$"
+)
 
 
 class VersionSort(Enum):
     SemVer = "semver"
     Timestamp = "timestamp"
+
+
+ASSIGNMENTS_PER_VERSION = -1
+VERSIONS_PER_STAGE = 1
diff --git a/gto/exceptions.py b/gto/exceptions.py
index ad8e05d9..6be72467 100644
--- a/gto/exceptions.py
+++ b/gto/exceptions.py
@@ -107,7 +107,7 @@ def __init__(self, stage, stages) -> None:
         super().__init__(self.message)
 
 
-class NoActivePromotion(GTOException):
+class NoActiveAssignment(GTOException):
     _message = "No version in stage '{stage}' was found for '{name}'"
 
     def __init__(self, stage, name) -> None:
@@ -115,6 +115,16 @@ def __init__(self, stage, name) -> None:
         super().__init__(self.message)
 
 
+class NoStageForVersion(GTOException):
+    _message = "The artifact '{artifact}' version '{version}' is not in stage '{stage}'"
+
+    def __init__(self, artifact, version, stage) -> None:
+        self.message = self._message.format(
+            artifact=artifact, version=version, stage=stage
+        )
+        super().__init__(self.message)
+
+
 class RefNotFound(GTOException):
     _message = "Ref '{ref}' was not found in the repository history"
 
@@ -123,6 +133,10 @@ def __init__(self, ref) -> None:
         super().__init__(self.message)
 
 
+class AmbiguousArg(GTOException):
+    pass
+
+
 class InvalidVersion(GTOException):
     pass
 
@@ -171,6 +185,14 @@ def __init__(self, name) -> None:
         super().__init__(self.message)
 
 
+class TagNotFound(GTOException):
+    message = "tag '{name}' is not found"
+
+    def __init__(self, name) -> None:
+        self.message = self.message.format(name=name)
+        super().__init__(self.message)
+
+
 class ValidationError(GTOException):
     pass
 
diff --git a/gto/ext.py b/gto/ext.py
index 60106c1c..9442841e 100644
--- a/gto/ext.py
+++ b/gto/ext.py
@@ -6,7 +6,7 @@
 import entrypoints
 from pydantic import BaseModel
 
-ENRICHMENT_ENRTYPOINT = "gto.enrichment"
+ENRICHMENT_ENTRYPOINT = "gto.enrichment"
 
 
 def import_string(path):
@@ -34,7 +34,7 @@ def get_human_readable(self) -> str:
         raise NotImplementedError
 
 
-class Enrichment(BaseModel, ABC):
+class EnrichmentReader(BaseModel, ABC):
     source: str
 
     @abstractmethod
@@ -55,7 +55,7 @@ def describe(
 #         return self.repr
 
 
-# class CLIEnrichment(Enrichment):
+# class CLIEnrichment(EnrichmentReader):
 #     cmd: str
 #     info_type: Union[str, Type[EnrichmentInfo]] = CLIEnrichmentInfo
 
@@ -88,28 +88,28 @@ def describe(
 
 @lru_cache()
 def _find_enrichments():
-    eps = entrypoints.get_group_named(ENRICHMENT_ENRTYPOINT)
+    eps = entrypoints.get_group_named(ENRICHMENT_ENTRYPOINT)
     return {k: ep.load() for k, ep in eps.items()}
 
 
 @lru_cache()
-def find_enrichments() -> Dict[str, Enrichment]:
+def find_enrichments() -> Dict[str, EnrichmentReader]:
     enrichments = _find_enrichments()
     res = {}
     for name, e in enrichments.items():
         # if isinstance(e, type) and issubclass(e, Enrichment) and not e.__fields_set__:
-        if isinstance(e, type) and issubclass(e, Enrichment):
+        if isinstance(e, type) and issubclass(e, EnrichmentReader):
             res[name] = e()
-        if isinstance(e, Enrichment):
+        if isinstance(e, EnrichmentReader):
             res[name] = e
     return res
 
 
 @lru_cache()
-def find_enrichment_types() -> Dict[str, Type[Enrichment]]:
+def find_enrichment_types() -> Dict[str, Type[EnrichmentReader]]:
     enrichments = _find_enrichments()
     return {
         k: e
         for k, e in enrichments.items()
-        if isinstance(e, type) and issubclass(e, Enrichment)
+        if isinstance(e, type) and issubclass(e, EnrichmentReader)
     }
diff --git a/gto/ext_dvc.py b/gto/ext_dvc.py
index db6eb919..b2b1c99b 100644
--- a/gto/ext_dvc.py
+++ b/gto/ext_dvc.py
@@ -3,7 +3,7 @@
 # from pydantic import BaseModel
 # from ruamel.yaml import safe_load
 
-# from gto.ext import Enrichment, EnrichmentInfo
+# from gto.ext import EnrichmentReader, EnrichmentInfo
 
 
 # class DVCEnrichmentInfo(EnrichmentInfo):
@@ -18,7 +18,7 @@
 #         return f"""DVC-tracked [{self.size} bytes]"""
 
 
-# class DVCEnrichment(Enrichment):
+# class DVCEnrichment(EnrichmentReader):
 #     def describe(self, obj: str) -> Optional[DVCEnrichmentInfo]:
 #         try:
 #             with open(obj + ".dvc", encoding="utf8") as f:
diff --git a/gto/ext_mlem.py b/gto/ext_mlem.py
index d946c2a7..442560e8 100644
--- a/gto/ext_mlem.py
+++ b/gto/ext_mlem.py
@@ -7,7 +7,7 @@
 # from mlem.core.objects import DatasetMeta, MlemMeta, ModelMeta
 # from pydantic import BaseModel
 
-# from gto.ext import Enrichment, EnrichmentInfo
+# from gto.ext import EnrichmentReader, EnrichmentInfo
 
 
 # class MlemInfo(EnrichmentInfo):
@@ -27,7 +27,7 @@
 #         return description
 
 
-# class MlemEnrichment(Enrichment):
+# class MlemEnrichment(EnrichmentReader):
 #     source = "mlem"
 
 #     def describe(self, repo, obj: str, rev: Optional[str]) -> Optional[MlemInfo]:
diff --git a/gto/index.py b/gto/index.py
index 3152c4cc..8e10cf0b 100644
--- a/gto/index.py
+++ b/gto/index.py
@@ -9,7 +9,8 @@
 import git
 from pydantic import BaseModel, parse_obj_as, validator
 
-from gto.base import BaseManager, BaseRegistryState, BaseVersion
+from gto.base import BaseManager, BaseRegistryState
+from gto.base import Commit as EnrichmentEvent
 from gto.config import (
     CONFIG_FILE_NAME,
     RegistryConfig,
@@ -26,7 +27,7 @@
     PathIsUsed,
     WrongArgs,
 )
-from gto.ext import Enrichment, EnrichmentInfo
+from gto.ext import EnrichmentInfo, EnrichmentReader
 from gto.utils import resolve_ref
 
 
@@ -182,7 +183,7 @@ def get_history(self) -> Dict[str, Index]:
         raise NotImplementedError
 
     def add(self, name, type, path, must_exist, labels, description, update):
-        for arg in [name] + (labels or []):
+        for arg in [name] + list(labels or []):
             assert_name_is_valid(arg)
         if type:
             self.config.assert_type(type)
@@ -355,13 +356,25 @@ def update_state(
         # processing registered artifacts and versions first
         for artifact in state.get_artifacts().values():
             for version in artifact.versions:
+                commit = self.repo.commit(version.commit_hexsha)
                 enrichments = self.describe(
-                    artifact.name,
+                    artifact.artifact,
                     # faster to make git.Reference here
-                    rev=self.repo.commit(version.commit_hexsha),
+                    rev=commit,
                 )
-                artifact.update_enrichments(
-                    version=version.name, enrichments=enrichments
+                version.add_event(
+                    EnrichmentEvent(
+                        artifact=artifact.artifact,
+                        version=version.version,
+                        created_at=datetime.fromtimestamp(commit.committed_date),
+                        author=commit.author.name,
+                        author_email=commit.author.email,
+                        commit_hexsha=commit.hexsha,
+                        message=commit.message,
+                        committer=commit.committer.name,
+                        committer_email=commit.committer.email,
+                        enrichments=enrichments,
+                    )
                 )
                 state.update_artifact(artifact)
         for commit in self.get_commits(
@@ -370,31 +383,26 @@ def update_state(
             for art_name in GTOEnrichment().discover(self.repo, commit):
                 enrichments = self.describe(art_name, rev=commit)
                 artifact = state.find_artifact(art_name, create_new=True)
-                version = artifact.find_version(commit_hexsha=commit.hexsha)
-                # TODO: duplicated in tag.py
-                if version:
-                    version = version.name
-                else:
-                    artifact.add_version(
-                        BaseVersion(
-                            artifact=art_name,
-                            name=commit.hexsha,
-                            created_at=datetime.fromtimestamp(commit.committed_date),
-                            author=commit.author.name,
-                            author_email=commit.author.email,
-                            commit_hexsha=commit.hexsha,
-                            discovered=True,
-                        )
+                version = artifact.find_version(
+                    commit_hexsha=commit.hexsha, create_new=True
+                )
+                version.add_event(
+                    EnrichmentEvent(
+                        artifact=artifact.artifact,
+                        version=version.version,
+                        created_at=datetime.fromtimestamp(commit.committed_date),
+                        author=commit.author.name,
+                        author_email=commit.author.email,
+                        commit_hexsha=commit.hexsha,
+                        message=commit.message,
+                        committer=commit.committer.name,
+                        committer_email=commit.committer.email,
+                        enrichments=enrichments,
                     )
-                    version = commit.hexsha
-                artifact.update_enrichments(version=version, enrichments=enrichments)
+                )
                 state.update_artifact(artifact)
         return state
 
-    def check_ref(self, ref: str, state: BaseRegistryState):
-        # TODO: implement
-        raise NotImplementedError()
-
 
 def init_index_manager(path):
     try:
@@ -417,7 +425,7 @@ def get_path(self):
         return self.artifact.path
 
 
-class GTOEnrichment(Enrichment):
+class GTOEnrichment(EnrichmentReader):
     source = "gto"
 
     def discover(  # pylint: disable=no-self-use
diff --git a/gto/registry.py b/gto/registry.py
index 5a94f0c6..8159ed69 100644
--- a/gto/registry.py
+++ b/gto/registry.py
@@ -1,31 +1,52 @@
+import logging
 import os
-from typing import Optional, Union
+from typing import Optional, TypeVar, Union
 
 import git
+from funcy import distinct
 from git import InvalidGitRepositoryError, NoSuchPathError, Repo
 from pydantic import BaseModel
 
-from gto.base import BaseAssignment, BaseRegistryState
+from gto.base import (
+    Assignment,
+    BaseEvent,
+    BaseRegistryState,
+    Deprecation,
+    Deregistration,
+    Registration,
+    Unassignment,
+)
 from gto.config import (
     CONFIG_FILE_NAME,
     RegistryConfig,
     assert_name_is_valid,
     read_registry_config,
 )
+from gto.constants import NAME
 from gto.exceptions import (
     NoRepo,
+    NotImplementedInGTO,
     VersionAlreadyRegistered,
     VersionExistsForCommit,
     WrongArgs,
 )
 from gto.index import EnrichmentManager
-from gto.tag import TagStageManager, TagVersionManager
+from gto.tag import (
+    TagArtifactManager,
+    TagStageManager,
+    TagVersionManager,
+    delete_tag,
+    parse_name,
+)
 from gto.ui import echo
 from gto.versions import SemVer
 
+TBaseEvent = TypeVar("TBaseEvent", bound=BaseEvent)
+
 
 class GitRegistry(BaseModel):
     repo: git.Repo
+    artifact_manager: TagArtifactManager
     version_manager: TagVersionManager
     stage_manager: TagStageManager
     enrichment_manager: EnrichmentManager
@@ -49,6 +70,7 @@ def from_repo(cls, repo=Union[str, Repo], config: RegistryConfig = None):
         return cls(
             repo=repo,
             config=config,
+            artifact_manager=TagArtifactManager(repo=repo, config=config),
             version_manager=TagVersionManager(repo=repo, config=config),
             stage_manager=TagStageManager(repo=repo, config=config),
             enrichment_manager=EnrichmentManager(repo=repo, config=config),
@@ -69,6 +91,7 @@ def get_state(
         all_commits=False,
     ) -> BaseRegistryState:
         state = BaseRegistryState()
+        state = self.artifact_manager.update_state(state)
         state = self.version_manager.update_state(state)
         state = self.stage_manager.update_state(state)
         state = self.enrichment_manager.update_state(
@@ -76,7 +99,6 @@ def get_state(
             all_branches=all_branches,
             all_commits=all_commits,
         )
-        state.sort()
         return state
 
     def get_artifacts(
@@ -109,13 +131,15 @@ def register(  # pylint: disable=too-many-locals
         ref,
         version=None,
         message=None,
+        simple=None,
+        force=False,
         bump_major=False,
         bump_minor=False,
         bump_patch=False,
         stdout=False,
         author: Optional[str] = None,
         author_email: Optional[str] = None,
-    ):
+    ) -> Registration:
         """Register artifact version"""
         assert_name_is_valid(name)
         version_args = sum(
@@ -124,27 +148,27 @@ def register(  # pylint: disable=too-many-locals
         if version_args > 1:
             raise WrongArgs("Need to specify either version or single bump argument")
         ref = self.repo.commit(ref).hexsha
-        # TODO: add the same check for other actions, to assign and etc
-        # also we need to check integrity of the index+state
         found_artifact = self.find_artifact(name, create_new=True)
         # check that this commit don't have a version already
         found_version = found_artifact.find_version(commit_hexsha=ref)
-        if found_version is not None and found_version.is_registered:
-            raise VersionExistsForCommit(name, found_version.name)
+        if found_version is not None:
+            if not force and found_version.is_registered:
+                raise VersionExistsForCommit(name, found_version.version)
+            if force and found_version.version != version:
+                raise WrongArgs(
+                    f"For this REF you can only register {found_version.version}"
+                )
         # if version name is provided, use it
         if version:
+            SemVer(version)
             if found_artifact.find_version(name=version) is not None:
                 raise VersionAlreadyRegistered(version)
-            if not SemVer.is_valid(version):
-                raise WrongArgs(
-                    f"Version '{version}' is not valid. Example of valid version: 'v1.0.0'"
-                )
         else:
             # if version name wasn't provided but there were some, bump the last one
             last_version = found_artifact.get_latest_version(registered_only=True)
             if last_version:
                 version = (
-                    SemVer(last_version.name)
+                    SemVer(last_version.version)
                     .bump(
                         bump_major=bump_major,
                         bump_minor=bump_minor,
@@ -158,22 +182,80 @@ def register(  # pylint: disable=too-many-locals
                 )
             else:
                 version = SemVer.get_minimal().version
-        self.version_manager.register(
+        tag = self.version_manager.register(
             name,
             version,
             ref,
             message=message or f"Registering artifact {name} version {version}",
+            simple=simple,
             author=author,
             author_email=author_email,
         )
-        registered_version = self.find_artifact(name).find_version(
-            name=version, raise_if_not_found=True
-        )
         if stdout:
-            echo(
-                f"Created git tag '{registered_version.tag}' that registers a new version"
+            echo(f"Created git tag '{tag}' that registers version")
+            self._echo_git_suggestion(tag)
+        return self._return_event(tag)
+
+    def unregister(  # pylint: disable=too-many-locals
+        self,
+        name,
+        ref=None,
+        version=None,
+        message=None,
+        simple=None,
+        force=False,
+        delete=False,
+        stdout=False,
+        author: Optional[str] = None,
+        author_email: Optional[str] = None,
+    ) -> Deregistration:
+        """Deregister artifact version"""
+        self._check_args(name, version, ref)
+        if ref is not None:
+            ref = self.repo.commit(ref).hexsha
+        found_artifact = self.find_artifact(name, create_new=True)
+        if not force:
+            found_version = found_artifact.find_version(
+                name=version, commit_hexsha=ref, raise_if_not_found=True
+            )
+            if not found_version.is_registered:
+                raise WrongArgs(
+                    f"The version at ref '{found_version.commit_hexsha}' is not registered"
+                )
+            if not found_version.is_active:
+                raise WrongArgs(
+                    f"The version at ref '{found_version.commit_hexsha}' was deregistered already"
+                )
+
+        found_version = found_artifact.find_version(name=version, commit_hexsha=ref)
+        version = version or getattr(found_version, "version", None)
+        commit_hexsha = ref or getattr(found_version, "commit_hexsha", None)
+        if not (version and commit_hexsha):
+            raise WrongArgs("Can't deregister a version that have no git tags")
+
+        if delete:
+            tags = distinct(
+                [
+                    e.tag
+                    for e in found_version.get_events(ascending=True)
+                    if hasattr(e, "tag")
+                ]
             )
-        return registered_version
+            return self._delete_tags(tags, stdout=stdout)
+
+        tag = self.version_manager.deregister(
+            name,
+            version,
+            found_version.commit_hexsha,
+            message=message or f"Deregistering artifact {name} version {version}",
+            simple=simple,
+            author=author,
+            author_email=author_email,
+        )
+        if stdout:
+            echo(f"Created git tag '{tag}' that deregisters version")
+            self._echo_git_suggestion(tag)
+        return self._return_event(tag)
 
     def assign(  # pylint: disable=too-many-locals
         self,
@@ -189,12 +271,11 @@ def assign(  # pylint: disable=too-many-locals
         stdout=False,
         author: Optional[str] = None,
         author_email: Optional[str] = None,
-    ) -> BaseAssignment:
+    ) -> Assignment:
         """Assign stage to specific artifact version"""
-        assert_name_is_valid(name)
-        self.config.assert_stage(stage)
-        if not (version is None) ^ (ref is None):
-            raise WrongArgs("One and only one of (version, ref) must be specified.")
+        self._check_args(name, version, ref, stage)
+        if name_version:
+            self._check_version(name_version)
         if name_version and skip_registration:
             raise WrongArgs(
                 "You either need to supply version name or skip registration"
@@ -214,17 +295,19 @@ def assign(  # pylint: disable=too-many-locals
             if found_version:
                 if name_version:
                     raise WrongArgs(
-                        f"Can't register '{SemVer(name_version).version}', since '{found_version.name}' is registered already at this ref"
+                        f"Can't register '{SemVer(name_version).version}', since '{found_version.version}' is registered already at this ref"
                     )
             elif not skip_registration:
                 self.register(name, version=name_version, ref=ref, stdout=stdout)
         if (
             not force
             and found_version
-            and found_version.assignments
-            and any(a.stage == stage for a in found_version.assignments)
+            and found_version.stages
+            and any(vstage.stage == stage for vstage in found_version.get_vstages())
         ):
-            raise WrongArgs(f"Version is already in stage '{stage}'")
+            raise WrongArgs(
+                f"Version '{found_version.version}' is already in stage '{stage}'"
+            )
         # TODO: getting tag name as a result and using it
         # is leaking implementation details in base module
         # it's roughly ok to have until we add other implementations
@@ -234,42 +317,203 @@ def assign(  # pylint: disable=too-many-locals
             stage,
             ref=ref,
             message=message
-            or f"Assigning stage {stage} to artifact {name} version {version}",
+            or f"Assigning stage {stage} to artifact {name} "
+            + (
+                f"version {found_version.version}" if found_version else f"commit {ref}"
+            ),
             simple=simple,
             author=author,
             author_email=author_email,
         )
-        assignment = self.stage_manager.check_ref(tag, self.get_state())[name]
         if stdout:
             echo(
-                f"Created git tag '{assignment.tag}' that assigns '{stage}' to '{assignment.version}'"
+                f"Created git tag '{tag}' that assigns stage to version '{found_version.version}'"
             )
-        return assignment
+            self._echo_git_suggestion(tag)
+        return self._return_event(tag)
 
-    def _check_ref(self, ref, state):
-        if ref.startswith("refs/tags/"):
-            ref = ref[len("refs/tags/") :]
-        if ref.startswith("refs/heads/"):
+    def unassign(  # pylint: disable=too-many-locals
+        self,
+        name,
+        stage,
+        version=None,
+        ref=None,
+        message=None,
+        simple=False,
+        force=False,
+        delete=False,
+        stdout=False,
+        author: Optional[str] = None,
+        author_email: Optional[str] = None,
+    ) -> Unassignment:
+        """Unassign stage to specific artifact version"""
+        self._check_args(name, version, ref, stage)
+        if ref:
             ref = self.repo.commit(ref).hexsha
-        return {
-            "version": self.version_manager.check_ref(ref, state),
-            "stage": self.stage_manager.check_ref(ref, state),
-        }
+        found_artifact = self.find_artifact(name)
+        found_version = found_artifact.find_version(
+            name=version, commit_hexsha=ref, raise_if_not_found=True
+        )
+        if not force and all(s != stage for s in getattr(found_version, "stages", [])):
+            raise WrongArgs(
+                f"Stage '{stage}' is not assigned to a version '{found_version.version}'"
+            )
+        if delete:
+            tags = distinct(
+                [
+                    e.tag
+                    for e in found_version.get_events(ascending=True)
+                    if hasattr(e, "tag") and hasattr(e, "stage") and e.stage == stage
+                ]
+            )
+            return self._delete_tags(tags, stdout=stdout)
+
+        # TODO: getting tag name as a result and using it
+        # is leaking implementation details in base module
+        # it's roughly ok to have until we add other implementations
+        # beside tag-based assignments
+        tag = self.stage_manager.unassign(  # type: ignore
+            name,
+            stage,
+            ref=found_version.commit_hexsha,
+            message=message
+            or f"Unassigning stage '{stage}' to artifact '{name}' version '{found_version.version}'",
+            simple=simple,
+            author=author,
+            author_email=author_email,
+        )
+        if stdout:
+            echo(
+                f"Created git tag '{tag}' that unassigns stage from version '{found_version.version}'"
+            )
+            self._echo_git_suggestion(tag)
+        return self._return_event(tag)
+
+    def deprecate(
+        self,
+        name,
+        ref=None,
+        message=None,
+        simple=False,
+        force=False,
+        delete=False,
+        stdout=False,
+        author: Optional[str] = None,
+        author_email: Optional[str] = None,
+    ) -> Optional[Deprecation]:
+        if not force:
+            found_artifact = self.find_artifact(name)
+            if not found_artifact.is_active:
+                raise WrongArgs("Artifact was deprecated already")
+        if delete:
+            tags = distinct(
+                [
+                    e.tag
+                    for e in self.find_artifact(name=name).get_events(ascending=True)
+                    if hasattr(e, "tag")
+                ]
+            )
+            return self._delete_tags(tags, stdout=stdout)
+        if ref is None:
+            if name in self.get_artifacts():
+                ref = self.find_artifact(name=name).get_events()[0].commit_hexsha
+            else:
+                ref = "HEAD"
+        tag = self.artifact_manager.deprecate(  # type: ignore
+            name,
+            ref=ref,
+            message=message or f"Deprecating artifact '{name}'",
+            simple=simple,
+            author=author,
+            author_email=author_email,
+        )
+        if stdout:
+            echo(f"Created git tag '{tag}' that deprecates artifact")
+            self._echo_git_suggestion(tag)
+        return self._return_event(tag)
+
+    def _check_args(self, name, version, ref, stage=None):
+        assert_name_is_valid(name)
+        if stage is not None:
+            self.config.assert_stage(stage)
+        if version:
+            self._check_version(version)
+        if not (version is None) ^ (ref is None):
+            raise WrongArgs("One and only one of (version, ref) must be specified.")
+
+    @staticmethod
+    def _check_version(version):
+        if not SemVer.is_valid(version):
+            raise WrongArgs(
+                f"Version '{version}' is not valid. Example of valid version: 'v1.0.0'"
+            )
+
+    def _return_event(self, tag) -> TBaseEvent:
+        event = self.check_ref(tag)
+        if len(event) > 1:
+            raise NotImplementedInGTO("Can't process a tag that caused multiple events")
+        event = event[0]
+        return event
+
+    @staticmethod
+    def _echo_git_suggestion(tag):
+        echo("To push the changes upstream, run:")
+        echo(f"    git push {tag}")
+
+    def _delete_tags(self, tags, stdout):
+        tags = list(tags)
+        for tag in tags:
+            delete_tag(self.repo, tag)
+            if stdout:
+                echo(f"Deleted git tag '{tag}'")
+        if stdout:
+            echo("To push the changes upstream, run:")
+            echo(f"    git push {' '.join(tags)} --delete".replace("!", "/!"))
 
     def check_ref(self, ref: str):
         "Find out what was registered/assigned in this ref"
+        try:
+            if ref.startswith("refs/tags/"):
+                ref = ref[len("refs/tags/") :]
+            if ref.startswith("refs/heads/"):
+                ref = self.repo.commit(ref).hexsha
+            # check the ref exists
+            _ = self.repo.tags[ref]
+            # check the ref follows the GTO format
+            name = parse_name(ref)[NAME]
+        except (KeyError, ValueError, IndexError):
+            logging.info("Provided ref doesn't exist or it is not of GTO format")
+            return []
         state = self.get_state()
-        return self._check_ref(ref, state)
+        return [
+            event
+            for aname, artifact in state.get_artifacts().items()
+            if aname == name
+            for event in artifact.get_events()
+            # TODO: support matching the shortened commit hashes
+            if event.ref == ref
+        ]
 
     def find_commit(self, name, version):
         return self.get_state().find_commit(name, version)
 
     def which(
-        self, name, stage, raise_if_not_found=True, all=False, registered_only=False
+        self,
+        name,
+        stage,
+        raise_if_not_found=True,
+        assignments_per_version=None,
+        versions_per_stage=None,
+        registered_only=False,
     ):
         """Return stage active in specific stage"""
         return self.get_state().which(
-            name, stage, raise_if_not_found, all=all, registered_only=registered_only
+            name,
+            stage,
+            raise_if_not_found,
+            assignments_per_version=assignments_per_version,
+            versions_per_stage=versions_per_stage,
+            registered_only=registered_only,
         )
 
     def latest(self, name: str, all: bool = False, registered: bool = True):
diff --git a/gto/tag.py b/gto/tag.py
index b4ecf69a..1e9dc3a6 100644
--- a/gto/tag.py
+++ b/gto/tag.py
@@ -1,3 +1,4 @@
+import re
 from datetime import datetime
 from enum import Enum
 from typing import FrozenSet, Iterable, Optional, Union
@@ -5,119 +6,99 @@
 import git
 from pydantic import BaseModel
 
-from gto.config import assert_name_is_valid, check_name_is_valid
-from gto.versions import SemVer
-
 from .base import (
-    BaseArtifact,
-    BaseAssignment,
+    Artifact,
+    Assignment,
     BaseManager,
     BaseRegistryState,
-    BaseVersion,
+    Deprecation,
+    Deregistration,
+    Registration,
+    Unassignment,
+)
+from .constants import (
+    ACTION,
+    COUNTER,
+    NAME,
+    STAGE,
+    TAG,
+    VERSION,
+    Action,
+    tag_regexp,
 )
-from .constants import ACTION, NAME, NUMBER, STAGE, TAG, VERSION, Action
 from .exceptions import (
     InvalidTagName,
-    InvalidVersion,
     MissingArg,
+    NotImplementedInGTO,
     RefNotFound,
     TagExists,
+    TagNotFound,
     UnknownAction,
 )
 
-ActionSign = {
-    Action.REGISTER: "@",
-    Action.ASSIGN: "#",
+COUNT_DELIMITER = "#"
+
+TagTemplates = {
+    # Action.CREATE: "{artifact}@",
+    Action.DEPRECATE: "{artifact}@deprecated",
+    Action.REGISTER: "{artifact}@{version}",
+    Action.DEREGISTER: "{artifact}@{version}!",
+    Action.ASSIGN: "{artifact}#{stage}",
+    Action.UNASSIGN: "{artifact}#{stage}!",
 }
 
 
 def name_tag(
     action: Action,
-    name: str,
+    artifact: str,
     version: Optional[str] = None,
     stage: Optional[str] = None,
     repo: Optional[git.Repo] = None,
     simple: bool = False,
 ):
-    if action == Action.REGISTER:
-        return f"{name}{ActionSign[action]}{version}"
-
-    if action == Action.ASSIGN:
-        if simple:
-            return f"{name}{ActionSign[action]}{stage}"
-        if repo is None:
-            raise MissingArg(arg="repo")
-        numbers = []
-        for tag in repo.tags:
-            parsed = parse_name(tag.name, raise_on_fail=False)
-            if (
-                parsed
-                and (parsed[NAME] == name)
-                and (parsed[ACTION] == Action.ASSIGN)
-                and (NUMBER in parsed)
-            ):
-                numbers.append(parsed[NUMBER])
-        new_number = max(numbers) + 1 if numbers else 1
-        return f"{name}{ActionSign[action]}{stage}{ActionSign[action]}{new_number}"
-    raise UnknownAction(action=action)
-
-
-def _parse_register(name: str, raise_on_fail: bool = True):
-    sign = len(name) - name[::-1].index(ActionSign[Action.REGISTER])
-    name, version = name[: sign - 1], name[sign:]
-    if check_name_is_valid(name) and SemVer.is_valid(version):
-        return {
-            ACTION: Action.REGISTER,
-            NAME: name,
-            VERSION: version,
-        }
-    if raise_on_fail:
-        assert_name_is_valid(name)
-        if not SemVer.is_valid(version):
-            raise InvalidVersion(
-                f"Version format should be v1.0.0, cannot parse {version}"
-            )
-    return {}
+    if action not in TagTemplates:
+        raise UnknownAction(action=action)
 
-
-def _parse_assign(name: str, raise_on_fail: bool = True):
-    parsed = name.split(ActionSign[Action.ASSIGN])
-    if (
-        check_name_is_valid(parsed[0])
-        and check_name_is_valid(parsed[1])
-        and (parsed[2].isdigit() if len(parsed) == 3 else True)
-    ):
-        if len(parsed) == 2:
-            return {
-                ACTION: Action.ASSIGN,
-                NAME: parsed[0],
-                STAGE: parsed[1],
-            }
-        if len(parsed) == 3:
-            return {
-                ACTION: Action.ASSIGN,
-                NAME: parsed[0],
-                STAGE: parsed[1],
-                NUMBER: int(parsed[2]),
-            }
-    if raise_on_fail:
-        assert_name_is_valid(parsed[0])
-        assert_name_is_valid(parsed[1])
-        if not parsed[2].isdigit():
-            raise InvalidTagName(name)
-    return {}
+    tag = TagTemplates[action].format(artifact=artifact, version=version, stage=stage)
+    if simple:
+        return tag
+    if repo is None:
+        raise MissingArg(arg="repo")
+    counter = 0
+    for t in repo.tags:
+        parsed = parse_name(t.name, raise_on_fail=False)  # type: ignore
+        if (
+            parsed
+            and parsed[NAME] == artifact
+            and COUNTER in parsed
+            and parsed[COUNTER] > counter
+        ):
+            counter = parsed[COUNTER]
+    return f"{tag}{COUNT_DELIMITER}{counter+1}"
 
 
 def parse_name(name: str, raise_on_fail: bool = True):
 
-    if ActionSign[Action.REGISTER] in name:
-        return _parse_register(name, raise_on_fail=raise_on_fail)
-
-    if ActionSign[Action.ASSIGN] in name:
-        return _parse_assign(name, raise_on_fail=raise_on_fail)
-
-    if raise_on_fail:
+    match = re.search(tag_regexp, name)
+    if raise_on_fail and not match:
         raise InvalidTagName(name)
+    if match:
+        parsed = {NAME: match["artifact"]}
+        if match["deprecated"]:
+            parsed[ACTION] = Action.DEPRECATE
+        if match[VERSION]:
+            parsed[VERSION] = match[VERSION]
+            parsed[ACTION] = (
+                Action.DEREGISTER if match["cancel"] == "!" else Action.REGISTER
+            )
+        if match[STAGE]:
+            parsed[STAGE] = match[STAGE]
+            parsed[ACTION] = (
+                Action.UNASSIGN if match["cancel"] == "!" else Action.ASSIGN
+            )
+        if match[COUNTER]:
+            parsed[COUNTER] = int(match[COUNTER])
+        return parsed
     return {}
 
 
@@ -167,11 +148,7 @@ def find(
     if tags is None:
         if repo is None:
             raise MissingArg(arg="repo")
-        tags = [
-            t
-            for t in repo.tags
-            if parse_name(t.name, raise_on_fail=False) and t.tag is not None
-        ]
+        tags = [t for t in repo.tags if parse_name(t.name, raise_on_fail=False)]
     if action:
         tags = [t for t in tags if parse_name(t.name)[ACTION] in action]
     if name:
@@ -180,6 +157,8 @@ def find(
         tags = [t for t in tags if parse_name(t.name).get(VERSION) == version]
     if stage:
         tags = [t for t in tags if parse_name(t.name).get(STAGE) == stage]
+    # remove lightweight tags - better to do later so the function is faster
+    tags = [t for t in tags if t.tag is not None]
     if sort == "by_time":
         tags = sorted(tags, key=lambda t: t.tag.tagged_date)
     else:
@@ -211,66 +190,77 @@ def create_tag(
     repo.git.tag(["-a", name, "-m", message, ref], env=env)
 
 
-def version_from_tag(tag: git.Tag) -> BaseVersion:
-    mtag = parse_tag(tag)
-    return BaseVersion(
-        artifact=mtag.name,
-        name=mtag.version,
-        created_at=mtag.created_at,
-        author=tag.tag.tagger.name,
-        author_email=tag.tag.tagger.email,
-        message=tag.tag.message,
-        commit_hexsha=tag.commit.hexsha,
-        tag=tag.name,
-    )
+def delete_tag(repo: git.Repo, name: str):
+    try:
+        repo.delete_tag(name)
+    except git.BadName as e:
+        raise TagNotFound(name=name) from e
 
 
-def assignment_from_tag(
-    artifact: BaseArtifact, tag: git.Tag, version_required: bool
-) -> BaseAssignment:
+def index_tag(artifact: Artifact, tag: git.TagReference) -> Artifact:
+    event: Union[Deprecation, Registration, Deregistration, Assignment, Unassignment]
     mtag = parse_tag(tag)
-    if version_required:
+    if mtag.action == Action.REGISTER:
+        event = Registration(
+            artifact=mtag.name,
+            version=mtag.version,
+            created_at=mtag.created_at,
+            author=tag.tag.tagger.name,
+            author_email=tag.tag.tagger.email,
+            message=tag.tag.message,
+            commit_hexsha=tag.commit.hexsha,
+            tag=tag.name,
+        )
+    elif mtag.action == Action.DEREGISTER:
+        event = Deregistration(
+            artifact=mtag.name,
+            version=mtag.version,
+            created_at=mtag.created_at,
+            author=tag.tag.tagger.name,
+            author_email=tag.tag.tagger.email,
+            message=tag.tag.message,
+            commit_hexsha=tag.commit.hexsha,
+            tag=tag.name,
+        )
+    elif mtag.action in (Action.ASSIGN, Action.UNASSIGN):
         version = artifact.find_version(
-            commit_hexsha=tag.commit.hexsha, raise_if_not_found=True
-        ).name  # type: ignore
-    else:
-        version = artifact.find_version(commit_hexsha=tag.commit.hexsha)
-        if version:
-            version = version.name  # type: ignore
+            commit_hexsha=tag.commit.hexsha, create_new=True
+        ).version  # type: ignore
+        if mtag.action == Action.ASSIGN:
+            event = Assignment(
+                artifact=mtag.name,
+                version=version,
+                stage=mtag.stage,
+                created_at=mtag.created_at,
+                author=tag.tag.tagger.name,
+                author_email=tag.tag.tagger.email,
+                message=tag.tag.message,
+                commit_hexsha=tag.commit.hexsha,
+                tag=tag.name,
+            )
         else:
-            artifact.add_version(
-                BaseVersion(
-                    artifact=mtag.name,
-                    name=tag.commit.hexsha,
-                    created_at=mtag.created_at,
-                    author=tag.tag.tagger.name,
-                    author_email=tag.tag.tagger.email,
-                    message=tag.tag.message,
-                    commit_hexsha=tag.commit.hexsha,
-                )
+            event = Unassignment(
+                artifact=mtag.name,
+                version=version,
+                stage=mtag.stage,
+                created_at=mtag.created_at,
+                author=tag.tag.tagger.name,
+                author_email=tag.tag.tagger.email,
+                message=tag.tag.message,
+                commit_hexsha=tag.commit.hexsha,
+                tag=tag.name,
             )
-            version = tag.commit.hexsha
-    return BaseAssignment(
-        artifact=mtag.name,
-        version=version,
-        stage=mtag.stage,
-        created_at=mtag.created_at,
-        author=tag.tag.tagger.name,
-        author_email=tag.tag.tagger.email,
-        message=tag.tag.message,
-        commit_hexsha=tag.commit.hexsha,
-        tag=tag.name,
-    )
-
-
-def index_tag(
-    artifact: BaseArtifact, tag: git.TagReference, version_required: bool
-) -> BaseArtifact:
-    mtag = parse_name(tag.tag.tag)
-    if mtag["action"] == Action.REGISTER:
-        artifact.add_version(version_from_tag(tag))
-    if mtag["action"] == Action.ASSIGN:
-        artifact.add_assignment(assignment_from_tag(artifact, tag, version_required))
+    elif mtag.action == Action.DEPRECATE:
+        event = Deprecation(
+            artifact=mtag.name,
+            created_at=mtag.created_at,
+            author=tag.tag.tagger.name,
+            author_email=tag.tag.tagger.email,
+            message=tag.tag.message,
+            commit_hexsha=tag.commit.hexsha,
+            tag=tag.name,
+        )
+    artifact.add_event(event)
     return artifact
 
 
@@ -285,14 +275,47 @@ def update_state(self, state: BaseRegistryState) -> BaseRegistryState:
                         parse_name(tag.tag.tag)["name"], create_new=True
                     ),
                     tag,
-                    version_required=False,
                 )
             )
         return state
 
 
+class TagArtifactManager(TagManager):
+    actions: FrozenSet[Action] = frozenset((Action.CREATE, Action.DEPRECATE))
+
+    def create(self):  # pylint: disable=no-self-use
+        raise NotImplementedInGTO(
+            "If you want to create artifact, register a version or assign a stage for it"
+        )
+
+    def deprecate(
+        self,
+        name,
+        ref,
+        message,
+        simple,
+        author: Optional[str] = None,
+        author_email: Optional[str] = None,
+    ):
+        tag = name_tag(
+            Action.DEPRECATE,
+            name,
+            repo=self.repo,
+            simple=simple,
+        )
+        create_tag(
+            self.repo,
+            tag,
+            ref=ref,
+            message=message,
+            tagger=author,
+            tagger_email=author_email,
+        )
+        return tag
+
+
 class TagVersionManager(TagManager):
-    actions: FrozenSet[Action] = frozenset((Action.REGISTER,))
+    actions: FrozenSet[Action] = frozenset((Action.REGISTER, Action.DEREGISTER))
 
     def register(
         self,
@@ -300,38 +323,49 @@ def register(
         version,
         ref,
         message,
+        simple,
         author: Optional[str] = None,
         author_email: Optional[str] = None,
     ):
+        tag = name_tag(
+            Action.REGISTER, name, version=version, repo=self.repo, simple=simple
+        )
         create_tag(
             self.repo,
-            name_tag(Action.REGISTER, name, version=version, repo=self.repo),
+            tag,
             ref=ref,
             message=message,
             tagger=author,
             tagger_email=author_email,
         )
+        return tag
 
-    def check_ref(self, ref: str, state: BaseRegistryState):
-        try:
-            _ = self.repo.tags[ref]
-            art_name = parse_name(ref)[NAME]
-            version_name = parse_name(ref)[VERSION]
-        except (KeyError, ValueError, IndexError):
-            # logging.warning(
-            #     "Provided ref doesn't exist or it is not a tag that registers a version"
-            # )
-            return {}
-        return {
-            name: version
-            for name, artifact in state.get_artifacts().items()
-            for version in artifact.versions
-            if name == art_name and version.name == version_name
-        }
+    def deregister(
+        self,
+        name,
+        version,
+        ref,
+        message,
+        simple,
+        author: Optional[str] = None,
+        author_email: Optional[str] = None,
+    ):
+        tag = name_tag(
+            Action.DEREGISTER, name, version=version, repo=self.repo, simple=simple
+        )
+        create_tag(
+            self.repo,
+            tag,
+            ref=ref,
+            message=message,
+            tagger=author,
+            tagger_email=author_email,
+        )
+        return tag
 
 
 class TagStageManager(TagManager):
-    actions: FrozenSet[Action] = frozenset((Action.ASSIGN,))
+    actions: FrozenSet[Action] = frozenset((Action.ASSIGN, Action.UNASSIGN))
 
     def assign(
         self,
@@ -354,16 +388,25 @@ def assign(
         )
         return tag
 
-    def check_ref(self, ref: str, state: BaseRegistryState):
-        try:
-            tag = self.repo.tags[ref]
-            _ = parse_name(ref)[STAGE]
-            art_name = parse_name(ref)[NAME]
-        except (KeyError, ValueError, IndexError):
-            return {}
-        return {
-            name: assignment
-            for name, artifact in state.get_artifacts().items()
-            for assignment in artifact.stages
-            if name == art_name and assignment.tag == tag.name
-        }
+    def unassign(
+        self,
+        name,
+        stage,
+        ref,
+        message,
+        simple,
+        author: Optional[str] = None,
+        author_email: Optional[str] = None,
+    ) -> str:
+        tag = name_tag(
+            Action.UNASSIGN, name, stage=stage, repo=self.repo, simple=simple
+        )
+        create_tag(
+            self.repo,
+            tag,
+            ref=ref,
+            message=message,
+            tagger=author,
+            tagger_email=author_email,
+        )
+        return tag
diff --git a/gto/utils.py b/gto/utils.py
index da808f60..9943cd1c 100644
--- a/gto/utils.py
+++ b/gto/utils.py
@@ -4,7 +4,7 @@
 from copy import deepcopy
 from datetime import datetime
 from enum import Enum
-from typing import Optional
+from typing import Optional, Union
 
 import click
 import git
@@ -12,6 +12,7 @@
 from tabulate import tabulate
 
 from gto.config import yaml
+from gto.exceptions import RefNotFound
 
 
 def flatten(obj):
@@ -73,11 +74,23 @@ def format_echo(result, format, format_table=None, if_empty="", missing_value="-
         if result:
             for line in result:
                 click.echo(line)
+    elif format == "line":
+        if result:
+            click.echo(result)
     else:
         raise NotImplementedError(f"Format {format} is not implemented")
 
 
-def resolve_ref(repo: git.Repo, ref: Optional[str] = None):
+def resolve_ref(
+    repo: Union[git.Repo, str], ref: Optional[str] = None, raise_if_not_found=True
+):
     # this becomes pretty slow if called many times
     # may need optimization if we will
-    return repo.refs[ref].commit if (ref and ref in repo.refs) else repo.commit(ref)
+    if isinstance(repo, str):
+        repo = git.Repo(repo)
+    try:
+        return repo.refs[ref].commit if (ref and ref in repo.refs) else repo.commit(ref)
+    except git.BadName as e:
+        if raise_if_not_found:
+            raise RefNotFound(ref) from e
+        return None


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index e4894ca8..24f1f8c0 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -8,11 +8,10 @@
 import pytest
 
 import gto
-from gto.constants import STAGE, VERSION, Action
 from gto.exceptions import PathIsUsed, WrongArgs
 from gto.tag import find
 from gto.versions import SemVer
-from tests.utils import _check_obj
+from tests.utils import check_obj
 
 
 def test_empty_index(empty_git_repo: Tuple[git.Repo, Callable]):
@@ -43,7 +42,7 @@ def test_add_remove(empty_git_repo: Tuple[git.Repo, Callable]):
         gto.api.annotate(repo.working_dir, "other-name", path=path)
     index = gto.api._get_index(repo.working_dir).get_index()
     assert name in index
-    _check_obj(
+    check_obj(
         index.state[name],
         dict(
             type=type,
@@ -86,12 +85,12 @@ def test_api_info_commands_repo_with_artifact(
     gto.api.history(repo.working_dir)
 
 
-def test_register(repo_with_artifact):
+def test_register_deregister(repo_with_artifact):
     repo, name = repo_with_artifact
     vname1, vname2 = "v1.0.0", "v1.0.1"
     gto.api.register(repo.working_dir, name, "HEAD", vname1)
     latest = gto.api.find_latest_version(repo.working_dir, name)
-    assert latest.name == vname1
+    assert latest.version == vname1
     gto.api.annotate(
         repo.working_dir,
         "something-irrelevant",
@@ -99,7 +98,9 @@ def test_register(repo_with_artifact):
         "anything",
         must_exist=False,
     )
-    repo.index.commit("Irrelevant action to create a git commit")
+    repo.index.commit(
+        "Irrelevant action to create a git commit to register another version"
+    )
     message = "Some message"
     author = "GTO"
     author_email = "gto@iterative.ai"
@@ -112,11 +113,15 @@ def test_register(repo_with_artifact):
         author_email=author_email,
     )
     latest = gto.api.find_latest_version(repo.working_dir, name)
-    assert latest.name == vname2
+    assert latest.version == vname2
     assert latest.message == message
     assert latest.author == author
     assert latest.author_email == author_email
 
+    gto.api.unregister(repo=repo.working_dir, name=name, version=vname2)
+    latest = gto.api.find_latest_version(repo.working_dir, name)
+    assert latest.version == vname1
+
 
 def test_assign(repo_with_artifact: Tuple[git.Repo, str]):
     repo, name = repo_with_artifact
@@ -126,7 +131,7 @@ def test_assign(repo_with_artifact: Tuple[git.Repo, str]):
     message = "some msg"
     author = "GTO"
     author_email = "gto@iterative.ai"
-    gto.api.assign(
+    event = gto.api.assign(
         repo.working_dir,
         name,
         stage,
@@ -136,9 +141,10 @@ def test_assign(repo_with_artifact: Tuple[git.Repo, str]):
         author=author,
         author_email=author_email,
     )
-    assignment = gto.api.find_versions_in_stage(repo.working_dir, name, stage)
-    _check_obj(
-        assignment,
+    assignments = gto.api.find_versions_in_stage(repo.working_dir, name, stage)
+    assert len(assignments) == 1
+    check_obj(
+        assignments[0].dict_state(),
         dict(
             artifact=name,
             version="v0.0.1",
@@ -147,8 +153,10 @@ def test_assign(repo_with_artifact: Tuple[git.Repo, str]):
             author_email=author_email,
             message=message,
             commit_hexsha=repo.commit().hexsha,
+            is_active=True,
+            ref=event.ref,
         ),
-        {"created_at", "assignments", "tag"},
+        {"created_at", "assignments", "unassignments", "tag", "activated_at"},
     )
 
 
@@ -165,8 +173,9 @@ def test_assign_skip_registration(repo_with_artifact: Tuple[git.Repo, str]):
             skip_registration=True,
         )
     gto.api.assign(repo.working_dir, name, stage, ref="HEAD", skip_registration=True)
-    assignment = gto.api.find_versions_in_stage(repo.working_dir, name, stage)
-    assert not SemVer.is_valid(assignment.version)
+    assignments = gto.api.find_versions_in_stage(repo.working_dir, name, stage)
+    assert len(assignments) == 1
+    assert not SemVer.is_valid(assignments[0].version)
 
 
 def test_assign_force_is_needed(repo_with_artifact: Tuple[git.Repo, str]):
@@ -212,20 +221,19 @@ def test_check_ref_detailed(repo_with_artifact: Tuple[git.Repo, Callable]):
     ):
         gto.api.register(repo, name=NAME, ref="HEAD", version=SEMVER)
 
-    info = gto.api.check_ref(repo, f"{NAME}@{SEMVER}")[VERSION][NAME]
-    _check_obj(
-        info,
+    events = gto.api.check_ref(repo, f"{NAME}@{SEMVER}")
+    assert len(events) == 1, "Should return one event"
+    check_obj(
+        events[0].dict_state(),
         {
+            "event": "registration",
             "artifact": NAME,
-            "name": SEMVER,
+            "version": SEMVER,
             "author": GIT_COMMITTER_NAME,
             "author_email": GIT_COMMITTER_EMAIL,
-            "discovered": False,
             "tag": f"{NAME}@{SEMVER}",
-            "assignments": [],
-            "enrichments": [],
         },
-        skip_keys={"commit_hexsha", "created_at", "message"},
+        skip_keys={"commit_hexsha", "created_at", "message", "priority", "addition"},
     )
 
 
@@ -239,15 +247,10 @@ def test_check_ref_multiple_showcase(showcase):
         second_commit,
     ) = showcase
 
-    tags = find(repo=repo, action=Action.REGISTER)
-    for tag in tags:
-        info = list(gto.api.check_ref(repo, tag.name)[VERSION].values())[0]
-        assert info.tag == tag.name
-
-    tags = find(repo=repo, action=Action.ASSIGN)
-    for tag in tags:
-        info = list(gto.api.check_ref(repo, tag.name)[STAGE].values())[0]
-        assert info.tag == tag.name
+    for tag in find(repo=repo):
+        events = gto.api.check_ref(repo, tag.name)
+        assert len(events) == 1, "Should return one event"
+        assert events[0].ref == tag.name
 
 
 def test_check_ref_catch_the_bug(repo_with_artifact: Tuple[git.Repo, Callable]):
@@ -261,8 +264,9 @@ def test_check_ref_catch_the_bug(repo_with_artifact: Tuple[git.Repo, Callable]):
         [assignment1, assignment2, assignment3],
         [f"{NAME}#staging#1", f"{NAME}#prod#2", f"{NAME}#dev#3"],
     ):
-        info = gto.api.check_ref(repo, tag)[STAGE][NAME]
-        assert info.tag == assignment.tag == tag
+        events = gto.api.check_ref(repo, tag)
+        assert len(events) == 1, events
+        assert events[0].ref == assignment.tag == tag
 
 
 def test_is_not_gto_repo(empty_git_repo):
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 3e921f7b..30753053 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -11,7 +11,7 @@
 from gto.api import _get_index
 from gto.cli import app
 
-from .utils import _check_obj
+from .utils import check_obj
 
 
 def _check_successful_cmd(cmd: str, args: list, expected_stdout: Optional[str]):
@@ -118,12 +118,12 @@ def test_commands(showcase):
     )
     _check_successful_cmd(
         "which",
-        ["-r", path, "rf", "production", "--all"],
+        ["-r", path, "rf", "production", "--vs", "-1"],
         "v1.2.4\nv1.2.3\n",
     )
     _check_successful_cmd(
         "which",
-        ["-r", path, "rf", "staging", "--all"],
+        ["-r", path, "rf", "staging", "--vs", "-1"],
         "v1.2.4\n",
     )
     _check_successful_cmd(
@@ -172,23 +172,29 @@ def test_commands(showcase):
     )
     _check_successful_cmd(
         "check-ref",
-        ["-r", path, "rf@v1.2.4", "--assignment"],
-        "",  # since this tag doesn't assign any stage
+        ["-r", path, "rf@v1.2.4", "--event"],
+        "registration\n",
     )
     _check_successful_cmd(
         "check-ref",
-        ["-r", path, "rf@v1.2.4", "--registration"],
+        ["-r", path, "rf@v1.2.4"],
         '✅  Version "v1.2.4" of artifact "rf" was registered\n',
     )
     _check_successful_cmd(
         "check-ref",
-        ["-r", path, "rf#production#3", "--registration"],
-        "",
+        ["-r", path, "rf#production#3", "--event"],
+        "assignment\n",
     )
     _check_successful_cmd(
         "check-ref",
-        ["-r", path, "rf#production#3", "--assignment"],
-        '✅  Stage "production" was assigned to version"v1.2.4" of artifact "rf"\n',
+        ["-r", path, "rf#production#3"],
+        '✅  Stage "production" was assigned to version "v1.2.4" of artifact "rf"\n',
+    )
+    # TODO: make unsuccessful
+    _check_successful_cmd(
+        "check-ref",
+        ["-r", path, "this-tag-does-not-exist"],
+        "",
     )
 
 
@@ -243,7 +249,7 @@ def test_annotate(empty_git_repo: Tuple[git.Repo, Callable]):
     artifact = (
         _get_index(repo.working_dir, file=True).get_index().state[name]
     )  # pylint: disable=protected-access
-    _check_obj(
+    check_obj(
         artifact,
         dict(
             type="new-type",
@@ -274,32 +280,44 @@ def test_register(repo_with_commit: Tuple[git.Repo, Callable]):
     _check_successful_cmd(
         "register",
         ["-r", repo.working_dir, "a1"],
-        "Created git tag 'a1@v0.0.1' that registers a new version\n",
+        "Created git tag 'a1@v0.0.1' that registers version\n"
+        "To push the changes upstream, run:\n"
+        "    git push a1@v0.0.1\n",
     )
 
     _check_successful_cmd(
         "register",
         ["-r", repo.working_dir, "a2", "--version", "v1.2.3"],
-        "Created git tag 'a2@v1.2.3' that registers a new version\n",
+        "Created git tag 'a2@v1.2.3' that registers version\n"
+        "To push the changes upstream, run:\n"
+        "    git push a2@v1.2.3\n",
     )
 
     _check_failing_cmd(
         "register",
         ["-r", repo.working_dir, "a3", "--version", "1.2.3"],
-        "❌ Version '1.2.3' is not valid. Example of valid version: 'v1.0.0'\n",
+        "❌ Supplied version '1.2.3' cannot be parsed\n",
     )
 
 
 def test_assign(repo_with_commit: Tuple[git.Repo, Callable]):
     repo, write_file = repo_with_commit
 
+    _check_successful_cmd(
+        "register",
+        ["-r", repo.working_dir, "nn1"],
+        "Created git tag 'nn1@v0.0.1' that registers version\n"
+        "To push the changes upstream, run:\n"
+        "    git push nn1@v0.0.1\n",
+    )
+    # this check depends on the previous one
     _check_successful_cmd(
         "assign",
         ["-r", repo.working_dir, "nn1", "prod", "HEAD"],
-        "Created git tag 'nn1@v0.0.1' that registers a new version\n"
-        "Created git tag 'nn1#prod#1' that assigns 'prod' to 'v0.0.1'\n",
+        "Created git tag 'nn1#prod#1' that assigns stage to version 'v0.0.1'\n"
+        "To push the changes upstream, run:\n"
+        "    git push nn1#prod#1\n",
     )
-
     # this check depends on the previous assignment
     _check_failing_cmd(
         "assign",
diff --git a/tests/test_config.py b/tests/test_config.py
index 9b3d47fa..80ff70c1 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -9,7 +9,7 @@
 from gto.cli import app
 from gto.config import CONFIG_FILE_NAME, check_name_is_valid
 from gto.exceptions import (
-    GTOException,
+    InvalidVersion,
     UnknownStage,
     UnknownType,
     ValidationError,
@@ -36,7 +36,7 @@ def init_repo(empty_git_repo: Tuple[git.Repo, Callable]):
     repo, write_file = empty_git_repo
 
     write_file(CONFIG_FILE_NAME, CONFIG_CONTENT)
-    repo.index.add(CONFIG_FILE_NAME)
+    repo.index.add([CONFIG_FILE_NAME])
     repo.index.commit("Initial commit")
     return repo
 
@@ -91,8 +91,8 @@ def test_register_incorrect_name(init_repo):
 
 
 def test_register_incorrect_version(init_repo):
-    with pytest.raises(GTOException):
-        register(init_repo, ALLOWED_STRING, ref="HEAD", version="###")
+    with pytest.raises(InvalidVersion):
+        register(init_repo, "model", ref="HEAD", version="###")
 
 
 def test_assign_incorrect_name(init_repo):
diff --git a/tests/test_registry.py b/tests/test_registry.py
index bf53d39b..41461178 100644
--- a/tests/test_registry.py
+++ b/tests/test_registry.py
@@ -1,152 +1,348 @@
-# pylint: disable=unused-variable
+# pylint: disable=unused-variable, too-many-locals
 from gto.registry import GitRegistry
 
-from .utils import _check_dict
+from .utils import check_obj
 
 EXPECTED_REGISTRY_TAG_TAG_STATE = {
     "artifacts": {
-        "nn": {
+        "rf": {
+            "artifact": "rf",
             "versions": [
                 {
-                    "artifact": "nn",
-                    "name": "v0.0.1",
-                    "created_at": "2022-04-11T21:51:56",
-                    "author": "Alexander Guschin",
-                    "author_email": "1aguschin@gmail.com",
-                    "commit_hexsha": "d1d973669cade722f2900e75379cee42fe6b0244",
-                    "discovered": False,
-                    "tag": "nn@v0.0.1",
-                    "assignments": [
+                    "artifact": "rf",
+                    "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                    "version": "v1.2.3",
+                    "enrichments": [
                         {
-                            "artifact": "nn",
-                            "version": "v0.0.1",
-                            "stage": "staging",
-                            "created_at": "2022-04-11T21:51:57",
+                            "priority": 0,
+                            "addition": True,
+                            "artifact": "rf",
+                            "created_at": "2022-08-04T16:56:57",
                             "author": "Alexander Guschin",
                             "author_email": "1aguschin@gmail.com",
-                            "commit_hexsha": "d1d973669cade722f2900e75379cee42fe6b0244",
-                            "tag": "nn#staging#1",
+                            "message": "Add artifacts",
+                            "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                            "version": "v1.2.3",
+                            "enrichments": [
+                                {
+                                    "source": "gto",
+                                    "artifact": {
+                                        "type": "model",
+                                        "path": "models/random-forest.pkl",
+                                        "virtual": False,
+                                        "labels": [],
+                                        "description": "",
+                                    },
+                                }
+                            ],
+                            "committer": "Alexander Guschin",
+                            "committer_email": "1aguschin@gmail.com",
                         }
                     ],
-                    "enrichments": [
+                    "registrations": [
                         {
-                            "source": "gto",
-                            "artifact": {
-                                "type": "model",
-                                "name": "nn",
-                                "path": "models/neural-network.pkl",
-                                "virtual": False,
-                                "labels": [],
-                                "description": "",
-                            },
+                            "priority": 3,
+                            "addition": True,
+                            "artifact": "rf",
+                            "created_at": "2022-08-04T16:56:57",
+                            "author": "Alexander Guschin",
+                            "author_email": "1aguschin@gmail.com",
+                            "message": "Registering artifact rf version v1.2.3",
+                            "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                            "tag": "rf@v1.2.3",
+                            "version": "v1.2.3",
                         }
                     ],
-                }
-            ],
-        },
-        "rf": {
-            "versions": [
+                    "deregistrations": [],
+                    "stages": {
+                        "production": {
+                            "artifact": "rf",
+                            "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                            "version": "v1.2.3",
+                            "stage": "production",
+                            "assignments": [
+                                {
+                                    "priority": 5,
+                                    "addition": True,
+                                    "artifact": "rf",
+                                    "created_at": "2022-08-04T16:56:59",
+                                    "author": "Alexander Guschin",
+                                    "author_email": "1aguschin@gmail.com",
+                                    "message": "Assigning stage production to artifact rf version v1.2.3",
+                                    "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                                    "tag": "rf#production#1",
+                                    "version": "v1.2.3",
+                                    "stage": "production",
+                                },
+                                {
+                                    "priority": 5,
+                                    "addition": True,
+                                    "artifact": "rf",
+                                    "created_at": "2022-08-04T16:57:02",
+                                    "author": "Alexander Guschin",
+                                    "author_email": "1aguschin@gmail.com",
+                                    "message": "Assigning stage production to artifact rf version v1.2.3",
+                                    "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                                    "tag": "rf#production#4",
+                                    "version": "v1.2.3",
+                                    "stage": "production",
+                                },
+                            ],
+                            "unassignments": [],
+                        }
+                    },
+                },
                 {
                     "artifact": "rf",
-                    "name": "v1.2.3",
-                    "created_at": "2022-04-11T21:51:56",
-                    "author": "Alexander Guschin",
-                    "author_email": "1aguschin@gmail.com",
-                    "commit_hexsha": "d1d973669cade722f2900e75379cee42fe6b0244",
-                    "discovered": False,
-                    "tag": "rf@v1.2.3",
-                    "assignments": [
+                    "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
+                    "version": "v1.2.4",
+                    "enrichments": [
                         {
+                            "priority": 0,
+                            "addition": True,
                             "artifact": "rf",
-                            "version": "v1.2.3",
-                            "stage": "production",
-                            "created_at": "2022-04-11T21:51:57",
+                            "created_at": "2022-08-04T16:56:59",
                             "author": "Alexander Guschin",
                             "author_email": "1aguschin@gmail.com",
-                            "commit_hexsha": "d1d973669cade722f2900e75379cee42fe6b0244",
-                            "tag": "rf#production#1",
-                        },
+                            "message": "Update model",
+                            "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
+                            "version": "v1.2.4",
+                            "enrichments": [
+                                {
+                                    "source": "gto",
+                                    "artifact": {
+                                        "type": "model",
+                                        "path": "models/random-forest.pkl",
+                                        "virtual": False,
+                                        "labels": [],
+                                        "description": "",
+                                    },
+                                }
+                            ],
+                            "committer": "Alexander Guschin",
+                            "committer_email": "1aguschin@gmail.com",
+                        }
+                    ],
+                    "registrations": [
                         {
+                            "priority": 3,
+                            "addition": True,
                             "artifact": "rf",
-                            "version": "v1.2.3",
-                            "stage": "production",
-                            "created_at": "2022-04-11T21:52:01",
+                            "created_at": "2022-08-04T16:56:59",
                             "author": "Alexander Guschin",
                             "author_email": "1aguschin@gmail.com",
-                            "commit_hexsha": "d1d973669cade722f2900e75379cee42fe6b0244",
-                            "tag": "rf#production#4",
-                        },
-                    ],
-                    "enrichments": [
-                        {
-                            "source": "gto",
-                            "artifact": {
-                                "type": "model",
-                                "name": "rf",
-                                "path": "models/random-forest.pkl",
-                                "virtual": False,
-                                "labels": [],
-                                "description": "",
-                            },
+                            "message": "Registering artifact rf version v1.2.4",
+                            "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
+                            "tag": "rf@v1.2.4",
+                            "version": "v1.2.4",
                         }
                     ],
-                },
-                {
-                    "artifact": "rf",
-                    "name": "v1.2.4",
-                    "created_at": "2022-04-12T19:03:44",
-                    "author": "Alexander Guschin",
-                    "author_email": "1aguschin@gmail.com",
-                    "commit_hexsha": "16b7b77f1219ea3c10ae5beeb8473fb49cbd8c13",
-                    "discovered": False,
-                    "tag": "rf@v1.2.4",
-                    "assignments": [
-                        {
+                    "deregistrations": [],
+                    "stages": {
+                        "staging": {
                             "artifact": "rf",
+                            "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
                             "version": "v1.2.4",
                             "stage": "staging",
-                            "created_at": "2022-04-11T21:51:58",
-                            "author": "Alexander Guschin",
-                            "author_email": "1aguschin@gmail.com",
-                            "commit_hexsha": "16b7b77f1219ea3c10ae5beeb8473fb49cbd8c13",
-                            "tag": "rf#staging#2",
+                            "assignments": [
+                                {
+                                    "priority": 5,
+                                    "addition": True,
+                                    "artifact": "rf",
+                                    "created_at": "2022-08-04T16:57:00",
+                                    "author": "Alexander Guschin",
+                                    "author_email": "1aguschin@gmail.com",
+                                    "message": "Assigning stage staging to artifact rf version v1.2.4",
+                                    "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
+                                    "tag": "rf#staging#2",
+                                    "version": "v1.2.4",
+                                    "stage": "staging",
+                                }
+                            ],
+                            "unassignments": [],
                         },
-                        {
+                        "production": {
                             "artifact": "rf",
+                            "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
                             "version": "v1.2.4",
                             "stage": "production",
-                            "created_at": "2022-04-11T21:51:59",
+                            "assignments": [
+                                {
+                                    "priority": 5,
+                                    "addition": True,
+                                    "artifact": "rf",
+                                    "created_at": "2022-08-04T16:57:01",
+                                    "author": "Alexander Guschin",
+                                    "author_email": "1aguschin@gmail.com",
+                                    "message": "Assigning stage production to artifact rf version v1.2.4",
+                                    "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
+                                    "tag": "rf#production#3",
+                                    "version": "v1.2.4",
+                                    "stage": "production",
+                                }
+                            ],
+                            "unassignments": [],
+                        },
+                    },
+                },
+            ],
+            "creations": [],
+            "deprecations": [],
+        },
+        "nn": {
+            "artifact": "nn",
+            "versions": [
+                {
+                    "artifact": "nn",
+                    "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                    "version": "v0.0.1",
+                    "enrichments": [
+                        {
+                            "priority": 0,
+                            "addition": True,
+                            "artifact": "nn",
+                            "created_at": "2022-08-04T16:56:57",
                             "author": "Alexander Guschin",
                             "author_email": "1aguschin@gmail.com",
-                            "commit_hexsha": "16b7b77f1219ea3c10ae5beeb8473fb49cbd8c13",
-                            "tag": "rf#production#3",
-                        },
+                            "message": "Add artifacts",
+                            "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                            "version": "v0.0.1",
+                            "enrichments": [
+                                {
+                                    "source": "gto",
+                                    "artifact": {
+                                        "type": "model",
+                                        "path": "models/neural-network.pkl",
+                                        "virtual": False,
+                                        "labels": [],
+                                        "description": "",
+                                    },
+                                }
+                            ],
+                            "committer": "Alexander Guschin",
+                            "committer_email": "1aguschin@gmail.com",
+                        }
+                    ],
+                    "registrations": [
                         {
-                            "artifact": "rf",
-                            "version": "v1.2.4",
-                            "stage": "stagegeg",
-                            "created_at": "2022-04-12T19:26:08",
+                            "priority": 3,
+                            "addition": True,
+                            "artifact": "nn",
+                            "created_at": "2022-08-04T16:56:58",
                             "author": "Alexander Guschin",
                             "author_email": "1aguschin@gmail.com",
-                            "commit_hexsha": "16b7b77f1219ea3c10ae5beeb8473fb49cbd8c13",
-                            "tag": "rf#stagegeg#5",
-                        },
+                            "message": "Registering artifact nn version v0.0.1",
+                            "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                            "tag": "nn@v0.0.1",
+                            "version": "v0.0.1",
+                        }
                     ],
+                    "deregistrations": [],
+                    "stages": {
+                        "staging": {
+                            "artifact": "nn",
+                            "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                            "version": "v0.0.1",
+                            "stage": "staging",
+                            "assignments": [
+                                {
+                                    "priority": 5,
+                                    "addition": True,
+                                    "artifact": "nn",
+                                    "created_at": "2022-08-04T16:56:59",
+                                    "author": "Alexander Guschin",
+                                    "author_email": "1aguschin@gmail.com",
+                                    "message": "Assigning stage staging to artifact nn version v0.0.1",
+                                    "commit_hexsha": "89de382074d472f8e6b8fd654490183c3c0fb497",
+                                    "tag": "nn#staging#1",
+                                    "version": "v0.0.1",
+                                    "stage": "staging",
+                                }
+                            ],
+                            "unassignments": [],
+                        }
+                    },
+                },
+                {
+                    "artifact": "nn",
+                    "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
+                    "version": "04d79900801d9aa7ec726169706280a32a25d198",
                     "enrichments": [
                         {
-                            "source": "gto",
-                            "artifact": {
-                                "type": "model",
-                                "name": "rf",
-                                "path": "models/random-forest.pkl",
-                                "virtual": False,
-                                "labels": [],
-                                "description": "",
-                            },
+                            "priority": 0,
+                            "addition": True,
+                            "artifact": "nn",
+                            "created_at": "2022-08-04T16:56:59",
+                            "author": "Alexander Guschin",
+                            "author_email": "1aguschin@gmail.com",
+                            "message": "Update model",
+                            "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
+                            "version": "04d79900801d9aa7ec726169706280a32a25d198",
+                            "enrichments": [
+                                {
+                                    "source": "gto",
+                                    "artifact": {
+                                        "type": "model",
+                                        "path": "models/neural-network.pkl",
+                                        "virtual": False,
+                                        "labels": [],
+                                        "description": "",
+                                    },
+                                }
+                            ],
+                            "committer": "Alexander Guschin",
+                            "committer_email": "1aguschin@gmail.com",
                         }
                     ],
+                    "registrations": [],
+                    "deregistrations": [],
+                    "stages": {},
                 },
             ],
+            "creations": [],
+            "deprecations": [],
+        },
+        "features": {
+            "artifact": "features",
+            "versions": [
+                {
+                    "artifact": "features",
+                    "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
+                    "version": "04d79900801d9aa7ec726169706280a32a25d198",
+                    "enrichments": [
+                        {
+                            "priority": 0,
+                            "addition": True,
+                            "artifact": "features",
+                            "created_at": "2022-08-04T16:56:59",
+                            "author": "Alexander Guschin",
+                            "author_email": "1aguschin@gmail.com",
+                            "message": "Update model",
+                            "commit_hexsha": "04d79900801d9aa7ec726169706280a32a25d198",
+                            "version": "04d79900801d9aa7ec726169706280a32a25d198",
+                            "enrichments": [
+                                {
+                                    "source": "gto",
+                                    "artifact": {
+                                        "type": "dataset",
+                                        "path": "datasets/features.csv",
+                                        "virtual": True,
+                                        "labels": [],
+                                        "description": "",
+                                    },
+                                }
+                            ],
+                            "committer": "Alexander Guschin",
+                            "committer_email": "1aguschin@gmail.com",
+                        }
+                    ],
+                    "registrations": [],
+                    "deregistrations": [],
+                    "stages": {},
+                }
+            ],
+            "creations": [],
+            "deprecations": [],
         },
     }
 }
@@ -160,26 +356,10 @@ def iter_over(sequence):
     raise NotImplementedError
 
 
-def _check_state(appread_state, expected_state, exclude):
-    for name in expected_state["artifacts"]:
-        for part in ["versions"]:  # "commits"
-            for appeared, expected in zip(
-                iter_over(appread_state["artifacts"][name][part]),
-                iter_over(expected_state["artifacts"][name][part]),
-            ):
-                _check_dict(appeared, expected, exclude[part])
-        for appeared, expected in zip(
-            appread_state["artifacts"][name]["versions"],
-            expected_state["artifacts"][name]["versions"],
-        ):
-            for a, e in zip(appeared["assignments"], expected["assignments"]):
-                _check_dict(a, e, exclude["assignments"])
-
-
 def test_registry_state_tag_tag(showcase):
     path, repo, write_file, first_commit, second_commit = showcase
     reg = GitRegistry.from_repo(repo)
-    state = reg.get_state().dict()
+    appeared_state = reg.get_state().dict()
 
     # TODO: update state
     exclude = {
@@ -189,17 +369,68 @@ def test_registry_state_tag_tag(showcase):
             "author_email",
             "created_at",
             "commit_hexsha",
-            "assignments",
+            "registrations",
+            "deregistrations",
             "enrichments",
+            "stages",
             "message",
+            "version",
         ],
-        "assignments": [
+        "enrichments": [
             "author",
             "author_email",
             "created_at",
             "commit_hexsha",
             "message",
+            "version",
+            "committer",
+            "committer_email",
+        ],
+        "registrations": [
+            "author",
+            "author_email",
+            "created_at",
+            "commit_hexsha",
+            "message",
+            "version",
+        ],
+        "deregistrations": [
+            "author",
+            "author_email",
+            "created_at",
+            "commit_hexsha",
+            "message",
+            "version",
+        ],
+        "stages": [
+            "author",
+            "author_email",
+            "created_at",
+            "commit_hexsha",
+            "message",
+            "version",
+            "assignments",
+            "unassignments",
         ],
     }
 
-    _check_state(state, EXPECTED_REGISTRY_TAG_TAG_STATE, exclude)
+    expected_state = EXPECTED_REGISTRY_TAG_TAG_STATE
+    for artifact in expected_state["artifacts"]:
+
+        for appeared, expected in zip(
+            iter_over(appeared_state["artifacts"][artifact]["versions"]),
+            iter_over(expected_state["artifacts"][artifact]["versions"]),
+        ):
+            check_obj(appeared, expected, exclude["versions"])
+
+            for key in ["enrichments", "registrations", "deregistrations"]:
+                for a, e in zip(
+                    iter_over(appeared[key]),
+                    iter_over(expected[key]),
+                ):
+                    check_obj(a, e, exclude[key])
+
+            for key in appeared["stages"]:
+                check_obj(
+                    appeared["stages"][key], expected["stages"][key], exclude["stages"]
+                )
diff --git a/tests/test_showcase.py b/tests/test_showcase.py
index 13157912..5028e840 100644
--- a/tests/test_showcase.py
+++ b/tests/test_showcase.py
@@ -1,8 +1,17 @@
 """TODO: break this file into multiple test/files"""
 # pylint: disable=unused-variable, too-many-locals, too-many-statements
 import gto
-from gto.base import BaseArtifact, BaseAssignment, BaseVersion
-from tests.utils import _check_obj
+from gto.base import (
+    Artifact,
+    Assignment,
+    Commit,
+    Deregistration,
+    Registration,
+    Unassignment,
+    Version,
+    VStage,
+)
+from tests.utils import check_obj
 
 
 def test_api(showcase):
@@ -29,44 +38,60 @@ def test_api(showcase):
     #     ["commits"],
     # )
     nn_artifact = artifacts["nn"]
-    assert isinstance(nn_artifact, BaseArtifact)
-    assert nn_artifact.name == "nn"
+    assert isinstance(nn_artifact, Artifact)
+    assert nn_artifact.artifact == "nn"
     assert len(nn_artifact.versions) == 2
     nn_version = nn_artifact.versions[0]
-    assert isinstance(nn_version, BaseVersion)
+    assert isinstance(nn_version, Version)
     author = repo.commit().author.name
     author_email = repo.commit().author.email
 
     skip_keys_registration = {
         "created_at",
-        "assignments",
+        "activated_at",
+        "registrations",
+        "deregistrations",
         "enrichments",
         "tag",
         "message",
+        "stages",
+    }
+    skip_keys_assignment = {
+        "created_at",
+        "message",
+        "assignments",
+        "unassignments",
+        "activated_at",
+        "priority",
+        "addition",
+        "event",
     }
-    skip_keys_assignment = {"created_at", "tag", "message"}
 
-    _check_obj(
-        nn_version,
+    check_obj(
+        nn_version.dict_state(),
         dict(
             artifact="nn",
-            name="v0.0.1",
+            version="v0.0.1",
             author=author,
             author_email=author_email,
             commit_hexsha=first_commit.hexsha,
             discovered=False,
+            is_active=True,
+            ref="nn@v0.0.1",
         ),
         skip_keys=skip_keys_registration,
     )
-    assert len(nn_artifact.stages) == 1
-    nn_assignment = nn_artifact.stages[0]
-    assert isinstance(nn_assignment, BaseAssignment)
-    _check_obj(
-        nn_assignment,
+    nn_vstages = nn_artifact.get_vstages()
+    assert len(nn_vstages) == 1
+    assert isinstance(nn_vstages["staging"][0], VStage)
+    check_obj(
+        nn_vstages["staging"][0].dict_state(),
         dict(
             artifact="nn",
             version="v0.0.1",
             stage="staging",
+            is_active=True,
+            ref="nn#staging#1",
             author=author,
             author_email=author_email,
             commit_hexsha=first_commit.hexsha,
@@ -75,17 +100,19 @@ def test_api(showcase):
     )
 
     rf_artifact = artifacts["rf"]
-    assert isinstance(rf_artifact, BaseArtifact)
-    assert rf_artifact.name == "rf"
+    assert isinstance(rf_artifact, Artifact)
+    assert rf_artifact.artifact == "rf"
 
     assert len(rf_artifact.versions) == 2
-    assert all(isinstance(v, BaseVersion) for v in rf_artifact.versions)
+    assert all(isinstance(v, Version) for v in rf_artifact.versions)
     rf_ver1, rf_ver2 = rf_artifact.versions
-    _check_obj(
-        rf_ver1,
+    check_obj(
+        rf_ver1.dict_state(),
         dict(
             artifact="rf",
-            name="v1.2.3",
+            version="v1.2.3",
+            is_active=True,
+            ref="rf@v1.2.3",
             author=author,
             author_email=author_email,
             commit_hexsha=first_commit.hexsha,
@@ -93,11 +120,13 @@ def test_api(showcase):
         ),
         skip_keys=skip_keys_registration,
     )
-    _check_obj(
-        rf_ver2,
+    check_obj(
+        rf_ver2.dict_state(),
         dict(
             artifact="rf",
-            name="v1.2.4",
+            version="v1.2.4",
+            is_active=True,
+            ref="rf@v1.2.4",
             author=author,
             author_email=author_email,
             commit_hexsha=second_commit.hexsha,
@@ -106,45 +135,62 @@ def test_api(showcase):
         skip_keys=skip_keys_registration,
     )
 
-    assert len(rf_artifact.stages) == 4
-    assert all(isinstance(p, BaseAssignment) for p in rf_artifact.stages)
-    rf_l1, _ = rf_ver1.assignments
-    rf_l3, rf_l4 = rf_ver2.assignments
+    assert len(rf_artifact.get_events()) == 8
+    assert all(
+        isinstance(p, (Assignment, Unassignment, Registration, Deregistration, Commit))
+        for p in rf_artifact.get_events()
+    )
+    assert all(
+        isinstance(p, (Registration, Deregistration))
+        for p in rf_ver1.get_events(indirect=False) + rf_ver2.get_events(indirect=False)
+    )
+    assert all(
+        isinstance(p, (Assignment, Unassignment, Commit))
+        for p in rf_ver1.get_events(direct=False) + rf_ver2.get_events(direct=False)
+    )
+    rf_a4, rf_a1, rf_c1 = rf_ver1.get_events(
+        direct=False
+    )  # pylint: disable=unused-variable
+    rf_a3, rf_a2, rf_c2 = rf_ver2.get_events(
+        direct=False
+    )  # pylint: disable=unused-variable
 
-    _check_obj(
-        rf_l1,
+    check_obj(
+        rf_a1.dict_state(),
         dict(
             artifact="rf",
             version="v1.2.3",
             stage="production",
+            tag="rf#production#1",
             author=author,
             author_email=author_email,
             commit_hexsha=first_commit.hexsha,
         ),
         skip_keys=skip_keys_assignment,
     )
-    _check_obj(
-        rf_l4,
+    check_obj(
+        rf_a3.dict_state(),
         dict(
             artifact="rf",
             version="v1.2.4",
             stage="production",
+            tag="rf#production#3",
             author=author,
             author_email=author_email,
             commit_hexsha=second_commit.hexsha,
         ),
         skip_keys=skip_keys_assignment,
     )
-    _check_obj(
-        rf_l3,
+    check_obj(
+        rf_a2.dict_state(),
         dict(
             artifact="rf",
             version="v1.2.4",
             stage="staging",
+            tag="rf#staging#2",
             author=author,
             author_email=author_email,
             commit_hexsha=second_commit.hexsha,
         ),
         skip_keys=skip_keys_assignment,
     )
-    assert gto.api.find_versions_in_stage(repo, "rf", "staging", all=True) == [rf_l3]
diff --git a/tests/test_tag.py b/tests/test_tag.py
index 38ef3361..b275a9ba 100644
--- a/tests/test_tag.py
+++ b/tests/test_tag.py
@@ -6,35 +6,59 @@
 
 from gto.constants import Action
 from gto.exceptions import RefNotFound, TagExists
-from gto.tag import ActionSign, create_tag, find, name_tag, parse_name
+from gto.tag import create_tag, find, name_tag, parse_name
 
 
 def test_name_tag(empty_git_repo):
     repo, write_func = empty_git_repo
+    assert name_tag(Action.REGISTER, "myartifact", "v1", simple=True) == "myartifact@v1"
     assert (
-        name_tag(Action.REGISTER, "myartifact", "v1")
-        == f"myartifact{ActionSign[Action.REGISTER]}v1"
+        name_tag(Action.REGISTER, "myartifact", "v1", simple=False, repo=repo)
+        == "myartifact@v1#1"
+    )
+    assert (
+        name_tag(Action.DEREGISTER, "myartifact", "v1", simple=True) == "myartifact@v1!"
+    )
+    assert (
+        name_tag(Action.DEREGISTER, "myartifact", "v1", simple=False, repo=repo)
+        == "myartifact@v1!#1"
     )
     assert (
         name_tag(Action.ASSIGN, "myartifact", stage="stage", simple=True)
-        == f"myartifact{ActionSign[Action.ASSIGN]}stage"
+        == "myartifact#stage"
     )
     assert (
         name_tag(Action.ASSIGN, "myartifact", repo=repo, stage="stage", simple=False)
-        == f"myartifact{ActionSign[Action.ASSIGN]}stage{ActionSign[Action.ASSIGN]}1"
+        == "myartifact#stage#1"
+    )
+    assert (
+        name_tag(Action.UNASSIGN, "myartifact", repo=repo, stage="stage", simple=False)
+        == "myartifact#stage!#1"
     )
 
 
 def test_parse_name():
-    assert parse_name(f"path{ActionSign[Action.REGISTER]}v1.2.3") == dict(
+    assert parse_name("path@v1.2.3") == dict(
         name="path", version="v1.2.3", action=Action.REGISTER
     )
-    assert parse_name(f"path{ActionSign[Action.ASSIGN]}stage") == dict(
+    assert parse_name("path@v1.2.3#5") == dict(
+        name="path", version="v1.2.3", action=Action.REGISTER, counter=5
+    )
+    assert parse_name("path@v1.2.3!") == dict(
+        name="path", version="v1.2.3", action=Action.DEREGISTER
+    )
+    assert parse_name("path@v1.2.3!#2") == dict(
+        name="path", version="v1.2.3", action=Action.DEREGISTER, counter=2
+    )
+    assert parse_name("path#stage") == dict(
         name="path", action=Action.ASSIGN, stage="stage"
     )
-    assert parse_name(
-        f"path{ActionSign[Action.ASSIGN]}stage{ActionSign[Action.ASSIGN]}1"
-    ) == dict(name="path", action=Action.ASSIGN, stage="stage", number=1)
+    assert parse_name("path#stage#1") == dict(
+        name="path", action=Action.ASSIGN, stage="stage", counter=1
+    )
+    assert parse_name("path#stage!#2") == dict(
+        name="path", action=Action.UNASSIGN, stage="stage", counter=2
+    )
 
 
 @pytest.mark.parametrize(
@@ -44,6 +68,7 @@ def test_parse_name():
         "###",
         "@@@",
         "model@v1",
+        "nn#prod#-1",
         "model@v111",
         "model-prod",
         "model@0.0.1",
@@ -51,6 +76,8 @@ def test_parse_name():
         "model#prod#-1",
         "model#prod#1-2",
         "model#prod?#1-2",
+        "model#prod@v1.2.3",
+        "model@v1.2.3#prod",
         "model-prod-v1-stage",
         "model@prod@v1@stage-1",
         "namespace/model@0.0.1",
@@ -64,6 +91,28 @@ def test_parse_wrong_names(tag_name):
     assert not parse_name(tag_name, raise_on_fail=False)
 
 
+@pytest.mark.parametrize(
+    "tag_name",
+    [
+        "nn#prod",
+        "nn#prod!",
+        "nn#prod#1",
+        "nn@v1.0.0",
+        "nn#prod!#2",
+        "nn@v1.0.0!",
+        "nn@v1.0.0#1",
+        "nn@v1.0.0!#1",
+        "nn@v1.0.0-rc.2",
+        "nn@v1.0.0-alpha",
+        "nn@v1.0.0-alpha.1",
+        "nn@v1.0.0-alpha.beta",
+        "new-artifact@v1.0.1!",
+    ],
+)
+def test_parse_correct_names(tag_name):
+    assert parse_name(tag_name, raise_on_fail=False)
+
+
 def test_create_tag_bad_ref(repo_with_commit):
     repo, _ = repo_with_commit
     with pytest.raises(RefNotFound):
diff --git a/tests/utils.py b/tests/utils.py
index efd0dc14..38258c7e 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,29 +1,34 @@
+from copy import deepcopy
 from typing import Any, Dict, Sequence, Set, Union
 
 from funcy import omit
 from pydantic import BaseModel
 
 
-def _assert_equals(a, b):
-    # separate function is helpful for debug
-    # cause you see dicts without skip_keys
-    assert a == b, f"\n{a} \n!=\n {b}"
+def show_difference(left: Dict, right: Dict):
+    left_ = deepcopy(left)
+    right_ = deepcopy(right)
+    for key in list(left_.keys()):
+        if left_.get(key) == right_.get(key):
+            left_.pop(key)
+            right_.pop(key)
+    return f"\n{left_} \n!=\n {right_}"
 
 
-def _check_obj(
-    obj: BaseModel, values: Dict[str, Any], skip_keys: Union[Set[str], Sequence[str]]
-):
-    obj_values = obj.dict(exclude=set(skip_keys))
-    _assert_equals(obj_values, values)
-    # assert obj_values == values
+def assert_equals(left, right):
+    # separate function is helpful for debug
+    # cause you see dicts without skip_keys
+    assert left == right, show_difference(left, right)
 
 
-def _check_dict(
-    obj: Dict[str, Any],
+def check_obj(
+    obj: Union[BaseModel, Dict[str, Any]],
     values: Dict[str, Any],
     skip_keys: Union[Set[str], Sequence[str]],
 ):
-    obj_values = omit(obj, skip_keys)
+    if isinstance(obj, BaseModel):
+        obj_values = obj.dict(exclude=set(skip_keys))
+    else:
+        obj_values = omit(obj, skip_keys)
     values = omit(values, skip_keys)
-    _assert_equals(obj_values, values)
-    # assert obj_values == values
+    assert_equals(obj_values, values)