Skip to content
Open
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
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,55 @@ serialize =
might be present multiple times in the file and you mean to only bump one of the
occurrences. Can be multiple lines, templated using [Python Format String Syntax](https://docs.python.org/3/library/string.html#format-string-syntax)

#### `search_regex =`
**default:** none

Regex to search for the string to be replaced in the file.
Support `{current_version}` template string (e.g. `(?<=version='){current_version}(?=')`).
If search_regex is defined, the search template string will be ignored in the section.

```ini
[bumpversion]
current_version = 2.4.5

[bumpversion:file:pom.xml]
search_regex = (?<=test-app-java</artifactId>\n\s{4}<version>){current_version}(?=</version>)
search = <version>{current_version}</version> ; will be ignored
replace = {new_version}
```
Example of use, when the application version is the same as some dependencies (java application, pom.xml):
```xml
...
<artifactId>test-app-java</artifactId>
<version>2.4.5</version>
<packaging>jar</packaging>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.4.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</pluginManagement>
...
```
Also available as `--search-regex`

#### `replace =`
**default:** `{new_version}`

Expand Down
13 changes: 12 additions & 1 deletion bumpversion/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
)

logger_list = logging.getLogger("bumpversion.list")
logger = logging.getLogger(__name__)
logger = logging.getLogger("bumpversion.cli")
time_context = {"now": datetime.now(), "utcnow": datetime.utcnow()}
special_char_context = {c: c for c in ("#", ";")}

Expand All @@ -68,6 +68,7 @@
"--parse",
"--serialize",
"--search",
"--search-regex",
"--replace",
"--tag-name",
"--tag-message",
Expand Down Expand Up @@ -361,6 +362,10 @@ def _load_configuration(config_file, explicit_config, defaults):
"search", "{current_version}"
)

if "search_regex" not in section_config:
section_config["search_regex"] = defaults.get("search_regex", "")


if "replace" not in section_config:
section_config["replace"] = defaults.get("replace", "{new_version}")

Expand Down Expand Up @@ -405,6 +410,11 @@ def _parse_arguments_phase_2(args, known_args, defaults, root_parser):
help="Template for complete string to search",
default=defaults.get("search", "{current_version}"),
)
parser2.add_argument(
"--search-regex",
metavar="SEARCH_REGEX",
help="Regex pattern to search",
)
parser2.add_argument(
"--replace",
metavar="REPLACE",
Expand All @@ -426,6 +436,7 @@ def _setup_versionconfig(known_args, part_configs):
parse=known_args.parse,
serialize=known_args.serialize,
search=known_args.search,
search_regex=known_args.search_regex,
replace=known_args.replace,
part_configs=part_configs,
)
Expand Down
3 changes: 3 additions & 0 deletions bumpversion/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ class VersionNotFoundException(BumpVersionException):

class InvalidVersionPartException(BumpVersionException):
"""The specified part (e.g. 'bugfix') was not found"""

class SearchRegexException(BumpVersionException):
"""Search regex error"""
138 changes: 97 additions & 41 deletions bumpversion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import logging
import os

from bumpversion.exceptions import VersionNotFoundException
from bumpversion.exceptions import VersionNotFoundException, SearchRegexException
import re

logger = logging.getLogger(__name__)
logger = logging.getLogger("bumpversion.cli")


