From ca49ce1ef8dde5c970dc781e86fb9d8bac2f2617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20L=C3=A1t?= Date: Sun, 15 Sep 2024 16:41:55 +0200 Subject: [PATCH] Release 8.0.0 --- CHANGELOG.md | 26 ++++- README.md | 31 +++++- poetry.lock | 124 ++++++++++++++++++++- pyproject.toml | 15 ++- src/delfino_core/commands/issue_tracker.py | 42 +++++++ src/delfino_core/commands/typecheck.py | 2 +- src/delfino_core/commands/vcs.py | 8 +- src/delfino_core/config.py | 61 +++++++++- src/delfino_core/vcs_tools.py | 43 ++++++- 9 files changed, 326 insertions(+), 26 deletions(-) create mode 100644 src/delfino_core/commands/issue_tracker.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eb12bc..0ee5820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,29 @@ Types of changes are: ## [Unreleased] +## [8.0.0] - 2024-09-15 + +### Breaking changes + +- Configuration option `tool.delfino.plugins.delfino-core.branch_prefix` has been moved to `tool.delfino.plugins.delfino-core.vcs.branch_prefix`. +- Configuration option `tool.delfino.plugins.delfino-core.typecheck` has been moved to `tool.delfino.plugins.delfino-core.mypy`. + +### Features + +#### Integration with Jira issue tracker + +In the `vcs`/`gh`/`glab` `pr`/`mr` `start` commands, the integration attempts to fetch issue title. The resulting branch name will be set to `-/` and the first commit message will be set to ``. Use the issue number in place of the title to trigger it. For example `delfino mr start 123`. + +##### New configuration options under `tool.delfino.plugins.delfino-core.issue_tracking` +- `issue_prefix` - The issue prefix (including a trailing dash). +- `tracker_url` - The issue tracker URL. If not set, the integration will be disabled. +- `username_env_var` - The environment variable containing the username for the issue tracker. Defaults to `DELFINO_CORE_ISSUE_TRACKING_USERNAME`. +- `api_key_env_var` - The environment variable containing the API key for the issue tracker. Defaults to `DELFINO_CORE_ISSUE_TRACKING_API_KEY`. + +##### New optional install dependencies + +- `vcs` - Installs `httpx` used for fetching issue title. + ## [7.5.0] - 2024-08-02 ### Features @@ -484,7 +507,8 @@ If `tool.delfino.plugins.delfino-core.dockerhub` exists in the `pyproject.toml`: - Initial source code -[Unreleased]: https://github.com/radeklat/delfino-core/compare/7.5.0...HEAD +[Unreleased]: https://github.com/radeklat/delfino-core/compare/8.0.0...HEAD +[8.0.0]: https://github.com/radeklat/delfino-core/compare/7.5.0...8.0.0 [7.5.0]: https://github.com/radeklat/delfino-core/compare/7.4.6...7.5.0 [7.4.6]: https://github.com/radeklat/delfino-core/compare/7.4.5...7.4.6 [7.4.5]: https://github.com/radeklat/delfino-core/compare/7.4.4...7.4.5 diff --git a/README.md b/README.md index 05417c3..71a1527 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Using `[all]` installs all the [optional dependencies](https://setuptools.pypa.i - `format` - `dependencies-update` - `pre-commit` + - `vsc` - For groups of commands: - `test` - for testing and coverage commands - `lint` - for all the linting commands @@ -114,14 +115,13 @@ test_commands = ["pytest", "coverage-report"] # Do not install pre-commit if this is set to true. disable_pre_commit = false - -# Enable to manually specify the branch prefix. By default it is set to git username. -# branch_prefix = "" ``` ## Commands configuration -Several commands have their own configuration as well: +Several commands have their own configuration as well. + +### `mypy` ```toml [tool.delfino.plugins.delfino-core.mypy] @@ -129,6 +129,29 @@ Several commands have their own configuration as well: strict_directories = [] ``` +### `vcs` + +```toml +[tool.delfino.plugins.delfino-core.vcs] +# Enable to manually specify the branch prefix. By default it is set to git username. +# branch_prefix = "" + +[tool.delfino.plugins.delfino-core.vcs.issue_tracker] +# Prefix for issue numbers, including a trailing hyphen if used. If not set, just the issue numbers will be used. +# issue_prefix = "ISSUE-" + +# URL for the issue tracker. If not set, issue tracker integration will be disabled. +# Implemented trackers: Jira. +# tracker_url = "https://.atlassian.net" + +# Environment variable name for the issue tracking username. If not set, 'ISSUE_TRACKER_USERNAME' will be used by default. +# username_env_var = "" + +# Environment variable name for the issue tracking API key. If not set, 'ISSUE_TRACKER_API_KEY' will be used by default. +# api_key_env_var = "" +``` + + # Usage Run `delfino --help`. diff --git a/poetry.lock b/poetry.lock index d32785c..942d8ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,28 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} +[[package]] +name = "anyio" +version = "4.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "astroid" version = "3.2.3" @@ -74,6 +96,17 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -297,6 +330,63 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "identify" version = "2.6.0" @@ -311,6 +401,20 @@ files = [ [package.extras] license = ["ukkonen"] +[[package]] +name = "idna" +version = "3.9" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.9-py3-none-any.whl", hash = "sha256:69297d5da0cc9281c77efffb4e730254dd45943f45bbfb461de5991713989b1e"}, + {file = "idna-3.9.tar.gz", hash = "sha256:e5c5dafde284f26e9e0f28f6ea2d6400abd5ca099864a67f576f3981c6476124"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" version = "8.0.0" @@ -561,8 +665,8 @@ files = [ annotated-types = ">=0.4.0" pydantic-core = "2.20.1" typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, ] [package.extras] @@ -701,8 +805,8 @@ files = [ astroid = ">=3.2.2,<=3.3.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" @@ -895,6 +999,17 @@ files = [ {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -1041,16 +1156,17 @@ doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linke test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] -all = ["PyYAML", "black", "coverage", "gitpython", "isort", "mypy", "pre-commit", "psutil", "pycodestyle", "pydocstyle", "pylint", "pytest", "pytest-cov", "pyupgrade", "ruff"] +all = ["PyYAML", "black", "coverage", "gitpython", "httpx", "isort", "mypy", "pre-commit", "psutil", "pycodestyle", "pydocstyle", "pylint", "pytest", "pytest-cov", "pyupgrade", "ruff"] dependencies-update = ["gitpython"] format = ["black", "isort", "pre-commit", "pyupgrade"] lint = ["psutil", "pycodestyle", "pydocstyle", "pylint", "ruff"] mypy = ["mypy"] pre-commit = ["PyYAML"] test = ["coverage", "pytest", "pytest-cov"] +vcs = ["httpx"] verify = ["black", "coverage", "isort", "mypy", "pre-commit", "psutil", "pycodestyle", "pydocstyle", "pylint", "pytest", "pytest-cov", "pyupgrade", "ruff"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "f8270c1de67328e36e8963f2f9bc3668d06aabd997f0e2ba1d2367611c201729" +content-hash = "843bf637d4b0ff169fc4b7685dd0549aa1e34065038c021ae8c3d36ca7586d52" diff --git a/pyproject.toml b/pyproject.toml index 3440f86..e4f32bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name="delfino-core" -version="7.5.0" +version="8.0.0" authors = ["Radek Lát "] description="Delfino core plugin" license = "MIT License" @@ -43,6 +43,7 @@ shellingham = {version = "*", optional = true} psutil = {version = "*", optional = true} PyYAML = {version = "*", optional = true} ruff = {version = ">=0.5.0", optional = true} +httpx = {version = "*", optional = true} # https://python-poetry.org/docs/pyproject/#plugins [tool.poetry.plugins] @@ -67,19 +68,21 @@ psutil = "^6.0" gitpython = "^3.1" pyupgrade = "^3.7" ruff = "^0.5.2" +httpx = "^0.27.2" types-termcolor = "*" types-toml = "*" types-psutil = "*" types-pyyaml = "*" [tool.poetry.extras] -all = ["black", "isort", "pyupgrade", "pre-commit", "pytest", "coverage", "pytest-cov", "mypy", "pylint", "pycodestyle", "pydocstyle", "psutil", "gitpython", "PyYAML", "ruff"] +all = ["black", "isort", "pyupgrade", "pre-commit", "pytest", "coverage", "pytest-cov", "mypy", "pylint", "pycodestyle", "pydocstyle", "psutil", "gitpython", "PyYAML", "ruff", "httpx"] verify = ["black", "isort", "pyupgrade", "pre-commit", "pytest", "coverage", "pytest-cov", "mypy", "pylint", "pycodestyle", "pydocstyle", "psutil", "ruff"] format = ["black", "isort", "pyupgrade", "pre-commit"] test = ["pytest", "coverage", "pytest-cov"] mypy = ["mypy"] lint = ["pylint", "pycodestyle", "pydocstyle", "psutil", "ruff"] dependencies_update = ["gitpython"] +vcs = ["httpx"] pre_commit = ["PyYAML"] [tool.isort] @@ -155,5 +158,9 @@ add-ignore = [ [tool.ruff] line-length = 120 -[tool.delfino.plugins.local] -#branch_prefix = "" \ No newline at end of file +[tool.delfino.plugins.local.vcs] +#branch_prefix = "" + +[tool.delfino.plugins.local.issue_tracking] +issue_prefix = "PCAT-" +tracker_url = "https://heurekagroup.atlassian.net" \ No newline at end of file diff --git a/src/delfino_core/commands/issue_tracker.py b/src/delfino_core/commands/issue_tracker.py new file mode 100644 index 0000000..4035ca2 --- /dev/null +++ b/src/delfino_core/commands/issue_tracker.py @@ -0,0 +1,42 @@ +from abc import ABC, abstractmethod +from base64 import b64encode +from typing import Dict + +from click import Abort + +from delfino_core.config import IssueTrackingConfig + +try: + import httpx +except ImportError: + pass + + +class _BaseIssuerTrackerClient(ABC): + def __init__(self, settings: IssueTrackingConfig): + self._settings = settings + + @abstractmethod + def get_issue_title(self, issue_number: int) -> str: + """Fetches the title of an issue using its issue number.""" + + +class JiraClient(_BaseIssuerTrackerClient): + def _headers(self) -> Dict[str, str]: + token = b64encode(f"{self._settings.username}:{self._settings.api_key}".encode()).decode() + return { + "Accept": "application/json", + "Authorization": f"Basic {token}", + } + + def get_issue_title(self, issue_number: int) -> str: + issue_key = f"{self._settings.issue_prefix.rstrip('-')}-{issue_number}" + url = f"{self._settings.tracker_url.rstrip('/')}/rest/api/3/issue/{issue_key}" + + response = httpx.get(url, headers=self._headers()) + + if response.status_code == 200: + data = response.json() + return data["fields"]["summary"] + + raise Abort(f"Failed to fetch issue: {response.status_code}, {response.text}") diff --git a/src/delfino_core/commands/typecheck.py b/src/delfino_core/commands/typecheck.py index 7e20cc8..376f212 100644 --- a/src/delfino_core/commands/typecheck.py +++ b/src/delfino_core/commands/typecheck.py @@ -104,7 +104,7 @@ def run_mypy( folder for folder in app_context.pyproject_toml.tool.delfino.local_command_folders if folder.exists() ) - strict_paths = plugin_config.typecheck.strict_directories + strict_paths = plugin_config.mypy.strict_directories grouped_paths = groupby( target_paths, lambda current_path: is_path_relative_to_paths(current_path, strict_paths), diff --git a/src/delfino_core/commands/vcs.py b/src/delfino_core/commands/vcs.py index 64fc27e..438a266 100644 --- a/src/delfino_core/commands/vcs.py +++ b/src/delfino_core/commands/vcs.py @@ -16,6 +16,7 @@ get_new_branch_name_or_switch_to_branch, get_trunk_branch, get_vcs_cli_tool, + title_and_branch_prefix_from_issue_tracker, ) @@ -248,10 +249,11 @@ def _run_vcs_start( title, args = consume_args_until_next_option(args) if not vcs_cli_tool: vcs_cli_tool = get_vcs_cli_tool() - while not title: - title = input(f"Enter a title for the {'PR' if vcs_cli_tool == 'gh' else 'MR'}: ") - branch_name, create_branch = get_new_branch_name_or_switch_to_branch(app_context.plugin_config.branch_prefix, title) + title, branch_prefix = title_and_branch_prefix_from_issue_tracker( + vcs_cli_tool, title, app_context.plugin_config.vcs + ) + branch_name, create_branch = get_new_branch_name_or_switch_to_branch(branch_prefix, title) if create_branch: run(["git", "stash"], on_error=OnError.ABORT) diff --git a/src/delfino_core/config.py b/src/delfino_core/config.py index 7049ff6..098013f 100644 --- a/src/delfino_core/config.py +++ b/src/delfino_core/config.py @@ -1,15 +1,68 @@ +import logging +import os from pathlib import Path from typing import List, Optional, Tuple from delfino.decorators import pass_app_context from delfino.models.pyproject_toml import PluginConfig from pydantic import BaseModel, Field +from typing_extensions import Annotated +_LOG = logging.getLogger(__name__) -class Typecheck(BaseModel): + +class MypyConfig(BaseModel): strict_directories: List[Path] = [] +class IssueTrackingConfig(BaseModel): + _DEFAULT_API_KEY_ENV_VAR = "DELFINO_CORE_ISSUE_TRACKING_API_KEY" + _DEFAULT_USERNAME_ENV_VAR = "DELFINO_CORE_ISSUE_TRACKING_USERNAME" + + issue_prefix: str = Field( + "", + description="Prefix for issue numbers, including a trailing hyphen if used. " + "If not set, just the issue numbers will be used.", + ) + tracker_url: str = Field( + "", + description="URL for the issue tracker. If not set, issue tracker integration will be disabled." + "Implemented trackers: Jira.", + ) + username_env_var: str = Field( + _DEFAULT_USERNAME_ENV_VAR, + description=f"Environment variable name for the issue tracking username. " + f"If not set, '{_DEFAULT_USERNAME_ENV_VAR}' will be used by default.", + ) + api_key_env_var: str = Field( + _DEFAULT_API_KEY_ENV_VAR, + description=f"Environment variable name for the issue tracking API key. " + f"If not set, '{_DEFAULT_API_KEY_ENV_VAR}' will be used by default.", + ) + + @staticmethod + def _get_env_var(name: str, purpose: str) -> Optional[str]: + if (value := os.getenv(name)) is None: + _LOG.warning(f"{purpose} environment variable '{name}' is not set.") + + return value + + @property + def api_key(self) -> Optional[str]: + return self._get_env_var(self.api_key_env_var, "Issue tracking API key") + + @property + def username(self) -> Optional[str]: + return self._get_env_var(self.username_env_var, "Issue tracking username") + + +class VCSConfig(BaseModel): + branch_prefix: Optional[str] = Field( + None, description="Prefix for branch names. If not set, git username will be used." + ) + issue_tracking: Annotated[IssueTrackingConfig, Field(default_factory=IssueTrackingConfig)] + + class CorePluginConfig(PluginConfig): sources_directory: Path = Path("src") tests_directory: Path = Path("tests") @@ -21,10 +74,8 @@ class CorePluginConfig(PluginConfig): format_commands: Tuple[str, ...] = ("ensure-pre-commit", "pyupgrade", "isort", "black") test_commands: Tuple[str, ...] = ("pytest", "coverage-report") disable_pre_commit: bool = False - typecheck: Typecheck = Field(default_factory=Typecheck) - branch_prefix: Optional[str] = Field( - None, description="Prefix for branch names. If not set, git username will be used." - ) + mypy: Annotated[MypyConfig, Field(default_factory=MypyConfig)] + vcs: Annotated[VCSConfig, Field(default_factory=VCSConfig)] pass_plugin_app_context = pass_app_context(plugin_config_type=CorePluginConfig) diff --git a/src/delfino_core/vcs_tools.py b/src/delfino_core/vcs_tools.py index eadadcf..1d8efcf 100644 --- a/src/delfino_core/vcs_tools.py +++ b/src/delfino_core/vcs_tools.py @@ -8,7 +8,10 @@ from delfino import run from delfino.execution import OnError +from delfino.validation import assert_pip_package_installed +from delfino_core.commands.issue_tracker import JiraClient +from delfino_core.config import VCSConfig from delfino_core.utils import ask, assert_executable_installed @@ -36,7 +39,7 @@ def _sanitize_branch_name(branch_name: str) -> str: return "/".join(_INVALID_BRANCH_NAME_CHARS.sub("_", part).strip("_") for part in branch_name.lower().split("/")) -def _get_sanitized_used_name() -> str: +def _get_user_name() -> str: # Get the current username from git config username = run(["git", "config", "user.email"], stdout=PIPE, on_error=OnError.ABORT).stdout.decode().strip() @@ -47,13 +50,16 @@ def _get_sanitized_used_name() -> str: elif "@" in username: # Strip the domain from the email address username = username.split("@")[0] - # Sanitize the username - return _sanitize_branch_name(username) + return username def get_new_branch_name_or_switch_to_branch(branch_prefix: str | None, title: str) -> tuple[str, bool]: if branch_prefix is None: - branch_prefix = f"{_get_sanitized_used_name()}/" + branch_prefix = _get_user_name() + + if branch_prefix != "": # allow for no prefix + branch_prefix = _sanitize_branch_name(branch_prefix) + "/" + branch_name = branch_prefix + _sanitize_branch_name(title) # Check if not already on the branch @@ -91,3 +97,32 @@ def get_vcs_cli_tool() -> Literal["gh", "glab"]: return "glab" raise RuntimeError("No supported VCS found in the git remote.") + + +def title_and_branch_prefix_from_issue_tracker( + vcs_cli_tool: str, title: str, command_config: VCSConfig +) -> tuple[str, str | None]: + branch_prefix = command_config.branch_prefix + prompt_part = "" + + if command_config.issue_tracking.tracker_url: + assert_pip_package_installed("httpx") + prompt_part = " or issue number" + + while not title: + title = input(f"Enter a title{prompt_part} for the {'PR' if vcs_cli_tool == 'gh' else 'MR'}: ").strip() + + if not command_config.issue_tracking.tracker_url: + return title, branch_prefix + + issue_number = None + try: + issue_number = int(title) + except ValueError: + pass + + if issue_number is not None: + title = JiraClient(command_config.issue_tracking).get_issue_title(issue_number) + branch_prefix = f"{command_config.issue_tracking.issue_prefix}{issue_number}" + + return title, branch_prefix