Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new git_excluded feature #15457

Merged
merged 5 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
memsharded marked this conversation as resolved.
Show resolved Hide resolved
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