class DiscardDefaultIfSpecifiedAppendAction(_AppendAction):
Expand Down Expand Up @@ -46,59 +47,104 @@ def should_contain_version(self, version, context):
Return normally if the version number is in fact present.
"""
context["current_version"] = self._versionconfig.serialize(version, context)
search_expression = self._versionconfig.search.format(**context)

if self.contains(search_expression):
return
if not self._versionconfig.search_regex:
search_expression = self._versionconfig.search.format(**context)

# the `search` pattern did not match, but the original supplied
# version number (representing the same version part values) might
# match instead.
if self.contains(search_expression):
return

# check whether `search` isn't customized, i.e. should match only
# very specific parts of the file
search_pattern_is_default = self._versionconfig.search == "{current_version}"
# the `search` pattern did not match, but the original supplied
# version number (representing the same version part values) might
# match instead.

if search_pattern_is_default and self.contains(version.original):
# original version is present and we're not looking for something
# more specific -> this is accepted as a match
return
# check whether `search` isn't customized, i.e. should match only
# very specific parts of the file
search_pattern_is_default = self._versionconfig.search == "{current_version}"

# version not found
raise VersionNotFoundException(
"Did not find '{}' in file: '{}'".format(
search_expression, self.path
)
)
if search_pattern_is_default and self.contains(version.original):
# original version is present and we're not looking for something
# more specific -> this is accepted as a match
return

def contains(self, search):
if not search:
return False
# version not found
raise VersionNotFoundException(
"Did not find '{}' in file: '{}'".format(
search_expression, self.path
)
)
else:
regex = self._escape_regex_braces(self._versionconfig.search_regex)
context["current_version"] = re.escape(context["current_version"])
search_expression = regex.format(**context)

try:
if self.contains(search_expression, regex=True):
return
except re.error as error:
raise SearchRegexException("Search regex error: {}".format(error))

# version not found
raise VersionNotFoundException(
"Did not match '{}' in file: '{}'".format(
search_expression, self.path
)
)

with open(self.path, "rt", encoding="utf-8") as f:
search_lines = search.splitlines()
lookbehind = []
def _escape_regex_braces(self, regex):
# Escape regex braces to support current_version template string
regex = re.sub(r"\{(?!current_version)", "{{", regex)
regex = re.sub(r"(?<!current_version)\}", "}}", regex)
return regex

for lineno, line in enumerate(f.readlines()):
lookbehind.append(line.rstrip("\n"))
def contains(self, search, regex=False):
found_or_match = False

if len(lookbehind) > len(search_lines):
lookbehind = lookbehind[1:]
if not search:
return False

if (
search_lines[0] in lookbehind[0]
and search_lines[-1] in lookbehind[-1]
and search_lines[1:-1] == lookbehind[1:-1]
):
if not regex:
with open(self.path, "rt", encoding="utf-8") as f:
search_lines = search.splitlines()
lookbehind = []

for lineno, line in enumerate(f.readlines()):
lookbehind.append(line.rstrip("\n"))

if len(lookbehind) > len(search_lines):
lookbehind = lookbehind[1:]

if (
search_lines[0] in lookbehind[0]
and search_lines[-1] in lookbehind[-1]
and search_lines[1:-1] == lookbehind[1:-1]
):
logger.info(
"Found '%s' in %s at line %s:\n%s",
search,
self.path,
lineno - (len(lookbehind) - 2),
line.rstrip(),
)
found_or_match = True
else:
with open(self.path, "rt", encoding="utf-8") as f:
file_content = f.read()
look_around = 100
matches = re.compile(search, re.MULTILINE|re.DOTALL)
for match in matches.finditer(file_content):
lineno, line = file_content[0:match.start()].count("\n"), file_content[(match.start()-look_around):(match.end()+look_around)]
line = "\n".join(line.split("\n")[1:-1]) if len(line.split("\n")) >= 3 else line
logger.info(
"Found '%s' in %s at line %s: %s",
"Matched '%s' in %s at line %s:\n%s",
search,
self.path,
lineno - (len(lookbehind) - 1),
(lineno + 1),
line.rstrip(),
)
return True
return False
found_or_match = True

return found_or_match

def replace(self, current_version, new_version, context, dry_run):

Expand All @@ -112,11 +158,21 @@ def replace(self, current_version, new_version, context, dry_run):
context["new_version"] = self._versionconfig.serialize(new_version, context)

search_for = self._versionconfig.search.format(**context)
search_for_regex = self._versionconfig.search_regex
if search_for_regex:
regex = self._escape_regex_braces(self._versionconfig.search_regex)
context["current_version"] = re.escape(context["current_version"])
search_for_regex = regex.format(**context)
replace_with = self._versionconfig.replace.format(**context)

file_content_after = file_content_before.replace(search_for, replace_with)

if file_content_before == file_content_after:
try:
file_content_after = re.sub(search_for_regex, replace_with, file_content_before) if search_for_regex else file_content_before.replace(search_for, replace_with)
except re.error as error:
raise SearchRegexException("Search regex error: {}".format(error))

if not search_for_regex and file_content_before == file_content_after:
# TODO expose this to be configurable
file_content_after = file_content_before.replace(
current_version.original, replace_with
Expand Down
3 changes: 2 additions & 1 deletion bumpversion/version_part.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class VersionConfig:
Hold a complete representation of a version string.
"""

def __init__(self, parse, serialize, search, replace, part_configs=None):
def __init__(self, parse, serialize, search, search_regex, replace, part_configs=None):
try:
self.parse_regex = re.compile(parse, re.VERBOSE)
except sre_constants.error as e:
Expand All @@ -156,6 +156,7 @@ def __init__(self, parse, serialize, search, replace, part_configs=None):

self.part_configs = part_configs
self.search = search
self.search_regex = search_regex
self.replace = replace

def order(self):
Expand Down
Loading