diff --git a/.github/workflows/test_fast.yml b/.github/workflows/test_fast.yml
index 5b4329842af..c785b33f98f 100644
--- a/.github/workflows/test_fast.yml
+++ b/.github/workflows/test_fast.yml
@@ -66,6 +66,9 @@ jobs:
run: |
pip install ."[all]"
+ - name: Configure git # Needed by the odd test
+ uses: cylc/release-actions/configure-git@v1
+
- name: style
run: |
pycodestyle
diff --git a/.github/workflows/test_functional.yml b/.github/workflows/test_functional.yml
index c1b18d5b588..f10f93b7b45 100644
--- a/.github/workflows/test_functional.yml
+++ b/.github/workflows/test_functional.yml
@@ -159,6 +159,9 @@ jobs:
sleep 1
ssh -vv _remote_background_indep_poll hostname
+ - name: Configure git # Needed by the odd test
+ uses: cylc/release-actions/configure-git@v1
+
- name: Filter Tests
env:
# NOTE: we only want the CHUNK set in this step else we will
diff --git a/CHANGES.md b/CHANGES.md
index da7a1a4d3e7..c2b5015c49e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -50,6 +50,10 @@ ones in. -->
-------------------------------------------------------------------------------
## __cylc-8.0b2 (Released 2021-??-??)__
+Third beta release of Cylc 8.
+
+(See note on cylc-8 backward-incompatible changes, above)
+
### Enhancements
[#4174](https://github.com/cylc/cylc-flow/pull/4174) - Terminology: replace
@@ -60,6 +64,9 @@ configuration items from `global.cylc[platforms][]`:
`run directory`, `work directory` and `suite definition directory`. This
functionality is now provided by `[symlink dirs]`.
+[#4142](https://github.com/cylc/cylc-flow/pull/4142) - Record source directory
+version control information on installation of a workflow.
+
### Fixes
[#4180](https://github.com/cylc/cylc-flow/pull/4180) - Fix bug where installing
@@ -71,6 +78,10 @@ of other, small bugs.
-------------------------------------------------------------------------------
## __cylc-8.0b1 (Released 2021-04-21)__
+Second beta release of Cylc 8.
+
+(See note on cylc-8 backward-incompatible changes, above)
+
### Enhancements
[#4154](https://github.com/cylc/cylc-flow/pull/4154) -
diff --git a/cylc/flow/install_plugins/__init__.py b/cylc/flow/install_plugins/__init__.py
new file mode 100644
index 00000000000..3c762247838
--- /dev/null
+++ b/cylc/flow/install_plugins/__init__.py
@@ -0,0 +1,32 @@
+# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
+# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Plugins for running Python code before and after installation of a workflow.
+
+Built In Plugins
+----------------
+
+Cylc Flow provides the following pre-configure and post-install plugins:
+
+.. autosummary::
+ :toctree: built-in
+ :template: docstring_only.rst
+
+ cylc.flow.install_plugins.log_vc_info
+
+.. Note: Autosummary generates files in this directory, these are cleaned
+ up by `make clean`.
+"""
diff --git a/cylc/flow/install_plugins/log_vc_info.py b/cylc/flow/install_plugins/log_vc_info.py
new file mode 100644
index 00000000000..70058802963
--- /dev/null
+++ b/cylc/flow/install_plugins/log_vc_info.py
@@ -0,0 +1,317 @@
+# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
+# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Record version control information to the workflow log directory on
+installation.
+
+If the workflow source directory is a supported repository/working copy
+(git or svn), information about the working copy will be saved in
+``/log/version/vcs.conf``.
+
+An example of this information for a git repo:
+
+.. code-block:: cylc
+
+ version control system = "git"
+ repository version = "2.8.0-dirty"
+ commit = "e5dc6573dd70cabd8f973d1535c17c29c026d553"
+ working copy root path = "~/cylc-src/my-workflow-git"
+ status = \"\"\"
+ M flow.cylc
+ \"\"\"
+
+And for an svn working copy:
+
+.. code-block:: cylc
+
+ version control system = "svn"
+ working copy root path = "~/cylc-src/my-workflow-svn"
+ url = "file:///home/my-workflow-svn/trunk"
+ repository uuid = "219f5687-8eb8-44b1-beb6-e8220fa964d3"
+ revision = "14"
+ status = \"\"\"
+ M flow.cylc
+ \"\"\"
+
+
+Any uncommitted changes will also be saved as a diff in
+``/log/version/uncommitted.diff``. (Note that git does not include
+untracked files in the diff.)
+"""
+
+from collections import OrderedDict
+from pathlib import Path
+from subprocess import Popen, DEVNULL, PIPE
+from typing import Dict, Iterable, List, Optional, TYPE_CHECKING, Union
+
+from cylc.flow import LOG
+from cylc.flow.exceptions import CylcError
+from cylc.flow.workflow_files import WorkflowFiles
+
+if TYPE_CHECKING:
+ from optparse import Values
+
+
+SVN = 'svn'
+GIT = 'git'
+
+INFO_COMMANDS: Dict[str, List[str]] = {
+ SVN: ['info', '--non-interactive'],
+ GIT: ['describe', '--always', '--dirty']
+}
+
+# git ['show', '--quiet', '--format=short'],
+
+STATUS_COMMANDS: Dict[str, List[str]] = {
+ SVN: ['status', '--non-interactive'],
+ GIT: ['status', '--short']
+}
+
+DIFF_COMMANDS: Dict[str, List[str]] = {
+ SVN: ['diff', '--internal-diff', '--non-interactive'],
+ GIT: ['diff', 'HEAD']
+ # ['diff', '--no-index', '/dev/null', '{0}'] # untracked files
+}
+
+GIT_REV_PARSE_COMMAND: List[str] = ['rev-parse', 'HEAD']
+
+NOT_REPO_ERRS: Dict[str, List[str]] = {
+ SVN: ['svn: e155007:',
+ 'svn: warning: w155007:'],
+ GIT: ['fatal: not a git repository',
+ 'warning: not a git repository']
+}
+
+NO_BASE_ERRS: Dict[str, List[str]] = {
+ SVN: [], # Not possible for svn working copy to have no base commit?
+ GIT: ['fatal: bad revision \'head\'',
+ 'fatal: ambiguous argument \'head\': unknown revision']
+}
+
+SVN_INFO_KEYS: List[str] = [
+ 'revision', 'url', 'working copy root path', 'repository uuid'
+]
+
+
+LOG_VERSION_DIR = Path(WorkflowFiles.LOG_DIR, 'version')
+
+
+class VCSNotInstalledError(CylcError):
+ """Exception to be raised if an attempted VCS command is not installed.
+
+ Args:
+ vcs: The version control system command.
+ exc: The exception that was raised when attempting to run the command.
+ """
+ def __init__(self, vcs: str, exc: Exception) -> None:
+ self.vcs = vcs
+ self.exc = exc
+
+ def __str__(self) -> str:
+ return f"{self.vcs} does not appear to be installed ({self.exc})"
+
+
+class VCSMissingBaseError(CylcError):
+ """Exception to be raised if a repository is missing a base commit.
+
+ Args:
+ vcs: The version control system command.
+ repo_path: The path to the working copy.
+ """
+ def __init__(self, vcs: str, repo_path: Union[Path, str]) -> None:
+ self.vcs = vcs
+ self.path = repo_path
+
+ def __str__(self) -> str:
+ return f"{self.vcs} repository at {self.path} is missing a base commit"
+
+
+def get_vc_info(path: Union[Path, str]) -> Optional['OrderedDict[str, str]']:
+ """Return the version control information for a repository, given its path.
+ """
+ info = OrderedDict()
+ missing_base = False
+ for vcs, args in INFO_COMMANDS.items():
+ try:
+ out = _run_cmd(vcs, args, cwd=path)
+ except VCSNotInstalledError as exc:
+ LOG.debug(exc)
+ continue
+ except VCSMissingBaseError as exc:
+ missing_base = True
+ LOG.debug(exc)
+ except OSError as exc:
+ if any(exc.strerror.lower().startswith(err)
+ for err in NOT_REPO_ERRS[vcs]):
+ LOG.debug(f"Source dir {path} is not a {vcs} repository")
+ continue
+ else:
+ raise exc
+
+ info['version control system'] = vcs
+ if vcs == SVN:
+ info.update(_parse_svn_info(out))
+ elif vcs == GIT:
+ if not missing_base:
+ info['repository version'] = out.splitlines()[0]
+ info['commit'] = _get_git_commit(path)
+ info['working copy root path'] = str(path)
+ info['status'] = get_status(vcs, path)
+
+ LOG.debug(f"{vcs} repository detected")
+ return info
+
+ return None
+
+
+def _run_cmd(vcs: str, args: Iterable[str], cwd: Union[Path, str]) -> str:
+ """Run a command, return stdout.
+
+ Args:
+ vcs: The version control system.
+ args: The args to pass to the version control command.
+ cwd: Directory to run the command in.
+
+ Raises:
+ OSError: with stderr if non-zero return code.
+ """
+ cmd = [vcs, *args]
+ try:
+ proc = Popen(
+ cmd, cwd=cwd, stdin=DEVNULL, stdout=PIPE, stderr=PIPE, text=True)
+ except FileNotFoundError as exc:
+ # This will only be raised if the VCS command is not installed,
+ # otherwise Popen() will succeed with a non-zero return code
+ raise VCSNotInstalledError(vcs, exc)
+ ret_code = proc.wait()
+ out, err = proc.communicate()
+ if ret_code:
+ if any(err.lower().startswith(msg)
+ for msg in NO_BASE_ERRS[vcs]):
+ # No base commit in repo
+ raise VCSMissingBaseError(vcs, cwd)
+ raise OSError(ret_code, err)
+ return out
+
+
+def write_vc_info(
+ info: 'OrderedDict[str, str]', run_dir: Union[Path, str]
+) -> None:
+ """Write version control info to the workflow's vcs log dir.
+
+ Args:
+ info: The vcs info.
+ run_dir: The workflow run directory.
+ """
+ if not info:
+ raise ValueError("Nothing to write")
+ info_file = Path(run_dir, LOG_VERSION_DIR, 'vcs.conf')
+ info_file.parent.mkdir(exist_ok=True)
+ with open(info_file, 'w') as f:
+ for key, value in info.items():
+ if key == 'status':
+ f.write(f"{key} = \"\"\"\n")
+ f.write(f"{value}\n")
+ f.write("\"\"\"\n")
+ else:
+ f.write(f"{key} = \"{value}\"\n")
+
+
+def _get_git_commit(path: Union[Path, str]) -> str:
+ """Return the hash of the HEAD of the repo at path."""
+ args = GIT_REV_PARSE_COMMAND
+ return _run_cmd(GIT, args, cwd=path).splitlines()[0]
+
+
+def get_status(vcs: str, path: Union[Path, str]) -> str:
+ """Return the short status of a repo.
+
+ Args:
+ vcs: The version control system.
+ path: The path to the repository.
+ """
+ args = STATUS_COMMANDS[vcs]
+ return _run_cmd(vcs, args, cwd=path).rstrip('\n')
+
+
+def _parse_svn_info(info_text: str) -> 'OrderedDict[str, str]':
+ """Return OrderedDict of certain info parsed from svn info raw output."""
+ ret = OrderedDict()
+ for line in info_text.splitlines():
+ if line:
+ key, value = (ln.strip() for ln in line.split(':', 1))
+ key = key.lower()
+ if key in SVN_INFO_KEYS:
+ ret[key] = value
+ return ret
+
+
+def get_diff(vcs: str, path: Union[Path, str]) -> Optional[str]:
+ """Return the diff of uncommitted changes for a repository.
+
+ Args:
+ vcs: The version control system.
+ path: The path to the repo.
+ """
+ args = DIFF_COMMANDS[vcs]
+ try:
+ diff = _run_cmd(vcs, args, cwd=path)
+ except (VCSNotInstalledError, VCSMissingBaseError):
+ return None
+ header = (
+ "# Auto-generated diff of uncommitted changes in the Cylc "
+ "workflow repository:\n"
+ f"# {path}")
+ return f"{header}\n{diff}"
+
+
+def write_diff(diff: str, run_dir: Union[Path, str]) -> None:
+ """Write a diff to the workflow's vcs log dir.
+
+ Args:
+ diff: The diff.
+ run_dir: The workflow run directory.
+ """
+ diff_file = Path(run_dir, LOG_VERSION_DIR, 'uncommitted.diff')
+ diff_file.parent.mkdir(exist_ok=True)
+ with open(diff_file, 'w') as f:
+ f.write(diff)
+
+
+# Entry point:
+def main(
+ srcdir: Union[Path, str], opts: 'Values', rundir: Union[Path, str]
+) -> bool:
+ """Entry point for this plugin. Write version control info and any
+ uncommmited diff to the workflow log dir.
+
+ Args:
+ srcdir: Workflow source dir for cylc install.
+ opts: CLI options (requirement for post_install entry point, but
+ not used here)
+ rundir: Workflow run dir.
+
+ Return True if source dir is a supported repo, else False.
+ """
+ vc_info = get_vc_info(srcdir)
+ if vc_info is None:
+ return False
+ vcs = vc_info['version control system']
+ diff = get_diff(vcs, srcdir)
+ write_vc_info(vc_info, rundir)
+ if diff is not None:
+ write_diff(diff, rundir)
+ return True
diff --git a/cylc/flow/main_loop/__init__.py b/cylc/flow/main_loop/__init__.py
index 44a6cd2b35b..3481da87714 100644
--- a/cylc/flow/main_loop/__init__.py
+++ b/cylc/flow/main_loop/__init__.py
@@ -39,7 +39,7 @@
Main loop plugins can be activated either by:
-* Using the ``-main-loop`` option with ``cylc play`` e.g:
+* Using the ``--main-loop`` option with ``cylc play`` e.g:
.. code-block:: console
diff --git a/setup.cfg b/setup.cfg
index ccc9aaf29c5..1a4d5d8805d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -124,6 +124,7 @@ cylc.main_loop =
log_memory = cylc.flow.main_loop.log_memory
prune_flow_labels = cylc.flow.main_loop.prune_flow_labels
# NOTE: all entry points should be listed here even if Cylc Flow does not
-# provide any implementations to make entry point scraping easier
+# provide any implementations, to make entry point scraping easier
cylc.pre_configure =
cylc.post_install =
+ log_vc_info = cylc.flow.install_plugins.log_vc_info:main
diff --git a/tests/functional/lib/bash/test_header b/tests/functional/lib/bash/test_header
index 41becf231ff..f97f7dce120 100644
--- a/tests/functional/lib/bash/test_header
+++ b/tests/functional/lib/bash/test_header
@@ -124,7 +124,7 @@
# Install a reference workflow using `install_workflow`, run a validation
# test on the workflow and run the reference workflow with `workflow_run_ok`.
# Expect 2 OK tests.
-#
+#
# create_test_global_config [PRE [POST]]
# Create a new global config file $PWD/etc from global-tests.cylc
# with PRE and POST pre- and ap-pended (PRE for e.g. jinja2 shebang).
@@ -501,7 +501,7 @@ log_scan () {
make_rnd_workflow() {
# Create a randomly-named workflow source directory.
# Define its run directory.
- RND_WORKFLOW_NAME=x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)
+ RND_WORKFLOW_NAME="cylctb-x$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c6)"
RND_WORKFLOW_SOURCE="$PWD/${RND_WORKFLOW_NAME}"
mkdir -p "${RND_WORKFLOW_SOURCE}"
touch "${RND_WORKFLOW_SOURCE}/flow.cylc"
@@ -566,6 +566,10 @@ purge () {
bash -c "$CMD" 2>/dev/null || true
}
purge_rnd_workflow() {
+ if ((FAILURES != 0)); then
+ # if tests have failed then don't clean up
+ return 0
+ fi
# Remove the workflow source created by make_rnd_workflow().
# And remove its run-directory too.
rm -rf "${RND_WORKFLOW_SOURCE}"
diff --git a/tests/functional/post-install/00-log-vc-info.t b/tests/functional/post-install/00-log-vc-info.t
new file mode 100644
index 00000000000..c8d8e1a89df
--- /dev/null
+++ b/tests/functional/post-install/00-log-vc-info.t
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
+# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#-------------------------------------------------------------------------------
+# Test logging of source dir version control information occurs post install
+
+. "$(dirname "$0")/test_header"
+if ! command -v 'git' > /dev/null; then
+ skip_all 'git not installed'
+fi
+set_test_number 4
+
+make_rnd_workflow
+cd "${RND_WORKFLOW_SOURCE}" || exit 1
+cat > 'flow.cylc' << __FLOW__
+[scheduling]
+ [[graph]]
+ R1 = foo
+__FLOW__
+
+git init
+git add 'flow.cylc'
+git commit -am 'Initial commit'
+
+run_ok "${TEST_NAME_BASE}-install" cylc install
+
+VCS_INFO_FILE="${RND_WORKFLOW_RUNDIR}/runN/log/version/vcs.conf"
+exists_ok "$VCS_INFO_FILE"
+# Basic check, unit tests cover this in more detail:
+contains_ok "$VCS_INFO_FILE" <<< 'version control system = "git"'
+
+DIFF_FILE="${RND_WORKFLOW_RUNDIR}/runN/log/version/uncommitted.diff"
+exists_ok "$DIFF_FILE" # Expected to be empty but should exist
+
+purge_rnd_workflow
diff --git a/tests/functional/post-install/test_header b/tests/functional/post-install/test_header
new file mode 120000
index 00000000000..90bd5a36f92
--- /dev/null
+++ b/tests/functional/post-install/test_header
@@ -0,0 +1 @@
+../lib/bash/test_header
\ No newline at end of file
diff --git a/tests/unit/post_install/test_log_vc_info.py b/tests/unit/post_install/test_log_vc_info.py
new file mode 100644
index 00000000000..987e379853d
--- /dev/null
+++ b/tests/unit/post_install/test_log_vc_info.py
@@ -0,0 +1,229 @@
+# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
+# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from pathlib import Path
+import pytest
+from pytest import MonkeyPatch, TempPathFactory
+import shutil
+import subprocess
+from typing import Any, Tuple
+from unittest.mock import Mock
+
+from cylc.flow.install_plugins.log_vc_info import (
+ get_diff, _get_git_commit, get_status, get_vc_info, main
+)
+
+Fixture = Any
+
+
+BASIC_FLOW_1 = """
+[scheduling]
+ [[graph]]
+ R1 = foo
+"""
+
+BASIC_FLOW_2 = """
+[scheduling]
+ [[graph]]
+ R1 = bar
+"""
+
+
+require_git = pytest.mark.skipif(
+ shutil.which('git') is None,
+ reason="git is not installed"
+)
+
+require_svn = pytest.mark.skipif(
+ shutil.which('svn') is None,
+ reason="svn is not installed"
+)
+
+
+@pytest.fixture(scope='module')
+def git_source_repo(tmp_path_factory: TempPathFactory) -> Tuple[str, str]:
+ """Init a git repo for a workflow source dir.
+
+ The repo has a flow.cylc file with uncommitted changes. This dir is reused
+ by all tests requesting it in this module.
+
+ Returns (source_dir_path, commit_hash)
+ """
+ source_dir: Path = tmp_path_factory.getbasetemp().joinpath('git_repo')
+ source_dir.mkdir()
+ subprocess.run(['git', 'init'], cwd=source_dir, check=True)
+ flow_file = source_dir.joinpath('flow.cylc')
+ flow_file.write_text(BASIC_FLOW_1)
+ subprocess.run(['git', 'add', '-A'], cwd=source_dir, check=True)
+ subprocess.run(
+ ['git', 'commit', '-am', '"Initial commit"'],
+ cwd=source_dir, check=True, capture_output=True)
+ # Overwrite file to introduce uncommitted changes:
+ flow_file.write_text(BASIC_FLOW_2)
+ commit_sha = subprocess.run(
+ ['git', 'rev-parse', 'HEAD'],
+ cwd=source_dir, check=True, capture_output=True, text=True
+ ).stdout.splitlines()[0]
+ return (str(source_dir), commit_sha)
+
+
+@pytest.fixture(scope='module')
+def svn_source_repo(tmp_path_factory: TempPathFactory) -> Tuple[str, str, str]:
+ """Init an svn repo & working copy for a workflow source dir.
+
+ The working copy has a flow.cylc file with uncommitted changes. This dir
+ is reused by all tests requesting it in this module.
+
+ Returns (source_dir_path, repository_UUID, repository_path)
+ """
+ tmp_path: Path = tmp_path_factory.getbasetemp()
+ repo = tmp_path.joinpath('svn_repo')
+ subprocess.run(
+ ['svnadmin', 'create', 'svn_repo'], cwd=tmp_path, check=True)
+ uuid = subprocess.run(
+ ['svnlook', 'uuid', repo],
+ check=True, capture_output=True, text=True
+ ).stdout.splitlines()[0]
+ project_dir = tmp_path.joinpath('project')
+ project_dir.mkdir()
+ project_dir.joinpath('flow.cylc').write_text(BASIC_FLOW_1)
+ subprocess.run(
+ ['svn', 'import', project_dir, f'file://{repo}/project/trunk',
+ '-m', '"Initial import"'], check=True)
+ source_dir = tmp_path.joinpath('svn_working_copy')
+ subprocess.run(
+ ['svn', 'checkout', f'file://{repo}/project/trunk', source_dir],
+ check=True)
+
+ flow_file = source_dir.joinpath('flow.cylc')
+ # Overwrite file to introduce uncommitted changes:
+ flow_file.write_text(BASIC_FLOW_2)
+
+ return (str(source_dir), uuid, str(repo))
+
+
+@require_git
+def test_get_git_commit(git_source_repo: Tuple[str, str]):
+ """Test get_git_commit()"""
+ source_dir, commit_sha = git_source_repo
+ assert _get_git_commit(source_dir) == commit_sha
+
+
+@require_git
+def test_get_status_git(git_source_repo: Tuple[str, str]):
+ """Test get_status() for a git repo"""
+ source_dir, commit_sha = git_source_repo
+ assert get_status('git', source_dir) == " M flow.cylc"
+
+
+@require_git
+def test_get_vc_info_git(git_source_repo: Tuple[str, str]):
+ """Test get_vc_info() for a git repo"""
+ source_dir, commit_sha = git_source_repo
+ vc_info = get_vc_info(source_dir)
+ assert vc_info is not None
+ expected = [
+ ('version control system', "git"),
+ ('repository version', f"{commit_sha[:7]}-dirty"),
+ ('commit', commit_sha),
+ ('working copy root path', source_dir),
+ ('status', " M flow.cylc")
+ ]
+ assert list(vc_info.items()) == expected
+
+
+@require_git
+def test_get_diff_git(git_source_repo: Tuple[str, str]):
+ """Test get_diff() for a git repo"""
+ source_dir, commit_sha = git_source_repo
+ diff = get_diff('git', source_dir)
+ assert diff is not None
+ diff_lines = diff.splitlines()
+ for line in ("diff --git a/flow.cylc b/flow.cylc",
+ "- R1 = foo",
+ "+ R1 = bar"):
+ assert line in diff_lines
+
+
+@require_svn
+def test_get_vc_info_svn(svn_source_repo: Tuple[str, str, str]):
+ """Test get_vc_info() for an svn working copy"""
+ source_dir, uuid, repo_path = svn_source_repo
+ vc_info = get_vc_info(source_dir)
+ assert vc_info is not None
+ expected = [
+ ('version control system', "svn"),
+ ('working copy root path', str(source_dir)),
+ ('url', f"file://{repo_path}/project/trunk"),
+ ('repository uuid', uuid),
+ ('revision', "1"),
+ ('status', "M flow.cylc")
+ ]
+ assert list(vc_info.items()) == expected
+
+
+@require_svn
+def test_get_diff_svn(svn_source_repo: Tuple[str, str, str]):
+ """Test get_diff() for an svn working copy"""
+ source_dir, uuid, repo_path = svn_source_repo
+ diff = get_diff('svn', source_dir)
+ assert diff is not None
+ diff_lines = diff.splitlines()
+ for line in ("--- flow.cylc (revision 1)",
+ "+++ flow.cylc (working copy)",
+ "- R1 = foo",
+ "+ R1 = bar"):
+ assert line in diff_lines
+
+
+def test_not_repo(tmp_path: Path, monkeypatch: MonkeyPatch):
+ """Test get_vc_info() and main() for a dir that is not a supported repo"""
+ source_dir = Path(tmp_path, 'git_repo')
+ source_dir.mkdir()
+ flow_file = source_dir.joinpath('flow.cylc')
+ flow_file.write_text(BASIC_FLOW_1)
+ mock_write_vc_info = Mock()
+ monkeypatch.setattr('cylc.flow.install_plugins.log_vc_info.write_vc_info',
+ mock_write_vc_info)
+ mock_write_diff = Mock()
+ monkeypatch.setattr('cylc.flow.install_plugins.log_vc_info.write_diff',
+ mock_write_diff)
+
+ assert get_vc_info(source_dir) is None
+ assert main(source_dir, None, None) is False # type: ignore
+ assert mock_write_vc_info.called is False
+ assert mock_write_diff.called is False
+
+
+@require_git
+def test_no_base_commit_git(tmp_path: Path):
+ """Test get_vc_info() and get_diff() for a recently init'd git source dir
+ that does not have a base commit yet."""
+ source_dir = Path(tmp_path, 'new_git_repo')
+ source_dir.mkdir()
+ subprocess.run(['git', 'init'], cwd=source_dir, check=True)
+ flow_file = source_dir.joinpath('flow.cylc')
+ flow_file.write_text(BASIC_FLOW_1)
+
+ vc_info = get_vc_info(source_dir)
+ assert vc_info is not None
+ expected = [
+ ('version control system', "git"),
+ ('working copy root path', str(source_dir)),
+ ('status', "?? flow.cylc")
+ ]
+ assert list(vc_info.items()) == expected
+ assert get_diff('git', source_dir) is None