Skip to content

Commit

Permalink
new git_excluded feature (#15457)
Browse files Browse the repository at this point in the history
* new git_excluded feature

* review

* added via conf
  • Loading branch information
memsharded authored Jan 22, 2024
1 parent fa991f3 commit 4c0e9c8
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 14 deletions.
25 changes: 23 additions & 2 deletions conan/tools/scm/git.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import fnmatch
import os

from conan.tools.files import chdir
from conan.errors import ConanException
from conans.model.conf import ConfDefinition
from conans.util.files import mkdir
from conans.util.runners import check_output_runner

Expand All @@ -10,13 +12,24 @@ class Git:
"""
Git is a wrapper for several common patterns used with *git* tool.
"""
def __init__(self, conanfile, folder="."):
def __init__(self, conanfile, folder=".", excluded=None):
"""
:param conanfile: Conanfile instance.
:param folder: Current directory, by default ``.``, the current working directory.
"""
self._conanfile = conanfile
self.folder = folder
self._excluded = excluded
global_conf = conanfile._conan_helpers.global_conf
conf_excluded = global_conf.get("core.scm:excluded", check_type=list)
if conf_excluded:
if excluded:
c = ConfDefinition()
c.loads(f"core.scm:excluded={excluded}")
c.update_conf_definition(global_conf)
self._excluded = c.get("core.scm:excluded", check_type=list)
else:
self._excluded = conf_excluded

def run(self, cmd):
"""
Expand Down Expand Up @@ -107,7 +120,15 @@ def is_dirty(self):
:return: True, if the current folder is dirty. Otherwise, False.
"""
status = self.run("status . --short --no-branch --untracked-files").strip()
return bool(status)
self._conanfile.output.debug(f"Git status:\n{status}")
if not self._excluded:
return bool(status)
# Parse the status output, line by line, and match it with "_excluded"
lines = [line.strip() for line in status.splitlines()]
lines = [line.split()[1] for line in lines if line]
lines = [line for line in lines if not any(fnmatch.fnmatch(line, p) for p in self._excluded)]
self._conanfile.output.debug(f"Filtered git status: {lines}")
return bool(lines)

def get_url_and_commit(self, remote="origin", repository=False):
"""
Expand Down
22 changes: 12 additions & 10 deletions conans/client/cmd/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

from conan.tools.files import copy
from conan.api.output import ConanOutput
from conan.tools.scm import Git
from conans.errors import ConanException, conanfile_exception_formatter
from conans.model.manifest import FileTreeManifest
from conans.model.recipe_ref import RecipeReference
from conans.paths import DATA_YML
from conans.util.files import is_dirty, rmdir, set_dirty, mkdir, clean_dirty, chdir
from conans.util.runners import check_output_runner


def cmd_export(app, global_conf, conanfile_path, name, version, user, channel, graph_lock=None,
Expand Down Expand Up @@ -61,7 +61,8 @@ def cmd_export(app, global_conf, conanfile_path, name, version, user, channel, g
revision = _calc_revision(scoped_output=conanfile.output,
path=os.path.dirname(conanfile_path),
manifest=manifest,
revision_mode=conanfile.revision_mode)
revision_mode=conanfile.revision_mode,
conanfile=conanfile)

ref.revision = revision
recipe_layout.reference = ref
Expand All @@ -86,27 +87,28 @@ def cmd_export(app, global_conf, conanfile_path, name, version, user, channel, g
return ref, conanfile


def _calc_revision(scoped_output, path, manifest, revision_mode):
def _calc_revision(scoped_output, path, manifest, revision_mode, conanfile):
if revision_mode not in ["scm", "scm_folder", "hash"]:
raise ConanException("Revision mode should be one of 'hash' (default) or 'scm'")

# Use the proper approach depending on 'revision_mode'
if revision_mode == "hash":
revision = manifest.summary_hash
else:
f = '-- "."' if revision_mode == "scm_folder" else ""
# Exception to the rule that tools should only be used in recipes, this Git helper is ok
excluded = getattr(conanfile, "revision_mode_excluded", None)
git = Git(conanfile, folder=path, excluded=excluded)
try:
with chdir(path):
revision = check_output_runner(f'git rev-list HEAD -n 1 --full-history {f}').strip()
revision = git.get_commit(repository=(revision_mode == "scm"))
except Exception as exc:
error_msg = "Cannot detect revision using '{}' mode from repository at " \
"'{}'".format(revision_mode, path)
raise ConanException("{}: {}".format(error_msg, exc))

with chdir(path):
if bool(check_output_runner(f'git status -s {f}').strip()):
raise ConanException("Can't have a dirty repository using revision_mode='scm' and doing"
" 'conan export', please commit the changes and run again.")
if git.is_dirty():
raise ConanException("Can't have a dirty repository using revision_mode='scm' and doing"
" 'conan export', please commit the changes and run again, or "
"use 'git_excluded = []' attribute")

scoped_output.info("Using git commit as the recipe revision: %s" % revision)

Expand Down
2 changes: 2 additions & 0 deletions conans/model/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
"core.net.http:clean_system_proxy": "If defined, the proxies system env-vars will be discarded",
# Gzip compression
"core.gzip:compresslevel": "The Gzip compression level for Conan artifacts (default=9)",
# Excluded from revision_mode = "scm" dirty and Git().is_dirty() checks
"core.scm:excluded": "List of excluded patterns for builtin git dirty checks",
# Tools
"tools.android:ndk_path": "Argument for the CMAKE_ANDROID_NDK",
"tools.android:cmake_legacy_toolchain": "Define to explicitly pass ANDROID_USE_LEGACY_TOOLCHAIN_FILE in CMake toolchain",
Expand Down
29 changes: 29 additions & 0 deletions conans/test/functional/command/export_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from conans.test.assets.genconanfile import GenConanfile
from conans.test.utils.scm import git_add_changes_commit
from conans.test.utils.tools import TestClient
from conans.util.files import save


@pytest.mark.tool("git")
Expand Down Expand Up @@ -60,3 +61,31 @@ def test_auto_revision_without_commits(self):
t.run("export .", assert_error=True)
# It errors, because no commits yet
assert "Cannot detect revision using 'scm' mode from repository" in t.out

@pytest.mark.parametrize("conf_excluded, recipe_excluded",
[("", ["*.cpp", "*.txt", "src/*"]),
(["*.cpp", "*.txt", "src/*"], ""),
('+["*.cpp", "*.txt"]', ["src/*"]),
('+["*.cpp"]', ["*.txt", "src/*"])])
def test_revision_mode_scm_excluded_files(self, conf_excluded, recipe_excluded):
t = TestClient()
recipe_excluded = f'revision_mode_excluded = {recipe_excluded}' if recipe_excluded else ""
conf_excluded = f'core.scm:excluded={conf_excluded}' if conf_excluded else ""
save(t.cache.global_conf_path, conf_excluded)
conanfile = GenConanfile("pkg", "0.1").with_class_attribute('revision_mode = "scm"') \
.with_class_attribute(recipe_excluded)
commit = t.init_git_repo({'conanfile.py': str(conanfile),
"test.cpp": "mytest"})

t.run(f"export .")
assert t.exported_recipe_revision() == commit

t.save({"test.cpp": "mytest2",
"new.txt": "new",
"src/potato": "hello"})
t.run(f"export . -vvv")
assert t.exported_recipe_revision() == commit

t.save({"test.py": ""})
t.run(f"export .", assert_error=True)
assert "ERROR: Can't have a dirty repository using revision_mode='scm'" in t.out
27 changes: 25 additions & 2 deletions conans/test/functional/tools/scm/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from conans.test.utils.scm import create_local_git_repo, git_add_changes_commit, git_create_bare_repo
from conans.test.utils.test_files import temp_folder
from conans.test.utils.tools import TestClient
from conans.util.files import rmdir, save_files
from conans.util.files import rmdir, save_files, save


@pytest.mark.tool("git")
Expand All @@ -26,7 +26,7 @@ class Pkg(ConanFile):
version = "0.1"
def export(self):
git = Git(self, self.recipe_folder)
git = Git(self, self.recipe_folder, excluded=["myfile.txt", "mynew.txt"])
commit = git.get_commit()
repo_commit = git.get_commit(repository=True)
url = git.get_remote_url()
Expand Down Expand Up @@ -116,6 +116,29 @@ def test_capture_commit_local_subfolder(self):
assert "pkg/0.1: COMMIT IN REMOTE: False" in c.out
assert "pkg/0.1: DIRTY: False" in c.out

def test_git_excluded(self):
"""
A local repo, without remote, will have commit, but no URL
"""
c = TestClient()
c.save({"conanfile.py": self.conanfile,
"myfile.txt": ""})
c.init_git_repo()
c.run("export . -vvv")
assert "pkg/0.1: DIRTY: False" in c.out
c.save({"myfile.txt": "changed",
"mynew.txt": "new"})
c.run("export .")
assert "pkg/0.1: DIRTY: False" in c.out
c.save({"other.txt": "new"})
c.run("export .")
assert "pkg/0.1: DIRTY: True" in c.out

conf_excluded = f'core.scm:excluded+=["other.txt"]'
save(c.cache.global_conf_path, conf_excluded)
c.run("export .")
assert "pkg/0.1: DIRTY: False" in c.out


@pytest.mark.tool("git")
class TestGitCaptureSCM:
Expand Down

0 comments on commit 4c0e9c8

Please sign in to comment.