mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-16 08:42:17 -06:00
Compare commits
15 Commits
21166-asn4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5f0c19860 | ||
|
|
8da9b11ab8 | ||
|
|
ca67fa9999 | ||
|
|
eff768192e | ||
|
|
1e297d55ee | ||
|
|
fdb987ef91 | ||
|
|
b5a23db43c | ||
|
|
366b69aff7 | ||
|
|
c3e8c5e69c | ||
|
|
b55f36469d | ||
|
|
7fded2fd87 | ||
|
|
8cbfe94fba | ||
|
|
fff99fd3ff | ||
|
|
f4892caa51 | ||
|
|
a54ad24b47 |
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -30,13 +30,13 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
config-file: .github/codeql/codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -140,9 +140,6 @@ class FrontPortFormMixin(forms.Form):
|
||||
widget=forms.SelectMultiple(attrs={'size': 8})
|
||||
)
|
||||
|
||||
port_mapping_model = PortMapping
|
||||
parent_field = 'device'
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@@ -203,3 +200,22 @@ class FrontPortFormMixin(forms.Form):
|
||||
using=connection,
|
||||
update_fields=None
|
||||
)
|
||||
|
||||
def _get_rear_port_choices(self, parent_filter, front_port):
|
||||
"""
|
||||
Return a list of choices representing each available rear port & position pair on the parent object (identified
|
||||
by a Q filter), excluding those assigned to the specified instance.
|
||||
"""
|
||||
occupied_rear_port_positions = [
|
||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
||||
for mapping in self.port_mapping_model.objects.filter(parent_filter).exclude(front_port=front_port.pk)
|
||||
]
|
||||
|
||||
choices = []
|
||||
for rear_port in self.rear_port_model.objects.filter(parent_filter):
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
pair_id = f'{rear_port.pk}:{i}'
|
||||
if pair_id not in occupied_rear_port_positions:
|
||||
pair_label = f'{rear_port.name}:{i}'
|
||||
choices.append((pair_id, pair_label))
|
||||
return choices
|
||||
|
||||
@@ -1124,9 +1124,8 @@ class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
|
||||
),
|
||||
)
|
||||
|
||||
# Override FrontPortFormMixin attrs
|
||||
port_mapping_model = PortTemplateMapping
|
||||
parent_field = 'device_type'
|
||||
rear_port_model = RearPortTemplate
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
@@ -1137,13 +1136,14 @@ class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Populate rear port choices based on parent DeviceType or ModuleType
|
||||
if device_type_id := self.data.get('device_type') or self.initial.get('device_type'):
|
||||
device_type = DeviceType.objects.get(pk=device_type_id)
|
||||
parent_filter = Q(device_type=device_type_id)
|
||||
elif module_type_id := self.data.get('module_type') or self.initial.get('module_type'):
|
||||
parent_filter = Q(module_type=module_type_id)
|
||||
else:
|
||||
return
|
||||
|
||||
# Populate rear port choices
|
||||
self.fields['rear_ports'].choices = self._get_rear_port_choices(device_type, self.instance)
|
||||
self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)
|
||||
|
||||
# Set initial rear port mappings
|
||||
if self.instance.pk:
|
||||
@@ -1152,27 +1152,6 @@ class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
|
||||
for mapping in PortTemplateMapping.objects.filter(front_port_id=self.instance.pk)
|
||||
]
|
||||
|
||||
def _get_rear_port_choices(self, device_type, front_port):
|
||||
"""
|
||||
Return a list of choices representing each available rear port & position pair on the device type, excluding
|
||||
those assigned to the specified instance.
|
||||
"""
|
||||
occupied_rear_port_positions = [
|
||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
||||
for mapping in device_type.port_mappings.exclude(front_port=front_port.pk)
|
||||
]
|
||||
|
||||
choices = []
|
||||
for rear_port in RearPortTemplate.objects.filter(device_type=device_type):
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
pair_id = f'{rear_port.pk}:{i}'
|
||||
if pair_id not in occupied_rear_port_positions:
|
||||
pair_label = f'{rear_port.name}:{i}'
|
||||
choices.append(
|
||||
(pair_id, pair_label)
|
||||
)
|
||||
return choices
|
||||
|
||||
|
||||
class RearPortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
@@ -1619,6 +1598,9 @@ class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
|
||||
),
|
||||
)
|
||||
|
||||
port_mapping_model = PortMapping
|
||||
rear_port_model = RearPort
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
@@ -1629,13 +1611,12 @@ class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Populate rear port choices
|
||||
if device_id := self.data.get('device') or self.initial.get('device'):
|
||||
device = Device.objects.get(pk=device_id)
|
||||
parent_filter = Q(device=device_id)
|
||||
else:
|
||||
return
|
||||
|
||||
# Populate rear port choices
|
||||
self.fields['rear_ports'].choices = self._get_rear_port_choices(device, self.instance)
|
||||
self.fields['rear_ports'].choices = self._get_rear_port_choices(parent_filter, self.instance)
|
||||
|
||||
# Set initial rear port mappings
|
||||
if self.instance.pk:
|
||||
@@ -1644,27 +1625,6 @@ class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
|
||||
for mapping in PortMapping.objects.filter(front_port_id=self.instance.pk)
|
||||
]
|
||||
|
||||
def _get_rear_port_choices(self, device, front_port):
|
||||
"""
|
||||
Return a list of choices representing each available rear port & position pair on the device, excluding those
|
||||
assigned to the specified instance.
|
||||
"""
|
||||
occupied_rear_port_positions = [
|
||||
f'{mapping.rear_port_id}:{mapping.rear_port_position}'
|
||||
for mapping in device.port_mappings.exclude(front_port=front_port.pk)
|
||||
]
|
||||
|
||||
choices = []
|
||||
for rear_port in RearPort.objects.filter(device=device):
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
pair_id = f'{rear_port.pk}:{i}'
|
||||
if pair_id not in occupied_rear_port_positions:
|
||||
pair_label = f'{rear_port.name}:{i}'
|
||||
choices.append(
|
||||
(pair_id, pair_label)
|
||||
)
|
||||
return choices
|
||||
|
||||
|
||||
class RearPortForm(ModularDeviceComponentForm):
|
||||
fieldsets = (
|
||||
|
||||
@@ -211,12 +211,16 @@ def sync_cached_scope_fields(instance, created, **kwargs):
|
||||
for model in (Prefix, Cluster, WirelessLAN):
|
||||
qs = model.objects.filter(**filters)
|
||||
|
||||
# Bulk update cached fields to avoid O(N) performance issues with large datasets.
|
||||
# This does not trigger post_save signals, avoiding spurious change log entries.
|
||||
objects_to_update = []
|
||||
for obj in qs:
|
||||
# Recompute cache using the same logic as save()
|
||||
obj.cache_related_objects()
|
||||
obj.save(update_fields=[
|
||||
'_location',
|
||||
'_site',
|
||||
'_site_group',
|
||||
'_region',
|
||||
])
|
||||
objects_to_update.append(obj)
|
||||
|
||||
if objects_to_update:
|
||||
model.objects.bulk_update(
|
||||
objects_to_update,
|
||||
['_location', '_site', '_site_group', '_region']
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ class RackDimensionsPanel(panels.ObjectAttributesPanel):
|
||||
outer_width = attrs.NumericAttr('outer_width', unit_accessor='get_outer_unit_display')
|
||||
outer_height = attrs.NumericAttr('outer_height', unit_accessor='get_outer_unit_display')
|
||||
outer_depth = attrs.NumericAttr('outer_depth', unit_accessor='get_outer_unit_display')
|
||||
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{}mm')
|
||||
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{} mm')
|
||||
|
||||
|
||||
class RackNumberingPanel(panels.ObjectAttributesPanel):
|
||||
|
||||
@@ -1845,6 +1845,7 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
|
||||
class ModuleTypeBulkRenameView(generic.BulkRenameView):
|
||||
queryset = ModuleType.objects.all()
|
||||
filterset = filtersets.ModuleTypeFilterSet
|
||||
field_name = 'model'
|
||||
|
||||
|
||||
@register_model_view(ModuleType, 'bulk_delete', path='delete', detail=False)
|
||||
|
||||
@@ -372,8 +372,8 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
|
||||
'virtual_machine_id': instance.assigned_object.virtual_machine.pk,
|
||||
})
|
||||
|
||||
# Disable object assignment fields if the IP address is designated as primary
|
||||
if self.initial.get('primary_for_parent'):
|
||||
# Disable object assignment fields if the IP address is designated as primary or OOB
|
||||
if self.initial.get('primary_for_parent') or self.initial.get('oob_for_parent'):
|
||||
self.fields['interface'].disabled = True
|
||||
self.fields['vminterface'].disabled = True
|
||||
self.fields['fhrpgroup'].disabled = True
|
||||
|
||||
@@ -20,7 +20,7 @@ from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
|
||||
from virtualization.models import VMInterface
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from netbox.graphql.filter_lookups import BigIntegerLookup, IntegerLookup, IntegerRangeArrayLookup
|
||||
from netbox.graphql.filter_lookups import IntegerLookup, IntegerRangeArrayLookup
|
||||
from circuits.graphql.filters import ProviderFilter
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
from dcim.graphql.filters import SiteFilter
|
||||
@@ -53,7 +53,7 @@ __all__ = (
|
||||
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
asn: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
asn: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
sites: (
|
||||
@@ -70,10 +70,10 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
start: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
start: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
end: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
end: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@@ -940,6 +940,13 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
_("Cannot reassign IP address while it is designated as the primary IP for the parent object")
|
||||
)
|
||||
|
||||
# can't use is_oob_ip as self.assigned_object might be changed
|
||||
if hasattr(original_parent, 'oob_ip') and original_parent.oob_ip_id == self.pk:
|
||||
if parent != original_parent:
|
||||
raise ValidationError(
|
||||
_("Cannot reassign IP address while it is designated as the OOB IP for the parent object")
|
||||
)
|
||||
|
||||
# Validate IP status selection
|
||||
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:
|
||||
raise ValidationError({
|
||||
|
||||
@@ -19,11 +19,8 @@ from strawberry_django import (
|
||||
process_filters,
|
||||
)
|
||||
|
||||
from netbox.graphql.scalars import BigInt
|
||||
|
||||
__all__ = (
|
||||
'ArrayLookup',
|
||||
'BigIntegerLookup',
|
||||
'FloatArrayLookup',
|
||||
'FloatLookup',
|
||||
'IntegerArrayLookup',
|
||||
@@ -81,29 +78,6 @@ class IntegerLookup:
|
||||
return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix)
|
||||
|
||||
|
||||
@strawberry.input(one_of=True, description='Lookup for BigInteger fields. Only one of the lookup fields can be set.')
|
||||
class BigIntegerLookup:
|
||||
filter_lookup: FilterLookup[BigInt] | None = strawberry_django.filter_field()
|
||||
range_lookup: RangeLookup[BigInt] | None = strawberry_django.filter_field()
|
||||
comparison_lookup: ComparisonFilterLookup[BigInt] | None = strawberry_django.filter_field()
|
||||
|
||||
def get_filter(self):
|
||||
for field in self.__strawberry_definition__.fields:
|
||||
value = getattr(self, field.name, None)
|
||||
if value is not strawberry.UNSET:
|
||||
return value
|
||||
return None
|
||||
|
||||
@strawberry_django.filter_field
|
||||
def filter(self, info: Info, queryset: QuerySet, prefix: DirectiveValue[str] = '') -> Tuple[QuerySet, Q]:
|
||||
filters = self.get_filter()
|
||||
|
||||
if not filters:
|
||||
return queryset, Q()
|
||||
|
||||
return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix)
|
||||
|
||||
|
||||
@strawberry.input(one_of=True, description='Lookup for Float fields. Only one of the lookup fields can be set.')
|
||||
class FloatLookup:
|
||||
filter_lookup: FilterLookup[float] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import FilterLookup
|
||||
from strawberry_django import ComparisonFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import ChangeLoggingMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
|
||||
@@ -23,7 +23,7 @@ __all__ = (
|
||||
|
||||
@dataclass
|
||||
class BaseModelFilter:
|
||||
id: FilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
id: ComparisonFilterLookup[ID] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
class ChangeLoggedModelFilter(ChangeLoggingMixin, BaseModelFilter):
|
||||
|
||||
@@ -164,7 +164,7 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
|
||||
"""
|
||||
label = name[:1].upper() + name[1:]
|
||||
label = label.replace('_', ' ')
|
||||
return label
|
||||
return _(label)
|
||||
|
||||
def get_context(self, context):
|
||||
# Determine which attributes to display in the panel based on only/exclude args
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-15 05:05+0000\n"
|
||||
"POT-Creation-Date: 2026-01-16 05:04+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -2435,7 +2435,7 @@ msgstr ""
|
||||
msgid "Change logging is not supported for this object type ({type})."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/config.py:21 netbox/core/models/data.py:282
|
||||
#: netbox/core/models/config.py:21 netbox/core/models/data.py:284
|
||||
#: netbox/core/models/files.py:29 netbox/core/models/jobs.py:60
|
||||
#: netbox/extras/models/models.py:847 netbox/extras/models/notifications.py:39
|
||||
#: netbox/extras/models/notifications.py:195
|
||||
@@ -2541,58 +2541,63 @@ msgstr ""
|
||||
msgid "Unknown backend type: {type}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:180
|
||||
#: netbox/core/models/data.py:131
|
||||
#, python-brace-format
|
||||
msgid "URLs for local sources must start with {scheme} (or specify no scheme)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:182
|
||||
msgid "Cannot initiate sync; syncing already in progress."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:193
|
||||
#: netbox/core/models/data.py:195
|
||||
msgid ""
|
||||
"There was an error initializing the backend. A dependency needs to be "
|
||||
"installed: "
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:286 netbox/core/models/files.py:33
|
||||
#: netbox/core/models/data.py:288 netbox/core/models/files.py:33
|
||||
#: netbox/netbox/models/features.py:67
|
||||
msgid "last updated"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:296 netbox/dcim/models/cables.py:622
|
||||
#: netbox/core/models/data.py:298 netbox/dcim/models/cables.py:622
|
||||
msgid "path"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:299
|
||||
#: netbox/core/models/data.py:301
|
||||
msgid "File path relative to the data source's root"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:303 netbox/ipam/models/ip.py:507
|
||||
#: netbox/core/models/data.py:305 netbox/ipam/models/ip.py:507
|
||||
msgid "size"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:306
|
||||
#: netbox/core/models/data.py:308
|
||||
msgid "hash"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:310
|
||||
#: netbox/core/models/data.py:312
|
||||
msgid "Length must be 64 hexadecimal characters."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:312
|
||||
#: netbox/core/models/data.py:314
|
||||
msgid "SHA256 hash of the file data"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:326
|
||||
#: netbox/core/models/data.py:328
|
||||
msgid "data file"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:327
|
||||
#: netbox/core/models/data.py:329
|
||||
msgid "data files"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:400
|
||||
#: netbox/core/models/data.py:402
|
||||
msgid "auto sync record"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/core/models/data.py:401
|
||||
#: netbox/core/models/data.py:403
|
||||
msgid "auto sync records"
|
||||
msgstr ""
|
||||
|
||||
@@ -11240,7 +11245,13 @@ msgid ""
|
||||
"parent object"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:946
|
||||
#: netbox/ipam/models/ip.py:947
|
||||
msgid ""
|
||||
"Cannot reassign IP address while it is designated as the OOB IP for the "
|
||||
"parent object"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/ipam/models/ip.py:953
|
||||
msgid "Only IPv6 addresses can be assigned SLAAC status"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from vpn import models
|
||||
if TYPE_CHECKING:
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
from ipam.graphql.filters import IPAddressFilter, RouteTargetFilter
|
||||
from netbox.graphql.filter_lookups import BigIntegerLookup, IntegerLookup
|
||||
from netbox.graphql.filter_lookups import IntegerLookup
|
||||
from .enums import *
|
||||
|
||||
__all__ = (
|
||||
@@ -187,7 +187,7 @@ class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
identifier: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
identifier: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
# TODO: Remove this file in NetBox v4.3
|
||||
# This script has been maintained to ease transition to the pre-commit tool.
|
||||
|
||||
exec 1>&2
|
||||
|
||||
EXIT=0
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[0;33m'
|
||||
NOCOLOR='\033[0m'
|
||||
|
||||
printf "${YELLOW}The pre-commit hook script is obsolete. Please use pre-commit instead:${NOCOLOR}\n"
|
||||
printf " pip install pre-commit\n"
|
||||
printf " pre-commit install${NOCOLOR}\n"
|
||||
|
||||
exit 1
|
||||
Reference in New Issue
Block a user