Skip to content

Commit

Permalink
Add support for older Git versions
Browse files Browse the repository at this point in the history
  • Loading branch information
mtkennerly committed Dec 2, 2022
1 parent 316c45a commit c1c96a9
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 61 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
poetry run pre-commit run --all-files --show-diff-on-failure
test:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
Expand All @@ -35,6 +35,12 @@ jobs:
include:
- python-version: '3.7'
git-version: '2.21.0' # https://lore.kernel.org/git/CAKqNo6RJqp94uLMf8Biuo=ZvMZB9Mq6RRMrUgsLW4u1ks+mnOA@mail.gmail.com/T/#u
- python-version: '3.7'
git-version: '2.7.0'
- python-version: '3.7'
git-version: '2.2.0'
- python-version: '3.7'
git-version: '1.8.2.3'
name: test (python = ${{ matrix.python-version }}, git = ${{ matrix.git-version }})
env:
DARCS_EMAIL: foo <foo@example.com>
Expand All @@ -46,6 +52,8 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- if: ${{ matrix.git-version != 'default' }}
env:
NO_OPENSSL: 'yes'
run: |
sudo apt-get update
sudo apt-get install dh-autoreconf libcurl4-gnutls-dev libexpat1-dev gettext libz-dev libssl-dev
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased

* Added compatibility with Git versions as old as 1.8.2.3.

## v1.14.1 (2022-11-15)

