...

Text file src/github.com/bazelbuild/bazel-gazelle/internal/go_repository.bzl

Documentation: github.com/bazelbuild/bazel-gazelle/internal

     1# Copyright 2014 The Bazel Authors. All rights reserved.
     2#
     3# Licensed under the Apache License, Version 2.0 (the "License");
     4# you may not use this file except in compliance with the License.
     5# You may obtain a copy of the License at
     6#
     7#    http://www.apache.org/licenses/LICENSE-2.0
     8#
     9# Unless required by applicable law or agreed to in writing, software
    10# distributed under the License is distributed on an "AS IS" BASIS,
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12# See the License for the specific language governing permissions and
    13# limitations under the License.
    14
    15load("//internal:common.bzl", "env_execute", "executable_extension")
    16load("//internal:go_repository_cache.bzl", "read_cache_env")
    17load("@bazel_tools//tools/build_defs/repo:utils.bzl", "patch", "read_user_netrc", "use_netrc")
    18
    19_DOC = """
    20`go_repository` downloads a Go project and generates build files with Gazelle
    21if they are not already present. This is the simplest way to depend on
    22external Go projects.
    23
    24When `go_repository` is in module mode, it saves downloaded modules in a shared,
    25internal cache within Bazel's cache. It may be cleared with `bazel clean --expunge`.
    26By setting the environment variable `GO_REPOSITORY_USE_HOST_CACHE=1`, you can
    27force `go_repository` to use the module cache on the host system in the location
    28returned by `go env GOPATH`. Alternatively, by setting the environment variable
    29`GO_REPOSITORY_USE_HOST_MODCACHE=1`, you can force `go_repository` to use only
    30the module cache on the host system in the location returned by `go env GOMODCACHE`.
    31
    32**Example**
    33
    34```starlark
    35load("@bazel_gazelle//:deps.bzl", "go_repository")
    36
    37# Download using "go mod download"
    38go_repository(
    39    name = "com_github_pkg_errors",
    40    importpath = "github.com/pkg/errors",
    41    sum = "h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=",
    42    version = "v0.8.1",
    43)
    44
    45# Download automatically via git
    46go_repository(
    47    name = "com_github_pkg_errors",
    48    commit = "816c9085562cd7ee03e7f8188a1cfd942858cded",
    49    importpath = "github.com/pkg/errors",
    50)
    51
    52# Download from git fork
    53go_repository(
    54    name = "com_github_pkg_errors",
    55    commit = "816c9085562cd7ee03e7f8188a1cfd942858cded",
    56    importpath = "github.com/pkg/errors",
    57    remote = "https://example.com/fork/github.com/pkg/errors",
    58    vcs = "git",
    59)
    60
    61# Download via HTTP
    62go_repository(
    63    name = "com_github_pkg_errors",
    64    importpath = "github.com/pkg/errors",
    65    urls = ["https://codeload.github.com/pkg/errors/zip/816c9085562cd7ee03e7f8188a1cfd942858cded"],
    66    strip_prefix = "errors-816c9085562cd7ee03e7f8188a1cfd942858cded",
    67    type = "zip",
    68)
    69
    70# Download major version suffixed via git
    71go_repository(
    72    name = "com_github_thediveo_enumflag_v2",
    73    commit = "0217df583bf3d37b92798602e5061b36556bcd38",
    74    importpath = "github.com/thediveo/enumflag/v2",
    75    remote = "https://github.com/thediveo/enumflag",
    76    vcs = "git",
    77)
    78```
    79
    80"""
    81
    82# copied from
    83# https://github.com/bazelbuild/bazel/blob/d273cb62f43ef8169415cf60fc96e503ea2ad823/tools/build_defs/repo/http.bzl#L76
    84_AUTH_PATTERN_DOC = """An optional dict mapping host names to custom authorization patterns.
    85
    86If a URL's host name is present in this dict the value will be used as a pattern when
    87generating the authorization header for the http request. This enables the use of custom
    88authorization schemes used in a lot of common cloud storage providers.
    89
    90The pattern currently supports 2 tokens: <code>&lt;login&gt;</code> and
    91<code>&lt;password&gt;</code>, which are replaced with their equivalent value
    92in the netrc file for the same host name. After formatting, the result is set
    93as the value for the <code>Authorization</code> field of the HTTP request.
    94
    95Example attribute and netrc for a http download to an oauth2 enabled API using a bearer token:
    96
    97<pre>
    98auth_patterns = {
    99    "storage.cloudprovider.com": "Bearer &lt;password&gt;"
   100}
   101</pre>
   102
   103netrc:
   104
   105<pre>
   106machine storage.cloudprovider.com
   107        password RANDOM-TOKEN
   108</pre>
   109
   110The final HTTP request would have the following header:
   111
   112<pre>
   113Authorization: Bearer RANDOM-TOKEN
   114</pre>
   115"""
   116
   117# We can't disable timeouts on Bazel, but we can set them to large values.
   118_GO_REPOSITORY_TIMEOUT = 86400
   119
   120def _go_repository_impl(ctx):
   121    # TODO(#549): vcs repositories are not cached and still need to be fetched.
   122    # Download the repository or module.
   123    fetch_repo_args = None
   124    gazelle_path = None
   125
   126    # Declare Label dependencies at the top of function to avoid unnecessary fetching:
   127    # https://docs.bazel.build/versions/main/skylark/repository_rules.html#when-is-the-implementation-function-executed
   128    go_env_cache = str(ctx.path(Label("@bazel_gazelle_go_repository_cache//:go.env")))
   129    if not ctx.attr.urls:
   130        fetch_repo = str(ctx.path(Label("@bazel_gazelle_go_repository_tools//:bin/fetch_repo{}".format(executable_extension(ctx)))))
   131    generate = ctx.attr.build_file_generation == "on"
   132    _gazelle = "@bazel_gazelle_go_repository_tools//:bin/gazelle{}".format(executable_extension(ctx))
   133    if generate:
   134        gazelle_path = ctx.path(Label(_gazelle))
   135
   136    if ctx.attr.urls:
   137        # HTTP mode
   138        for key in ("commit", "tag", "vcs", "remote", "version", "sum", "replace"):
   139            if getattr(ctx.attr, key):
   140                fail("cannot specifiy both urls and %s" % key, key)
   141        result = ctx.download_and_extract(
   142            url = ctx.attr.urls,
   143            sha256 = ctx.attr.sha256,
   144            canonical_id = ctx.attr.canonical_id,
   145            stripPrefix = ctx.attr.strip_prefix,
   146            type = ctx.attr.type,
   147            auth = use_netrc(read_user_netrc(ctx), ctx.attr.urls, ctx.attr.auth_patterns),
   148        )
   149        if not ctx.attr.sha256:
   150            print("For Go module \"{path}\", integrity not specified, calculated sha256 = \"{sha256}\"".format(
   151                path = ctx.attr.importpath,
   152                sha256 = result.sha256,
   153            ))
   154    elif ctx.attr.commit or ctx.attr.tag:
   155        # repository mode
   156        if ctx.attr.commit:
   157            rev = ctx.attr.commit
   158            rev_key = "commit"
   159        elif ctx.attr.tag:
   160            rev = ctx.attr.tag
   161            rev_key = "tag"
   162        for key in ("urls", "strip_prefix", "type", "sha256", "version", "sum", "replace", "canonical_id"):
   163            if getattr(ctx.attr, key):
   164                fail("cannot specify both %s and %s" % (rev_key, key), key)
   165
   166        if ctx.attr.vcs and not ctx.attr.remote:
   167            fail("if vcs is specified, remote must also be")
   168
   169        fetch_repo_args = ["-dest", ctx.path(""), "-importpath", ctx.attr.importpath]
   170        if ctx.attr.remote:
   171            fetch_repo_args.extend(["--remote", ctx.attr.remote])
   172        if rev:
   173            fetch_repo_args.extend(["--rev", rev])
   174        if ctx.attr.vcs:
   175            fetch_repo_args.extend(["--vcs", ctx.attr.vcs])
   176    elif ctx.attr.version:
   177        # module mode
   178        for key in ("urls", "strip_prefix", "type", "sha256", "commit", "tag", "vcs", "remote"):
   179            if getattr(ctx.attr, key):
   180                fail("cannot specify both version and %s" % key)
   181        if not ctx.attr.sum:
   182            fail("if version is specified, sum must also be")
   183
   184        fetch_path = ctx.attr.replace if ctx.attr.replace else ctx.attr.importpath
   185        fetch_repo_args = [
   186            "-dest=" + str(ctx.path("")),
   187            "-importpath=" + fetch_path,
   188            "-version=" + ctx.attr.version,
   189            "-sum=" + ctx.attr.sum,
   190        ]
   191    else:
   192        fail("one of urls, commit, tag, or version must be specified")
   193
   194    env = read_cache_env(ctx, go_env_cache)
   195    env_keys = [
   196        # Respect user proxy and sumdb settings for privacy.
   197        # TODO(jayconrod): gazelle in go_repository mode should probably
   198        # not go out to the network at all. This means *the build*
   199        # goes out to the network. We tolerate this for downloading
   200        # archives, but finding module roots is a bit much.
   201        "GOPROXY",
   202        "GONOPROXY",
   203        "GOPRIVATE",
   204        "GOSUMDB",
   205        "GONOSUMDB",
   206
   207        # PATH is needed to locate git and other vcs tools.
   208        "PATH",
   209
   210        # HOME is needed to locate vcs configuration files (.gitconfig).
   211        "HOME",
   212
   213        # Settings below are used by vcs tools.
   214        "SSH_AUTH_SOCK",
   215        "SSL_CERT_FILE",
   216        "SSL_CERT_DIR",
   217        "HTTP_PROXY",
   218        "HTTPS_PROXY",
   219        "NO_PROXY",
   220        "http_proxy",
   221        "https_proxy",
   222        "no_proxy",
   223        "GIT_SSL_CAINFO",
   224        "GIT_SSH",
   225        "GIT_SSH_COMMAND",
   226        "GIT_CONFIG_COUNT",
   227    ]
   228
   229    # Git allows passing configuration through environmental variables, this will be picked
   230    # by go get properly: https://www.git-scm.com/docs/git-config/#Documentation/git-config.txt-GITCONFIGCOUNT
   231    if "GIT_CONFIG_COUNT" in ctx.os.environ:
   232        count = ctx.os.environ["GIT_CONFIG_COUNT"]
   233        if count:
   234            if not count.isdigit or int(count) < 1:
   235                fail("GIT_CONFIG_COUNT has to be a positive integer")
   236            count = int(count)
   237            for i in range(count):
   238                key = "GIT_CONFIG_KEY_%d" % i
   239                value = "GIT_CONFIG_VALUE_%d" % i
   240                for j in [key, value]:
   241                    if j not in ctx.os.environ:
   242                        fail("%s is not defined as an environment variable, but you asked for GIT_COUNT_COUNT=%d" % (j, count))
   243                env_keys = env_keys + [key, value]
   244
   245    env.update({k: ctx.os.environ[k] for k in env_keys if k in ctx.os.environ})
   246
   247    if fetch_repo_args:
   248        # Disable sumdb in fetch_repo. In module mode, the sum is a mandatory
   249        # attribute of go_repository, so we don't need to look it up.
   250        fetch_repo_env = dict(env)
   251        fetch_repo_env["GOSUMDB"] = "off"
   252
   253        # Override external GO111MODULE, because it is needed by module mode, no-op in repository mode
   254        fetch_repo_env["GO111MODULE"] = "on"
   255
   256        result = env_execute(
   257            ctx,
   258            [fetch_repo] + fetch_repo_args,
   259            environment = fetch_repo_env,
   260            timeout = _GO_REPOSITORY_TIMEOUT,
   261        )
   262        if result.return_code:
   263            fail("failed to fetch %s: %s" % (ctx.name, result.stderr))
   264        if ctx.attr.debug_mode and result.stderr:
   265            print("fetch_repo: " + result.stderr)
   266
   267    # Repositories are fetched. Determine if build file generation is needed.
   268    build_file_names = ctx.attr.build_file_name.split(",")
   269    existing_build_file = ""
   270    for name in build_file_names:
   271        path = ctx.path(name)
   272        if path.exists and not env_execute(ctx, ["test", "-f", path]).return_code:
   273            existing_build_file = name
   274            break
   275
   276    generate = generate or (not existing_build_file and ctx.attr.build_file_generation == "auto")
   277
   278    if generate:
   279        # Build file generation is needed. Populate Gazelle directive at root build file
   280        build_file_name = existing_build_file or build_file_names[0]
   281        if len(ctx.attr.build_directives) > 0:
   282            ctx.file(
   283                build_file_name,
   284                "\n".join(["# " + d for d in ctx.attr.build_directives]),
   285            )
   286
   287        # Run Gazelle
   288        if gazelle_path == None:
   289            gazelle_path = ctx.path(Label(_gazelle))
   290
   291        # ctx.attr.name is the canonical name of this repository, which contains a '~' if and only
   292        # if this repository is generated by a module extension rather than an invocation in
   293        # WORKSPACE.
   294        is_module_extension_repo = "~" in ctx.attr.name
   295        if is_module_extension_repo:
   296            # TODO: In Bazel 6.3.0 and earlier, there is no way to obtain a label referencing a repo
   297            # generated by an extension from within that extension. We thus have to manually
   298            # construct such a label pointing to the sibling `_go_repository_config` repo created by
   299            # the `go_deps` extension. All extension-generated repos have names of the form
   300            # `<prefix>~<name set by the extension>`.
   301            extension_repo_prefix = ctx.attr.name.rpartition("~")[0] + "~"
   302            repo_config = ctx.path(Label("@@" + extension_repo_prefix + "bazel_gazelle_go_repository_config//:WORKSPACE"))
   303        else:
   304            repo_config = ctx.path(ctx.attr.build_config)
   305        cmd = [
   306            gazelle_path,
   307            "-go_repository_mode",
   308            "-go_prefix",
   309            ctx.attr.importpath,
   310            "-mode",
   311            "fix",
   312            "-repo_root",
   313            ctx.path(""),
   314            "-repo_config",
   315            repo_config,
   316        ]
   317        if ctx.attr.version:
   318            cmd.append("-go_repository_module_mode")
   319        if ctx.attr.build_file_name:
   320            cmd.extend(["-build_file_name", ctx.attr.build_file_name])
   321        if ctx.attr.build_tags:
   322            cmd.extend(["-build_tags", ",".join(ctx.attr.build_tags)])
   323        if ctx.attr.build_external:
   324            cmd.extend(["-external", ctx.attr.build_external])
   325        if ctx.attr.build_file_proto_mode:
   326            cmd.extend(["-proto", ctx.attr.build_file_proto_mode])
   327        if ctx.attr.build_naming_convention:
   328            cmd.extend(["-go_naming_convention", ctx.attr.build_naming_convention])
   329        if is_module_extension_repo:
   330            cmd.append("-bzlmod")
   331        cmd.extend(ctx.attr.build_extra_args)
   332        cmd.append(ctx.path(""))
   333        ctx.report_progress("running Gazelle")
   334        result = env_execute(ctx, cmd, environment = env, timeout = _GO_REPOSITORY_TIMEOUT)
   335        if result.return_code:
   336            fail("failed to generate BUILD files for %s: %s" % (
   337                ctx.attr.importpath,
   338                result.stderr,
   339            ))
   340        if ctx.attr.debug_mode and result.stderr:
   341            print("%s gazelle.stdout: %s" % (ctx.name, result.stdout))
   342            print("%s gazelle.stderr: %s" % (ctx.name, result.stderr))
   343
   344    # Apply patches if necessary.
   345    patch(ctx)
   346
   347go_repository = repository_rule(
   348    implementation = _go_repository_impl,
   349    doc = _DOC,
   350    attrs = {
   351        # Fundamental attributes of a go repository
   352        "importpath": attr.string(
   353            doc = """The Go import path that matches the root directory of this repository.
   354
   355            In module mode (when `version` is set), this must be the module path. If
   356            neither `urls` nor `remote` is specified, `go_repository` will
   357            automatically find the true path of the module, applying import path
   358            redirection.
   359
   360            If build files are generated for this repository, libraries will have their
   361            `importpath` attributes prefixed with this `importpath` string.  """,
   362            mandatory = True,
   363        ),
   364
   365        # Attributes for a repository that should be checked out from VCS
   366        "commit": attr.string(
   367            doc = """If the repository is downloaded using a version control tool, this is the
   368            commit or revision to check out. With git, this would be a sha1 commit id.
   369            `commit` and `tag` may not both be set.""",
   370        ),
   371        "tag": attr.string(
   372            doc = """If the repository is downloaded using a version control tool, this is the
   373            named revision to check out. `commit` and `tag` may not both be set.""",
   374        ),
   375        "vcs": attr.string(
   376            default = "",
   377            doc = """One of `"git"`, `"hg"`, `"svn"`, `"bzr"`.
   378
   379            The version control system to use. This is usually determined automatically,
   380            but it may be necessary to set this when `remote` is set and the VCS cannot
   381            be inferred. You must have the corresponding tool installed on your host.""",
   382            values = [
   383                "",
   384                "git",
   385                "hg",
   386                "svn",
   387                "bzr",
   388            ],
   389        ),
   390        "remote": attr.string(
   391            doc = """The VCS location where the repository should be downloaded from. This is
   392            usually inferred from `importpath`, but you can set `remote` to download
   393            from a private repository or a fork.""",
   394        ),
   395
   396        # Attributes for a repository that should be downloaded via HTTP.
   397        "urls": attr.string_list(
   398            doc = """A list of HTTP(S) URLs where an archive containing the project can be
   399            downloaded. Bazel will attempt to download from the first URL; the others
   400            are mirrors.""",
   401        ),
   402        "strip_prefix": attr.string(
   403            doc = """If the repository is downloaded via HTTP (`urls` is set), this is a
   404            directory prefix to strip. See [`http_archive.strip_prefix`].""",
   405        ),
   406        "type": attr.string(
   407            doc = """One of `"zip"`, `"tar.gz"`, `"tgz"`, `"tar.bz2"`, `"tar.xz"`.
   408
   409            If the repository is downloaded via HTTP (`urls` is set), this is the
   410            file format of the repository archive. This is normally inferred from the
   411            downloaded file name.""",
   412        ),
   413        "sha256": attr.string(
   414            doc = """If the repository is downloaded via HTTP (`urls` is set), this is the
   415            SHA-256 sum of the downloaded archive. When set, Bazel will verify the archive
   416            against this sum before extracting it.
   417
   418            **CAUTION:** Do not use this with services that prepare source archives on
   419            demand, such as codeload.github.com. Any minor change in the server software
   420            can cause differences in file order, alignment, and compression that break
   421            SHA-256 sums.""",
   422        ),
   423        "canonical_id": attr.string(
   424            doc = """If the repository is downloaded via HTTP (`urls` is set) and this is set, restrict cache hits to those cases where the
   425            repository was added to the cache with the same canonical id.""",
   426        ),
   427        "auth_patterns": attr.string_dict(
   428            doc = _AUTH_PATTERN_DOC,
   429        ),
   430
   431        # Attributes for a module that should be downloaded with the Go toolchain.
   432        "version": attr.string(
   433            doc = """If specified, `go_repository` will download the module at this version
   434            using `go mod download`. `sum` must also be set. `commit`, `tag`,
   435            and `urls` may not be set. """,
   436        ),
   437        "sum": attr.string(
   438            doc = """A hash of the module contents. In module mode, `go_repository` will verify
   439            the downloaded module matches this sum. May only be set when `version`
   440            is also set.
   441
   442            A value for `sum` may be found in the `go.sum` file or by running
   443            `go mod download -json <module>@<version>`.""",
   444        ),
   445        "replace": attr.string(
   446            doc = """A replacement for the module named by `importpath`. The module named by
   447            `replace` will be downloaded at `version` and verified with `sum`.
   448
   449            NOTE: There is no `go_repository` equivalent to file path `replace`
   450            directives. Use `local_repository` instead.""",
   451        ),
   452
   453        # Attributes for a repository that needs automatic build file generation
   454        "build_external": attr.string(
   455            default = "static",
   456            doc = """One of `"external"`, `"static"` or `"vendored"`.
   457
   458            This sets Gazelle's `-external` command line flag. In `"static"` mode,
   459            Gazelle will not call out to the network to resolve imports.
   460
   461            **NOTE:** This cannot be used to ignore the `vendor` directory in a
   462            repository. The `-external` flag only controls how Gazelle resolves
   463            imports which are not present in the repository. Use
   464            `build_extra_args = ["-exclude=vendor"]` instead.""",
   465            values = [
   466                "",
   467                "external",
   468                "static",
   469                "vendored",
   470            ],
   471        ),
   472        "build_file_name": attr.string(
   473            default = "BUILD.bazel,BUILD",
   474            doc = """Comma-separated list of names Gazelle will consider to be build files.
   475            If a repository contains files named `build` that aren't related to Bazel,
   476            it may help to set this to `"BUILD.bazel"`, especially on case-insensitive
   477            file systems.""",
   478        ),
   479        "build_file_generation": attr.string(
   480            default = "auto",
   481            doc = """One of `"auto"`, `"on"`, `"off"`.
   482
   483            Whether Gazelle should generate build files in the repository. In `"auto"`
   484            mode, Gazelle will run if there is no build file in the repository root
   485            directory.""",
   486            values = [
   487                "on",
   488                "auto",
   489                "off",
   490            ],
   491        ),
   492        "build_naming_convention": attr.string(
   493            values = [
   494                "go_default_library",
   495                "import",
   496                "import_alias",
   497            ],
   498            default = "import_alias",
   499            doc = """Sets the library naming convention to use when resolving dependencies against this external
   500            repository. If unset, the convention from the external workspace is used.
   501            Legal values are `go_default_library`, `import`, and `import_alias`.
   502
   503            See the `gazelle:go_naming_convention` directive in [Directives] for more information.""",
   504        ),
   505        "build_tags": attr.string_list(
   506            doc = "This sets Gazelle's `-build_tags` command line flag.",
   507        ),
   508        "build_file_proto_mode": attr.string(
   509            doc = """One of `"default"`, `"legacy"`, `"disable"`, `"disable_global"` or `"package"`.
   510
   511            This sets Gazelle's `-proto` command line flag. See [Directives] for more
   512            information on each mode.""",
   513            values = [
   514                "",
   515                "default",
   516                "package",
   517                "disable",
   518                "disable_global",
   519                "legacy",
   520            ],
   521        ),
   522        "build_extra_args": attr.string_list(
   523            doc = "A list of additional command line arguments to pass to Gazelle when generating build files.",
   524        ),
   525        "build_config": attr.label(
   526            default = "@bazel_gazelle_go_repository_config//:WORKSPACE",
   527            doc = """A file that Gazelle should read to learn about external repositories before
   528            generating build files. This is useful for dependency resolution. For example,
   529            a `go_repository` rule in this file establishes a mapping between a
   530            repository name like `golang.org/x/tools` and a workspace name like
   531            `org_golang_x_tools`. Workspace directives like
   532            `# gazelle:repository_macro` are recognized.
   533
   534            `go_repository` rules will be re-evaluated when parts of WORKSPACE related
   535            to Gazelle's configuration are changed, including Gazelle directives and
   536            `go_repository` `name` and `importpath` attributes.
   537            Their content should still be fetched from a local cache, but build files
   538            will be regenerated. If this is not desirable, `build_config` may be set
   539            to a less frequently updated file or `None` to disable this functionality.""",
   540        ),
   541        "build_directives": attr.string_list(
   542            default = [],
   543            doc = """A list of directives to be written to the root level build file before
   544            Calling Gazelle to generate build files. Each string in the list will be
   545            prefixed with `#` automatically. A common use case is to pass a list of
   546            Gazelle directives.""",
   547        ),
   548
   549        # Patches to apply after running gazelle.
   550        "patches": attr.label_list(
   551            doc = "A list of patches to apply to the repository after gazelle runs.",
   552        ),
   553        "patch_tool": attr.string(
   554            default = "",
   555            doc = """The patch tool used to apply `patches`. If this is specified, Bazel will
   556            use the specifed patch tool instead of the Bazel-native patch implementation.""",
   557        ),
   558        "patch_args": attr.string_list(
   559            default = ["-p0"],
   560            doc = "Arguments passed to the patch tool when applying patches.",
   561        ),
   562        "patch_cmds": attr.string_list(
   563            default = [],
   564            doc = "Commands to run in the repository after patches are applied.",
   565        ),
   566
   567        # Attributes that affect the verbosity of logging:
   568        "debug_mode": attr.bool(
   569            default = False,
   570            doc = """Enables logging of fetch_repo and Gazelle output during succcesful runs. Gazelle can be noisy
   571            so this defaults to `False`. However, setting to `True` can be useful for debugging build failures and
   572            unexpected behavior for the given rule.
   573            """,
   574        ),
   575    },
   576)
   577"""See repository.md#go-repository for full documentation."""

View as plain text