# Extending setuptools-scm

`setuptools-scm` uses [entry-point][entry-point] based hooks to extend its default capabilities.

[entry-point]: https://packaging.python.org/en/latest/specifications/entry-points/

## Adding a new SCM

`setuptools-scm` provides two entrypoints for adding new SCMs:

`setuptools_scm.parse_scm`
:   A function used to parse the metadata of the current workdir
    using the name of the control directory/file of your SCM as the
    entrypoint's name. E.g. for the built-in entrypoint for Git the
    entrypoint is named `.git` and references `setuptools_scm.git:parse`

    The return value MUST be a [`vcs_versioning.ScmVersion`][] instance
    created by the function [`vcs_versioning._version_schemes.meta`][].

`setuptools_scm.files_command`
:  Either a string containing a shell command that prints all SCM-managed
   files in its current working directory or a callable, that given a
   pathname will return that list.

   Also uses the name of your SCM control directory as the name of the entrypoint.


### api reference for scm version objects

::: vcs_versioning.ScmVersion
    options:
      show_root_heading: yes
      heading_level: 4

::: vcs_versioning._version_schemes.meta
    options:
      show_root_heading: yes
      heading_level: 4

::: vcs_versioning._version_schemes.DirtyWorkingTreeError
    options:
      show_root_heading: yes
      heading_level: 4

## Adding a workdir discovery factory

The `vcs_versioning.discover_workdir` entry-point group controls how
setuptools-scm finds VCS checkouts and fallback metadata. Each factory
receives a directory path and the current `Configuration`, and returns
a `ScmWorkdir`, `FallbackWorkdir`, or `None`.

```python
# mypackage/discovery.py
from pathlib import Path
from vcs_versioning._backends._scm_workdir import ScmWorkdir
from vcs_versioning._fallback_workdir import FallbackWorkdir
from vcs_versioning._config import Configuration


def discover_my_scm(
    path: Path, *, config: Configuration
) -> ScmWorkdir | FallbackWorkdir | None:
    if (path / ".myscm").is_dir():
        return MyScmWorkdir(path=path, _config=config)
    return None
```

Register it in your `pyproject.toml`:

```toml
[project.entry-points."vcs_versioning.discover_workdir"]
myscm = "mypackage.discovery:discover_my_scm"
```

Discovery runs in two phases:

1. **SCM phase** — probes the configured root (and parent directories when
   `search_parent_directories` is enabled) for live VCS checkouts.
   A `ScmWorkdir` result is returned immediately.
2. **Fallback phase** — if no SCM workdir was found, probes the project
   directory for fallback metadata (archival files, egg-info, etc.).

SCM results always take priority over fallback results.

Several fallback factories can match the same directory -- a setuptools
built sdist carries both a `PKG-INFO` and an `*.egg-info/scm_version.json`.
Discovery stashes every match and takes the first that yields a version,
ordered by the workdir's `discovery_priority` (lower wins) rather than by
entry point order, which follows the installed distributions and is not
something a factory can control. Rank by how much the workdir knows:

| `discovery_priority` | Workdir | Carries |
|---|---|---|
| 10 | `MetadataWorkdir` | tag, distance, node, tracked file list |
| 20 | `ArchivedWorkdir` | describe output |
| 30 | `PkgInfoWorkdir` | flat preformatted version |
| 100 | *default* | third-party workdirs, unless they set their own |

### Built-in factories

| Entry point | Package | Type |
|---|---|---|
| `hg-git` | vcs-versioning | SCM (Git + Mercurial) |
| `archival` | vcs-versioning | Fallback (`.git_archival.txt`) |
| `pkginfo` | vcs-versioning | Fallback (`PKG-INFO`, present in any sdist) |
| `egg-info` | setuptools-scm | Fallback (`*.egg-info/scm_version.json`; written for sdists, omitted from wheels) |


## Version number construction

### `setuptools_scm.version_scheme`

Configures how the version number is constructed given a
[ScmVersion][vcs_versioning.ScmVersion] instance and should return a string
representing the version.
### Available implementations

`guess-next-dev (default)`
:   Automatically guesses the next development version (default).
    Guesses the upcoming release by incrementing the pre-release segment if present,
    otherwise by incrementing the micro segment. Then appends `.devN`.
    In case the tag ends with `.dev0` the version is not bumped
    and custom `.devN` versions will trigger an error.

    **Examples:**

    - Tag `v1.0.0` → version `1.0.1.dev0` (if dirty or distance > 0)
    - Tag `v1.0.0` → version `1.0.0` (if exact match)