* Fixed Git 2.7.0 compatibility by changing `git log --no-show-signature` to `git -c log.showsignature=false log`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Dunamai is also available as a [GitHub Action](https://github.com/marketplace/ac

## Features
* Version control system support:
* [Git](https://git-scm.com) (minimum version: 2.7.0)
* [Git](https://git-scm.com) (2.7.0+ is recommended, but versions as old as 1.8.2.3 will work with some reduced functionality)
* [Mercurial](https://www.mercurial-scm.org)
* [Darcs](http://darcs.net)
* [Subversion](https://subversion.apache.org)
Expand Down
111 changes: 78 additions & 33 deletions dunamai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,15 @@ def _equal_if_set(x: _T, y: Optional[_T], unset: Sequence[Any] = (None,)) -> boo
return x == y


def _get_git_version() -> List[int]:
_, msg = _run_cmd("git version")
result = re.search(r"git version (\d+(\.\d+)*)", msg.strip())
if result is not None:
parts = result.group(1).split(".")
return [int(x) for x in parts]
return []


def _detect_vcs(expected_vcs: Optional[Vcs] = None) -> Vcs:
checks = OrderedDict(
[
Expand Down Expand Up @@ -932,6 +941,8 @@ def from_git(
if tag_branch is None:
tag_branch = "HEAD"

git_version = _get_git_version()

code, msg = _run_cmd("git symbolic-ref --short HEAD", codes=[0, 128])
if code == 128:
branch = None
Expand All @@ -946,8 +957,13 @@ def from_git(
return cls._fallback(strict, distance=0, dirty=True, branch=branch)
commit = msg

code, msg = _run_cmd('git -c log.showsignature=false log -n 1 --pretty=format:"%cI"')
timestamp = _parse_git_timestamp_iso_strict(msg)
timestamp = None
if git_version < [2, 2]:
code, msg = _run_cmd('git log -n 1 --pretty=format:"%ci"')
timestamp = _parse_git_timestamp_iso(msg)
else:
code, msg = _run_cmd('git -c log.showsignature=false log -n 1 --pretty=format:"%cI"')
timestamp = _parse_git_timestamp_iso_strict(msg)

code, msg = _run_cmd("git describe --always --dirty")
dirty = msg.endswith("-dirty")
Expand All @@ -957,41 +973,64 @@ def from_git(
if msg.strip() != "":
dirty = True

code, msg = _run_cmd(
'git for-each-ref "refs/tags/**" --merged {}'.format(tag_branch)
+ ' --format "%(refname)'
"@{%(objectname)"
"@{%(creatordate:iso-strict)"
"@{%(*committerdate:iso-strict)"
"@{%(taggerdate:iso-strict)"
'"'
)
if not msg:
try:
code, msg = _run_cmd("git rev-list --count HEAD")
distance = int(msg)
except Exception:
distance = 0
return cls._fallback(
strict,
distance=distance,
commit=commit,
dirty=dirty,
branch=branch,
timestamp=timestamp,
if git_version < [2, 7]:
code, msg = _run_cmd(
'git for-each-ref "refs/tags/**" --format "%(refname)" --sort -creatordate'
)
if not msg:
try:
code, msg = _run_cmd("git rev-list --count HEAD")
distance = int(msg)
except Exception:
distance = 0
return cls._fallback(
strict,
distance=distance,
commit=commit,
dirty=dirty,
branch=branch,
timestamp=timestamp,
)
tags = [line.replace("refs/tags/", "") for line in msg.splitlines()]
tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern(
pattern, tags, latest_tag
)
else:
code, msg = _run_cmd(
'git for-each-ref "refs/tags/**" --merged {}'.format(tag_branch)
+ ' --format "%(refname)'
"@{%(objectname)"
"@{%(creatordate:iso-strict)"
"@{%(*committerdate:iso-strict)"
"@{%(taggerdate:iso-strict)"
'"'
)
if not msg:
try:
code, msg = _run_cmd("git rev-list --count HEAD")
distance = int(msg)
except Exception:
distance = 0
return cls._fallback(
strict,
distance=distance,
commit=commit,
dirty=dirty,
branch=branch,
timestamp=timestamp,
)

detailed_tags = [] # type: List[_GitRefInfo]
tag_topo_lookup = _GitRefInfo.from_git_tag_topo_order(tag_branch)
detailed_tags = [] # type: List[_GitRefInfo]
tag_topo_lookup = _GitRefInfo.from_git_tag_topo_order(tag_branch)

for line in msg.strip().splitlines():
parts = line.split("@{")
detailed_tags.append(_GitRefInfo(*parts).with_tag_topo_lookup(tag_topo_lookup))
for line in msg.strip().splitlines():
parts = line.split("@{")
detailed_tags.append(_GitRefInfo(*parts).with_tag_topo_lookup(tag_topo_lookup))

tags = [t.ref for t in sorted(detailed_tags, key=lambda x: x.sort_key, reverse=True)]
tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern(
pattern, tags, latest_tag
)
tags = [t.ref for t in sorted(detailed_tags, key=lambda x: x.sort_key, reverse=True)]
tag, base, stage, unmatched, tagged_metadata, epoch = _match_version_pattern(
pattern, tags, latest_tag
)

code, msg = _run_cmd("git rev-list --count refs/tags/{}..HEAD".format(tag))
distance = int(msg)
Expand Down Expand Up @@ -1923,6 +1962,12 @@ def _parse_git_timestamp_iso_strict(raw: str) -> dt.datetime:
return dt.datetime.strptime(compat, "%Y-%m-%dT%H:%M:%S%z")


def _parse_git_timestamp_iso(raw: str) -> dt.datetime:
# Remove colon from timezone offset for pre-3.7 Python:
compat = re.sub(r"(.* .* [-+]\d+):(\d+)", r"\1\2", raw)
return dt.datetime.strptime(compat, "%Y-%m-%d %H:%M:%S %z")


def _parse_timestamp(raw: str, space: bool = False) -> dt.datetime:
if space:
format = "%Y-%m-%d %H:%M:%S.%f%z"
Expand Down
73 changes: 47 additions & 26 deletions tests/integration/test_dunamai.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import pytest

from dunamai import Version, Vcs, _run_cmd
from dunamai import Version, Vcs, _get_git_version, _run_cmd


def avoid_identical_ref_timestamps() -> None:
Expand All @@ -28,6 +28,11 @@ def chdir(where: Path) -> Iterator[None]:
os.chdir(str(start))


def is_git_legacy() -> bool:
version = _get_git_version()
return version < [2, 7]


def make_run_callback(where: Path) -> Callable:
def inner(command, expected_code: int = 0, env: Optional[dict] = None):
_, out = _run_cmd(command, where=where, codes=[expected_code], env=env)
Expand Down Expand Up @@ -73,6 +78,7 @@ def test__version__from_git__with_annotated_tags(tmp_path) -> None:
run = make_run_callback(vcs)
from_vcs = make_from_callback(Version.from_git)
b = "master"
legacy = is_git_legacy()

with chdir(vcs):
run("git init")
Expand Down Expand Up @@ -136,9 +142,10 @@ def test__version__from_git__with_annotated_tags(tmp_path) -> None:
'dunamai from any --format "{commit}" --full-commit'
)

# Verify tags with '/' work
run("git tag test/v0.1.0")
assert run(r'dunamai from any --pattern "^test/v(?P<base>\d\.\d\.\d)"') == "0.1.0"
if not legacy:
# Verify tags with '/' work
run("git tag test/v0.1.0")
assert run(r'dunamai from any --pattern "^test/v(?P<base>\d\.\d\.\d)"') == "0.1.0"

(vcs / "foo.txt").write_text("bye")
assert from_vcs() == Version("0.1.0", dirty=True, branch=b)
Expand All @@ -154,12 +161,16 @@ def test__version__from_git__with_annotated_tags(tmp_path) -> None:
from_vcs(latest_tag=True)

# check that we find the expected tag that has the most recent tag creation time
avoid_identical_ref_timestamps()
run("git tag v0.2.0b1 -m Annotated")
avoid_identical_ref_timestamps()
run("git tag v0.2.0 -m Annotated")
avoid_identical_ref_timestamps()
run("git tag v0.1.1 HEAD~1 -m Annotated")
if legacy:
run("git tag -d unmatched")
run("git tag v0.2.0 -m Annotated")
if not legacy:
avoid_identical_ref_timestamps()
run("git tag v0.2.0b1 -m Annotated")
avoid_identical_ref_timestamps()
run("git tag v0.2.0 -m Annotated")
avoid_identical_ref_timestamps()
run("git tag v0.1.1 HEAD~1 -m Annotated")
assert from_vcs() == Version("0.2.0", dirty=False, branch=b)
assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch=b)

Expand All @@ -168,9 +179,10 @@ def test__version__from_git__with_annotated_tags(tmp_path) -> None:
assert from_vcs() == Version("0.2.0", dirty=False, branch="heads/v0.2.0")
assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch="heads/v0.2.0")

run("git checkout v0.1.0")
assert from_vcs() == Version("0.1.1", dirty=False)
assert from_vcs(latest_tag=True) == Version("0.1.1", dirty=False)
if not legacy:
run("git checkout v0.1.0")
assert from_vcs() == Version("0.1.1", dirty=False)
assert from_vcs(latest_tag=True) == Version("0.1.1", dirty=False)

# Additional one-off check not in other VCS integration tests:
run("git checkout master")
Expand All @@ -180,16 +192,18 @@ def test__version__from_git__with_annotated_tags(tmp_path) -> None:
# bumping:
commit = run('dunamai from any --format "{commit}"')
assert run("dunamai from any --bump") == "0.2.1.dev1+{}".format(commit)
# tag with pre-release segment.
run("git tag v0.2.1b3 -m Annotated")
assert from_vcs() == Version("0.2.1", stage=("b", 3), dirty=False, branch=b)
if not legacy:
# tag with pre-release segment.
run("git tag v0.2.1b3 -m Annotated")
assert from_vcs() == Version("0.2.1", stage=("b", 3), dirty=False, branch=b)

# Additional one-off check: tag containing comma.
(vcs / "foo.txt").write_text("fourth")
run("git add .")
run('git commit --no-gpg-sign -m "Fourth"')
run("git tag v0.3.0+a,b -m Annotated")
assert from_vcs() == Version("0.3.0", dirty=False, tagged_metadata="a,b", branch=b)
if not legacy:
# Additional one-off check: tag containing comma.
(vcs / "foo.txt").write_text("fourth")
run("git add .")
run('git commit --no-gpg-sign -m "Fourth"')
run("git tag v0.3.0+a,b -m Annotated")
assert from_vcs() == Version("0.3.0", dirty=False, tagged_metadata="a,b", branch=b)


@pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git")
Expand All @@ -199,6 +213,7 @@ def test__version__from_git__with_lightweight_tags(tmp_path) -> None:
run = make_run_callback(vcs)
from_vcs = make_from_callback(Version.from_git)
b = "master"
legacy = is_git_legacy()

with chdir(vcs):
run("git init")
Expand Down Expand Up @@ -231,9 +246,10 @@ def test__version__from_git__with_lightweight_tags(tmp_path) -> None:
assert from_vcs() == Version("0.2.0", dirty=False, branch=b)
assert from_vcs(latest_tag=True) == Version("0.2.0", dirty=False, branch=b)

run("git checkout v0.1.1")
assert from_vcs() == Version("0.1.1", dirty=False)
assert from_vcs(latest_tag=True) == Version("0.1.1", dirty=False)
if not legacy:
run("git checkout v0.1.1")
assert from_vcs() == Version("0.1.1", dirty=False)
assert from_vcs(latest_tag=True) == Version("0.1.1", dirty=False)


@pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git")
Expand Down Expand Up @@ -277,6 +293,7 @@ def test__version__from_git__with_nonchronological_commits(tmp_path) -> None:
run = make_run_callback(vcs)
from_vcs = make_from_callback(Version.from_git, chronological=False)
b = "master"
legacy = is_git_legacy()

with chdir(vcs):
run("git init")
Expand Down Expand Up @@ -305,10 +322,14 @@ def test__version__from_git__with_nonchronological_commits(tmp_path) -> None:
)

run("git tag v0.2.0")
assert from_vcs() == Version("0.2.0", dirty=False, branch=b)
if legacy:
assert from_vcs() == Version("0.1.0", distance=1, dirty=False, branch=b)
else:
assert from_vcs() == Version("0.2.0", dirty=False, branch=b)


@pytest.mark.skipif(shutil.which("git") is None, reason="Requires Git")
@pytest.mark.skipif(is_git_legacy(), reason="Requires non-legacy Git")
def test__version__from_git__gitflow(tmp_path) -> None:
vcs = tmp_path / "dunamai-git-gitflow"
vcs.mkdir()
Expand Down

0 comments on commit c1c96a9

Please sign in to comment.