`calver-by-date`
:   Calendar versioning scheme that generates versions based on dates.
    Uses the format `YY.MM.DD.patch` or `YYYY.MM.DD.patch` depending on the existing tag format.
    If the commit is on the same date as the latest tag, increments the patch number.
    Otherwise, uses the current date with patch 0. Supports branch-specific versioning
    for release branches.

    **Examples:**

    - Tag `v23.01.15.0` on same day → version `23.01.15.1.devN`
    - Tag `v23.01.15.0` on different day (e.g., 2023-01-16) → version `23.01.16.0.devN`
    - Tag `v2023.01.15.0` → uses 4-digit year format for new versions

`no-guess-dev`
:   Does no next version guessing, just adds `.post1.devN`.
    This is the recommended replacement for the deprecated `post-release` scheme.

    **Examples:**

    - Tag `v1.0.0` → version `1.0.0.post1.devN` (if distance > 0)
    - Tag `v1.0.0` → version `1.0.0` (if exact match)

`only-version`
:   Only use the version from the tag, as given.

    !!! warning "This means version is no longer pseudo unique per commit"

    **Examples:**

    - Tag `v1.0.0` → version `1.0.0` (always, regardless of distance or dirty state)

`post-release (deprecated)`
:   Generates post release versions (adds `.postN`)
    after review of the version number pep this is considered a bad idea
    as post releases are intended to be chosen not autogenerated.

    !!! warning "the recommended replacement is `no-guess-dev`"

    **Examples:**

    - Tag `1.0.0` → version `1.0.0.postN` (where N is the distance)

`semver-pep440`
:   Basic semantic versioning with PEP 440-compliant version numbers.

    Guesses the upcoming release by incrementing the minor segment
    and setting the micro segment to zero if the current branch contains the string `feature`,
    otherwise by incrementing the micro version. Then appending `.devN`.

    This scheme is not compatible with pre-releases.

    !!! note "Renamed in setuptools-scm 10"
        Previously called `python-simplified-semver`. The old name still works but is deprecated.

    **Examples:**

    - Tag `1.0.0` on non-feature branch → version `1.0.1.devN`
    - Tag `1.0.0` on feature branch → version `1.1.0.devN`

`semver-pep440-release-branch`
:   Semantic versioning with PEP 440-compliant version numbers for projects with release branches.
    The same as `guess-next-dev` (incrementing the pre-release or micro segment)
    however when on a release branch: a branch whose name (ignoring namespace) parses as a version
    that matches the most recent tag up to the minor segment. Otherwise if on a
    non-release branch, increments the minor segment and sets the micro segment to
    zero, then appends `.devN`

    Namespaces are unix pathname separated parts of a branch/tag name.

    !!! note "Renamed in setuptools-scm 10"
        Previously called `release-branch-semver`. The old name still works but is deprecated.

    **Examples:**

    - Tag `1.0.0` on release branch `release-1.0` → version `1.0.1.devN`

    - Tag `1.0.0` on release branch `release/v1.0` → version `1.0.1.devN`

    - Tag `1.0.0` on development branch → version `1.1.0.devN`

### `setuptools_scm.local_scheme`

Configures how the local part of a version is rendered given a
[ScmVersion][vcs_versioning.ScmVersion] instance. A callable should return a string
for the local segment, or you may pass a list of scheme names—each is tried in order
until one returns a non-`None` value (an empty string still stops the chain; return
`None` to defer to the next scheme—see `fail-on-uncommitted-changes` below).
Dates and times are in Coordinated Universal Time (UTC), because as part
of the version, they should be location independent.

#### Available implementations

`node-and-date (default)`
: Adds the node on dev versions and the date on dirty workdir

`node-and-timestamp`
: Like `node-and-date` but with a timestamp of the form `%Y%m%d%H%M%S` instead

`dirty-tag`
: Adds `+dirty` if the current workdir has changes

`no-local-version`
: Omits local version, useful e.g. because PyPI does not support it

`no-local-version-strict`
: Like `no-local-version` but raises
  [`DirtyWorkingTreeError`][vcs_versioning._version_schemes.DirtyWorkingTreeError]
  when the working tree is dirty. Equivalent to
  `["fail-on-uncommitted-changes", "no-local-version"]` as a single scheme name.
  Recommended for release CI where you want PyPI-compatible versions **and** an
  explicit build failure on uncommitted changes.

`fail-on-uncommitted-changes`
:   When the working tree is dirty (`ScmVersion.dirty` is true), raises
    [`DirtyWorkingTreeError`][vcs_versioning._version_schemes.DirtyWorkingTreeError].
    When clean, returns `None` so the **next** scheme in a `local_scheme` list
    runs—pair it with your usual local scheme (for example `node-and-date` or
    `no-local-version`). This works with any `version_scheme`, so release CI can enforce a
    clean tree without a separate implementation per version scheme.

    **Example:** `local_scheme = ["fail-on-uncommitted-changes", "node-and-date"]`

    See [Configuration Overrides](overrides.md#fail-on-uncommitted-changes-in-release-ci) for
    enabling this via environment variables in CI without editing `pyproject.toml`.
