├── .circleci ├── config.yml └── images │ └── Dockerfile ├── .dockerignore ├── .env.example ├── .github ├── images │ └── Dockerfile ├── pull_request_template.md └── workflows │ ├── release.yml │ ├── test.yml │ └── update_major_tag.yml ├── .gitignore ├── .gitmodules ├── .goreleaser.yaml ├── .idea ├── .gitignore ├── inspectionProfiles │ └── Project_Default.xml ├── mob.xml ├── modules.xml ├── unity-meta-checker.iml └── vcs.xml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── action.yml ├── aqua.yaml ├── build.Dockerfile ├── docs └── images │ └── github-pr-comment-screenshot.png ├── filecollector ├── aggregator.go ├── aggregator_stub.go ├── aggregator_test.go ├── entry.go ├── filecollector.go ├── filecollector_test.go ├── options.go └── repofinder │ ├── compose.go │ ├── find.go │ ├── find_stub.go │ ├── find_test.go │ └── foundrepo.go ├── git ├── config.go ├── config_test.go ├── lsfiles.go ├── lsfiles_stub.go ├── lsfiles_test.go ├── revparse.go └── revparse_test.go ├── go.mod ├── go.sum ├── ignore ├── file.go ├── file_test.go └── testdata │ └── Assets │ ├── Bar │ └── Baz │ └── Foo ├── main.go ├── main_test.go ├── options ├── argparser.go ├── env.go ├── ignorefile.go ├── ignorefile_stub.go ├── ignorefile_test.go ├── options.go ├── options_test.go ├── rootdir.go ├── rootdir_stub.go ├── rootdir_test.go ├── testdata │ └── ignorefile │ │ ├── ProjectDoesNotHaveMetaCheckIgnore │ │ └── .gitempty │ │ └── ProjectHasMetaCheckIgnore │ │ └── .meta-check-ignore ├── unityproject.go └── unityproject_stub.go ├── report ├── parser.go ├── parser_test.go └── printer.go ├── resultfilter ├── resultfilter.go └── resultfilter_test.go ├── test ├── compat ├── compat-revisions └── perf-compat ├── tool ├── gh-action │ ├── action-yaml-gen │ │ ├── cmd │ │ │ ├── cmd.go │ │ │ └── cmd_test.go │ │ ├── main.go │ │ ├── testdata │ │ │ └── action.yml │ │ └── yaml │ │ │ ├── inputs.go │ │ │ ├── inputs_test.go │ │ │ ├── metadata.go │ │ │ ├── runs.go │ │ │ ├── runs_test.go │ │ │ ├── yaml.go │ │ │ └── yaml_test.go │ ├── cmd │ │ ├── cmd.go │ │ └── cmd_test.go │ ├── inputs │ │ ├── env.go │ │ ├── event.go │ │ ├── event_stub.go │ │ ├── event_test.go │ │ ├── json.go │ │ ├── json_test.go │ │ └── testdata │ │ │ ├── inputs-example.json │ │ │ ├── pr-event-payload-example.json │ │ │ └── push-event-payload-example.json │ ├── main.go │ ├── options │ │ ├── options.go │ │ └── options_test.go │ └── runner │ │ ├── options.go │ │ ├── options_test.go │ │ ├── runner.go │ │ ├── runner_test.go │ │ └── testdata │ │ ├── InvalidProject │ │ └── Packages │ │ │ └── manifest.json │ │ ├── InvalidSubDir │ │ ├── dangling.meta │ │ └── missing │ │ ├── ValidProject │ │ └── Packages │ │ │ └── manifest.json │ │ └── ValidSubDir │ │ ├── ok │ │ └── ok.meta ├── unity-meta-autofix │ ├── autofix │ │ ├── autofix.go │ │ ├── autofix_stub.go │ │ ├── autofix_test.go │ │ ├── create.go │ │ ├── create_stub.go │ │ ├── create_test.go │ │ ├── metatype.go │ │ ├── metatype_stub.go │ │ ├── metatype_test.go │ │ ├── options.go │ │ ├── options_stub.go │ │ ├── remover.go │ │ ├── remover_stub.go │ │ └── testdata │ │ │ ├── InvalidProject │ │ │ └── Assets │ │ │ │ ├── Dangling.meta │ │ │ │ └── Missing │ │ │ └── ValidProject │ │ │ └── Assets │ │ │ ├── OK │ │ │ └── OK.meta │ ├── cmd │ │ ├── cmd.go │ │ ├── cmd_test.go │ │ └── testdata │ │ │ ├── InvalidProject │ │ │ └── Assets │ │ │ │ ├── Dangling.meta │ │ │ │ └── Missing │ │ │ └── ValidProject │ │ │ └── Assets │ │ │ ├── OK │ │ │ └── OK.meta │ ├── main.go │ └── options │ │ ├── options.go │ │ └── options_test.go ├── unity-meta-check-github-pr-comment │ ├── cmd │ │ ├── cmd.go │ │ └── cmd_test.go │ ├── github │ │ ├── api.go │ │ ├── api_test.go │ │ ├── http.go │ │ ├── send.go │ │ ├── send_stub.go │ │ └── types.go │ ├── l10n │ │ ├── lang.go │ │ ├── template.go │ │ ├── template_stub.go │ │ └── template_test.go │ ├── main.go │ ├── markdown │ │ ├── markdown.go │ │ └── markdown_test.go │ └── options │ │ ├── options.go │ │ ├── options_test.go │ │ └── testdata │ │ └── example-template.json ├── unity-meta-check-junit │ ├── cmd │ │ ├── cmd.go │ │ └── cmd_test.go │ ├── junit │ │ ├── junit.go │ │ ├── junit_stub.go │ │ └── junit_test.go │ ├── main.go │ └── options │ │ └── options.go └── unity-meta-check-meta-audit │ └── main.go ├── unity ├── checker │ ├── checker.go │ ├── checker_stub.go │ ├── checker_test.go │ ├── chkworker.go │ ├── chkworker_test.go │ ├── options.go │ ├── result.go │ ├── strategy.go │ └── testdata │ │ ├── EmptyManifest │ │ └── Packages │ │ │ └── manifest.json │ │ ├── LocalsManifest │ │ ├── LocalPackages │ │ │ └── com.example.local │ │ │ │ └── .gitempty │ │ └── Packages │ │ │ └── manifest.json │ │ └── NoLocalsManifest │ │ └── Packages │ │ └── manifest.json ├── manifest.go ├── manifest_test.go ├── meta │ ├── defaultimportergen.go │ ├── defaultimportergen_test.go │ ├── foldergen.go │ ├── foldergen_test.go │ ├── gen.go │ ├── guid.go │ ├── guid_stub.go │ ├── guid_test.go │ ├── guidgen.go │ ├── guidgen_stub.go │ ├── monoimportergen.go │ ├── monoimportergen_test.go │ ├── textscriptimportergen.go │ └── textscriptimportergen_test.go ├── metanecessity.go ├── metanecessity_stub.go ├── metanecessity_test.go ├── package.go ├── package_test.go └── testdata │ ├── EmptyManifest │ └── Packages │ │ └── manifest.json │ ├── LocalsManifest │ ├── LocalPackages │ │ └── com.example.local │ │ │ └── .gitempty │ └── Packages │ │ └── manifest.json │ └── NoLocalsManifest │ └── Packages │ ├── com.example.exists │ └── .gitempty │ └── manifest.json ├── util ├── chanutil │ ├── fromslice.go │ └── toslice.go ├── cli │ ├── cmd.go │ ├── env.go │ ├── env_stub.go │ ├── loglevel.go │ ├── opt │ │ ├── flag.go │ │ ├── flag │ │ │ ├── flag.go │ │ │ └── flag_test.go │ │ ├── flag_test.go │ │ ├── ghactions │ │ │ ├── ghactions.go │ │ │ └── ghactions_test.go │ │ └── markdown │ │ │ ├── markdown.go │ │ │ └── markdown_test.go │ ├── processinout.go │ ├── processinout_stub.go │ └── prof.go ├── cstrset │ ├── cistrset.go │ ├── csstrset.go │ └── cstrset.go ├── errutil │ └── errors.go ├── globs │ ├── glob.go │ └── glob_test.go ├── logging │ ├── logger.go │ └── logger_stub.go ├── ostestable │ ├── getwd.go │ ├── getwd_stub.go │ ├── isdir.go │ └── isdir_stub.go ├── pathutil │ ├── ancestors.go │ ├── ancestors_test.go │ ├── set.go │ ├── split.go │ ├── split_test.go │ ├── tree.go │ └── tree_test.go ├── prefix │ ├── writer.go │ └── writer_test.go ├── testutil │ ├── env.go │ ├── readcloser_stub.go │ ├── writecloser_spy.go │ └── writecloser_stub.go └── typedpath │ ├── basename.go │ ├── raw.go │ ├── raw_test.go │ └── slash.go └── version └── version.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | go_get: 4 | docker: 5 | - image: cimg/go:1.22.2 6 | steps: 7 | - run: 8 | name: Test go get 9 | command: | 10 | go get github.com/DeNA/unity-meta-check 11 | - run: 12 | name: Test 13 | command: unity-meta-check --version 14 | 15 | workflows: 16 | version: 2 17 | 18 | main: 19 | jobs: 20 | - go_get: 21 | filters: 22 | branches: 23 | only: master 24 | -------------------------------------------------------------------------------- /.circleci/images/Dockerfile: -------------------------------------------------------------------------------- 1 | # image-name: ghcr.io/dena/unity-meta-check/unity-meta-check-circleci 2 | FROM ghcr.io/dena/unity-meta-check/unity-meta-check-builder:latest as builder 3 | 4 | FROM debian:bookworm-slim 5 | # https://circleci.com/docs/2.0/custom-images/#required-tools-for-primary-containers 6 | RUN apt-get update \ 7 | && apt-get install --yes --no-install-recommends git openssh-server tar gzip ca-certificates \ 8 | && apt-get clean \ 9 | && rm -rf /var/lib/apt/lists/* 10 | COPY --from=builder /go/src/unity-meta-check/out/* /usr/bin/ 11 | ENTRYPOINT ["unity-meta-check"] 12 | CMD ["-help"] 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.gitignore 3 | /out 4 | /test 5 | /tmp 6 | /Dockerfile 7 | testdata 8 | .env 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | UNITY_META_CHECK_GITHUB_API_ENDPOINT=https://github.example.com/api/v3 2 | UNITY_META_CHECK_GITHUB_OWNER=owner 3 | UNITY_META_CHECK_GITHUB_REPO=repo 4 | UNITY_META_CHECK_GITHUB_PULL_NUMBER=1 5 | UNITY_META_CHECK_GITHUB_TOKEN=**************************************** 6 | -------------------------------------------------------------------------------- /.github/images/Dockerfile: -------------------------------------------------------------------------------- 1 | # image-name: ghcr.io/dena/unity-meta-check/unity-meta-check-gh-action 2 | FROM ghcr.io/dena/unity-meta-check/unity-meta-check-builder:latest as builder 3 | 4 | FROM debian:bookworm-slim 5 | RUN apt-get update \ 6 | && apt-get install --yes --no-install-recommends git ca-certificates \ 7 | && apt-get clean \ 8 | && rm -rf /var/lib/apt/lists/* 9 | COPY --from=builder /go/src/unity-meta-check/out/gh-action /usr/bin/ 10 | ENTRYPOINT ["gh-action"] 11 | CMD ["-help"] 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | --- 4 | 5 | ### Contribution License Agreement 6 | 7 | - [ ] By placing an "x" in the box, I hereby understand, accept and agree to be bound by the terms and conditions of the [Contribution License Agreement](https://dena.github.io/cla/). 8 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Tests 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - '**/*.md' 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ${{ matrix.os }} 11 | permissions: 12 | contents: read 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - windows-latest 18 | - macos-latest 19 | steps: 20 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 21 | with: 22 | submodules: recursive 23 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 24 | with: 25 | go-version-file: go.mod 26 | - run: go test -v ./... 27 | -------------------------------------------------------------------------------- /.github/workflows/update_major_tag.yml: -------------------------------------------------------------------------------- 1 | name: Update major version and latest tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | tag: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 15 | - uses: nowsprinting/check-version-format-action@98485692a883d962227b09f40f29a63de0771299 # v4.0.2 16 | id: version 17 | with: 18 | prefix: v 19 | - name: Update major version and latest tag 20 | run: | 21 | git push --force origin 'HEAD:refs/heads/${{ steps.version.outputs.major_prerelease }}' 22 | git push --force origin 'HEAD:refs/heads/latest' 23 | if: steps.version.outputs.is_valid == 'true' 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | dist 3 | out 4 | .env 5 | # Created by https://www.toptal.com/developers/gitignore/api/go 6 | # Edit at https://www.toptal.com/developers/gitignore?templates=go 7 | 8 | ### Go ### 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | 25 | ### Go Patch ### 26 | /vendor/ 27 | /Godeps/ 28 | 29 | # End of https://www.toptal.com/developers/gitignore/api/go 30 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "testdata/ValidProject"] 2 | path = testdata/ValidProject 3 | url = https://github.com/DeNA/unity-meta-check-ValidProject 4 | [submodule "testdata/InvalidProject"] 5 | path = testdata/InvalidProject 6 | url = https://github.com/DeNA/unity-meta-check-InvalidProject 7 | [submodule "tool/gh-action/cmd/testdata/ValidProject"] 8 | path = tool/gh-action/cmd/testdata/ValidProject 9 | url = https://github.com/DeNA/unity-meta-check-ValidProject 10 | [submodule "tool/gh-action/cmd/testdata/InvalidProject"] 11 | path = tool/gh-action/cmd/testdata/InvalidProject 12 | url = https://github.com/DeNA/unity-meta-check-InvalidProject 13 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | builds: 4 | - id: unity-meta-check 5 | binary: unity-meta-check 6 | main: ./main.go 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - windows 12 | - darwin 13 | goarch: 14 | - amd64 15 | - arm64 16 | - id: unity-meta-check-junit 17 | binary: unity-meta-check-junit 18 | main: ./tool/unity-meta-check-junit 19 | env: 20 | - CGO_ENABLED=0 21 | goos: 22 | - linux 23 | - windows 24 | - darwin 25 | goarch: 26 | - amd64 27 | - arm64 28 | - id: gh-action 29 | binary: gh-action 30 | main: ./tool/gh-action 31 | env: 32 | - CGO_ENABLED=0 33 | goos: 34 | - linux 35 | goarch: 36 | - amd64 37 | - arm64 38 | - id: gh-action-yaml-gen 39 | binary: gh-action-yaml-gen 40 | main: ./tool/gh-action/action-yaml-gen 41 | env: 42 | - CGO_ENABLED=0 43 | goos: 44 | - linux 45 | - windows 46 | - darwin 47 | goarch: 48 | - amd64 49 | - arm64 50 | - id: unity-meta-autofix 51 | binary: unity-meta-autofix 52 | main: ./tool/unity-meta-autofix 53 | env: 54 | - CGO_ENABLED=0 55 | goos: 56 | - linux 57 | - windows 58 | - darwin 59 | goarch: 60 | - amd64 61 | - arm64 62 | - id: unity-meta-check-github-pr-comment 63 | binary: unity-meta-check-github-pr-comment 64 | main: ./tool/unity-meta-check-github-pr-comment 65 | env: 66 | - CGO_ENABLED=0 67 | goos: 68 | - linux 69 | - windows 70 | - darwin 71 | goarch: 72 | - amd64 73 | - arm64 74 | 75 | archives: 76 | - format: binary 77 | name_template: >- 78 | {{ .Binary }}- 79 | {{- if eq .Os "Darwin" }}darwin- 80 | {{- else if eq .Os "Linux" }}linux- 81 | {{- else }}{{ .Os }}{{ end }}-{{ .Arch }} 82 | # https://goreleaser.com/errors/multiple-binaries-archive/ 83 | allow_different_binary_count: true 84 | 85 | checksum: 86 | name_template: "checksums.txt" 87 | 88 | release: 89 | prerelease: auto 90 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | # GitHub Copilot persisted chat sessions 10 | /copilot/chatSessions 11 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /.idea/mob.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/unity-meta-checker.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to run all tests 2 | ==================== 3 | 4 | 1. Copy `.env.example` to `.env` 5 | 2. Fill `.env` 6 | 3. Run `dotenv go test ./...` 7 | 8 | 9 | 10 | How to deploy 11 | ============= 12 | 13 | ```console 14 | $ cd path/to/unity-meta-check 15 | 16 | $ # Write the new version to deploy into version.go 17 | $ edit ./version/version.go 18 | $ go run ./tool/gh-action/action-yaml-gen/main.go ./action.yml 19 | $ git add ./version/version.go ./action.yml 20 | $ git commit -m "Bump to $(./scripts/print-version)" 21 | $ git push 22 | 23 | $ # Deploy to GitHub releases and Docker registry and unity-meta-check-bins: 24 | $ scripts/deploy 25 | ``` 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # image-name: ghcr.io/dena/unity-meta-check/unity-meta-check 2 | FROM ghcr.io/dena/unity-meta-check/unity-meta-check-builder:latest as builder 3 | 4 | FROM debian:bookworm-slim 5 | RUN apt-get update \ 6 | && apt-get install --yes --no-install-recommends git \ 7 | && apt-get clean \ 8 | && rm -rf /var/lib/apt/lists/* 9 | COPY --from=builder /go/src/unity-meta-check/out/* /usr/bin/ 10 | ENTRYPOINT ["unity-meta-check"] 11 | CMD ["-help"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 DeNA Co., Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /aqua.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # aqua - Declarative CLI Version Manager 3 | # https://aquaproj.github.io/ 4 | # checksum: 5 | # enabled: true 6 | # require_checksum: true 7 | # supported_envs: 8 | # - all 9 | registries: 10 | - type: standard 11 | ref: v4.192.0 # renovate: depName=aquaproj/aqua-registry 12 | packages: 13 | - name: goreleaser/goreleaser@v2.0.0 14 | - name: cli/cli@v2.50.0 15 | -------------------------------------------------------------------------------- /build.Dockerfile: -------------------------------------------------------------------------------- 1 | # image-name: ghcr.io/dena/unity-meta-check/unity-meta-check-builder 2 | FROM golang:1.22.2-bookworm as builder 3 | ARG TARGETARCH 4 | WORKDIR /go/src/unity-meta-check 5 | COPY . . 6 | RUN make -j$(nproc) out/unity-meta-check-linux-${TARGETARCH} \ 7 | out/unity-meta-check-junit-linux-${TARGETARCH} \ 8 | out/unity-meta-check-github-pr-comment-linux-${TARGETARCH} \ 9 | out/unity-meta-autofix-linux-${TARGETARCH} \ 10 | out/gh-action-linux-${TARGETARCH} && \ 11 | mv ./out/unity-meta-check-linux-${TARGETARCH} ./out/unity-meta-check && \ 12 | mv ./out/unity-meta-check-junit-linux-${TARGETARCH} ./out/unity-meta-check-junit && \ 13 | mv ./out/unity-meta-check-github-pr-comment-linux-${TARGETARCH} ./out/unity-meta-check-github-pr-comment && \ 14 | mv ./out/unity-meta-autofix-linux-${TARGETARCH} ./out/unity-meta-autofix && \ 15 | mv ./out/gh-action-linux-${TARGETARCH} ./out/gh-action 16 | -------------------------------------------------------------------------------- /docs/images/github-pr-comment-screenshot.png: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /filecollector/aggregator.go: -------------------------------------------------------------------------------- 1 | package filecollector 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/filecollector/repofinder" 6 | "github.com/DeNA/unity-meta-check/git" 7 | "github.com/DeNA/unity-meta-check/util/errutil" 8 | "github.com/DeNA/unity-meta-check/util/logging" 9 | "github.com/DeNA/unity-meta-check/util/typedpath" 10 | "sync" 11 | ) 12 | 13 | type FileAggregator func(rootDirAbs typedpath.RawPath, opts *Options, ch chan<- Entry) error 14 | 15 | func NewFileAggregator(gitLsFiles git.LsFiles, findRepo repofinder.RepoFinder, logger logging.Logger) FileAggregator { 16 | collectFiles := New(gitLsFiles, logger) 17 | 18 | return func(rootDirAbs typedpath.RawPath, opts *Options, ch chan<- Entry) error { 19 | var errsMutex sync.Mutex 20 | errs := make([]error, 0) 21 | 22 | foundRepos, err := findRepo() 23 | if err != nil { 24 | return err 25 | } 26 | 27 | var wg sync.WaitGroup 28 | for _, foundRepo := range foundRepos { 29 | logger.Info(fmt.Sprintf("repository found: %q (submodule=%t)", foundRepo.RawPath, foundRepo.Type)) 30 | 31 | wg.Add(1) 32 | go func(foundRepo repofinder.FoundRepo) { 33 | defer wg.Done() 34 | ch <- Entry{Path: foundRepo.RawPath.ToSlash(), IsDir: true} 35 | }(foundRepo) 36 | 37 | wg.Add(1) 38 | go func(foundRepo repofinder.FoundRepo) { 39 | defer wg.Done() 40 | if err := collectFiles(rootDirAbs, foundRepo.RawPath, opts, ch); err != nil { 41 | errsMutex.Lock() 42 | errs = append(errs, err) 43 | errsMutex.Unlock() 44 | return 45 | } 46 | }(foundRepo) 47 | } 48 | 49 | if err = collectFiles(rootDirAbs, ".", opts, ch); err != nil { 50 | errsMutex.Lock() 51 | errs = append(errs, err) 52 | errsMutex.Unlock() 53 | } 54 | wg.Wait() 55 | 56 | if len(errs) > 0 { 57 | return errutil.NewErrors(errs) 58 | } 59 | return nil 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /filecollector/aggregator_stub.go: -------------------------------------------------------------------------------- 1 | package filecollector 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | ) 6 | 7 | func StubFileAggregator(result []Entry, err error) FileAggregator { 8 | return func(rootDirAbs typedpath.RawPath, opts *Options, ch chan<- Entry) error { 9 | for _, path := range result { 10 | ch <- path 11 | } 12 | return err 13 | } 14 | } 15 | 16 | func StubSuccessfulFileAggregator(result []Entry) FileAggregator { 17 | return StubFileAggregator(result, nil) 18 | } 19 | -------------------------------------------------------------------------------- /filecollector/entry.go: -------------------------------------------------------------------------------- 1 | package filecollector 2 | 3 | import "github.com/DeNA/unity-meta-check/util/typedpath" 4 | 5 | type Entry struct { 6 | Path typedpath.SlashPath 7 | IsDir bool 8 | } 9 | -------------------------------------------------------------------------------- /filecollector/filecollector.go: -------------------------------------------------------------------------------- 1 | package filecollector 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/git" 7 | "github.com/DeNA/unity-meta-check/util/logging" 8 | "github.com/DeNA/unity-meta-check/util/pathutil" 9 | "github.com/DeNA/unity-meta-check/util/typedpath" 10 | "os" 11 | "sync" 12 | ) 13 | 14 | type FileCollector func(projRootAbs typedpath.RawPath, targetRel typedpath.RawPath, opts *Options, writer chan<- Entry) error 15 | 16 | func New(gitLsFiles git.LsFiles, logger logging.Logger) FileCollector { 17 | return func(projRootAbs typedpath.RawPath, targetRel typedpath.RawPath, opts *Options, writer chan<- Entry) error { 18 | targetAbs := projRootAbs.JoinRawPath(targetRel) 19 | targetRelSlash := targetRel.ToSlash() 20 | 21 | logger.Debug(fmt.Sprintf("searching: %q", targetAbs)) 22 | dirSet := pathutil.NewPathSetWithSize(opts.IgnoreCase, 50000) 23 | 24 | stdoutReader, stdoutWriter, err := os.Pipe() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | var wg sync.WaitGroup 30 | wg.Add(1) 31 | go func() { 32 | defer wg.Done() 33 | dirSet.Add(targetRelSlash) // XXX: To prevent endless loop. 34 | 35 | scanner := bufio.NewScanner(stdoutReader) 36 | for scanner.Scan() { 37 | var filePath typedpath.SlashPath 38 | if targetRelSlash == "." { 39 | filePath = typedpath.SlashPath(scanner.Text()) 40 | } else { 41 | filePath = targetRelSlash.JoinSlashPath(typedpath.SlashPath(scanner.Text())) 42 | } 43 | writer <- Entry{ 44 | Path: filePath, 45 | // NOTE: git ls-files list only files. 46 | IsDir: false, 47 | } 48 | 49 | dirname := filePath.Dir() 50 | for { 51 | // NOTE: Do not list duplicated entries. 52 | if dirSet.Has(dirname) { 53 | break 54 | } 55 | 56 | writer <- Entry{ 57 | Path: dirname, 58 | IsDir: true, 59 | } 60 | dirSet.Add(dirname) 61 | dirname = dirname.Dir() 62 | } 63 | } 64 | }() 65 | 66 | if err := gitLsFiles(targetAbs, []string{}, stdoutWriter); err != nil { 67 | return err 68 | } 69 | wg.Wait() 70 | logger.Debug(fmt.Sprintf("length of set for collected path footprints: %d", dirSet.Len())) 71 | return nil 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /filecollector/filecollector_test.go: -------------------------------------------------------------------------------- 1 | package filecollector 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/git" 5 | "github.com/DeNA/unity-meta-check/util/chanutil" 6 | "github.com/DeNA/unity-meta-check/util/logging" 7 | "github.com/google/go-cmp/cmp" 8 | "reflect" 9 | "sort" 10 | "sync" 11 | "testing" 12 | ) 13 | 14 | func TestNewOnlyTracked(t *testing.T) { 15 | spyLogger := logging.SpyLogger() 16 | 17 | // NOTE: git returns a slash separated path on Windows. 18 | gitLsFiles := git.StubRawLsFiles(`dir1/fileB 19 | fileA 20 | `, nil) 21 | opts := &Options{IgnoreCase: false} 22 | onlyTracked := New(gitLsFiles, spyLogger) 23 | 24 | spyCh := make(chan Entry) 25 | actual := make([]Entry, 0) 26 | 27 | var wg sync.WaitGroup 28 | wg.Add(1) 29 | go func() { 30 | actual = chanutil.ToSlice(spyCh) 31 | sort.Slice(actual, func(i, j int) bool { 32 | return actual[i].Path < actual[j].Path 33 | }) 34 | defer wg.Done() 35 | }() 36 | 37 | if err := onlyTracked("/path/to/repo", ".", opts, spyCh); err != nil { 38 | t.Log(spyLogger.Logs.String()) 39 | t.Errorf("want nil, got %#v", err) 40 | return 41 | } 42 | close(spyCh) 43 | wg.Wait() 44 | 45 | expected := []Entry{ 46 | // NOTE: Directory should be added automatically (git ls-files does not print any directories) 47 | {Path: "dir1", IsDir: true}, 48 | {Path: "dir1/fileB", IsDir: false}, 49 | {Path: "fileA", IsDir: false}, 50 | } 51 | if !reflect.DeepEqual(actual, expected) { 52 | t.Log(spyLogger.Logs.String()) 53 | t.Error(cmp.Diff(expected, actual)) 54 | return 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /filecollector/options.go: -------------------------------------------------------------------------------- 1 | package filecollector 2 | 3 | type Options struct { 4 | IgnoreCase bool 5 | } 6 | -------------------------------------------------------------------------------- /filecollector/repofinder/compose.go: -------------------------------------------------------------------------------- 1 | package repofinder 2 | 3 | func Compose(repoFinders []RepoFinder) RepoFinder { 4 | return func() ([]FoundRepo, error) { 5 | result := make([]FoundRepo, 0) 6 | for _, repoFinder := range repoFinders { 7 | foundRepo, err := repoFinder() 8 | if err != nil { 9 | return nil, err 10 | } 11 | result = append(result, foundRepo...) 12 | } 13 | return result, nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /filecollector/repofinder/find.go: -------------------------------------------------------------------------------- 1 | package repofinder 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type RepositoryType bool 10 | 11 | const ( 12 | RepositoryTypeIsSubmodule RepositoryType = true 13 | RepositoryTypeIsNested RepositoryType = false 14 | ) 15 | 16 | type RepoFinder func() ([]FoundRepo, error) 17 | 18 | func New(rootDirAbs typedpath.RawPath, targetDirRel typedpath.RawPath) RepoFinder { 19 | return func() ([]FoundRepo, error) { 20 | var targetDirAbs typedpath.RawPath 21 | if targetDirRel == "." { 22 | targetDirAbs = rootDirAbs 23 | } else { 24 | targetDirAbs = rootDirAbs.JoinRawPath(targetDirRel) 25 | } 26 | 27 | result := make([]FoundRepo, 0) 28 | 29 | if err := filepath.Walk(string(targetDirAbs), func(path string, info os.FileInfo, err error) error { 30 | if info == nil { 31 | return err 32 | } 33 | 34 | // NOTE: This matches both submodules and nested repositories. The "nested repository" means a repository 35 | // that is cloned into other repository with no any submodule commands. The nested repositories are 36 | // known as anti-pattern, but it is frequently needed for game developers. 37 | if info.Name() == ".git" { 38 | relPath, err := rootDirAbs.Rel(typedpath.RawPath(path)) 39 | if err != nil { 40 | return err 41 | } 42 | isDir := info.IsDir() 43 | 44 | // NOTE: Ignore the repository at the rootDirAbs. 45 | if relPath != ".git" { 46 | result = append(result, FoundRepo{ 47 | Type: RepositoryType(!isDir), 48 | RawPath: relPath.Dir(), 49 | }) 50 | } 51 | // NOTE: Skip finding into .git if it is a directory (this is a nested repository). But if it is a file 52 | // (this is a submodule) skipping will skip unexpectedly the parent directory instead of the file. 53 | if isDir { 54 | return filepath.SkipDir 55 | } 56 | return nil 57 | } 58 | return nil 59 | }); err != nil { 60 | return result, err 61 | } 62 | return result, nil 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /filecollector/repofinder/find_stub.go: -------------------------------------------------------------------------------- 1 | package repofinder 2 | 3 | func StubRepoFinder(result []FoundRepo, err error) RepoFinder { 4 | return func() ([]FoundRepo, error) { 5 | return result, err 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /filecollector/repofinder/find_test.go: -------------------------------------------------------------------------------- 1 | package repofinder 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "github.com/google/go-cmp/cmp" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestFind(t *testing.T) { 13 | testDir := setUpTestDir() 14 | 15 | findNested := New(testDir, ".") 16 | 17 | actual, err := findNested() 18 | if err != nil { 19 | t.Errorf("want nil, got %#v", err) 20 | return 21 | } 22 | 23 | expected := []FoundRepo{ 24 | {RepositoryTypeIsNested, "nested1"}, 25 | {RepositoryTypeIsNested, typedpath.NewRawPath("nested1", "nestedInNested1")}, 26 | {RepositoryTypeIsSubmodule, "nested2"}, 27 | {RepositoryTypeIsSubmodule, typedpath.NewRawPath("nested2", "nestedInNested2")}, 28 | } 29 | if !reflect.DeepEqual(actual, expected) { 30 | t.Error(cmp.Diff(expected, actual)) 31 | return 32 | } 33 | } 34 | 35 | func TestFindOnRel(t *testing.T) { 36 | testDir := setUpTestDir() 37 | 38 | findNested := New(testDir, "nested1") 39 | 40 | actual, err := findNested() 41 | if err != nil { 42 | t.Errorf("want nil, got %#v", err) 43 | return 44 | } 45 | 46 | expected := []FoundRepo{ 47 | {RepositoryTypeIsNested, "nested1"}, 48 | {RepositoryTypeIsNested, typedpath.NewRawPath("nested1", "nestedInNested1")}, 49 | } 50 | if !reflect.DeepEqual(actual, expected) { 51 | t.Error(cmp.Diff(expected, actual)) 52 | return 53 | } 54 | } 55 | 56 | func setUpTestDir() typedpath.RawPath { 57 | workDir, err := os.MkdirTemp(os.TempDir(), "unity-meta-check-tests.") 58 | if err != nil { 59 | panic(err.Error()) 60 | } 61 | 62 | mkdirp(workDir, ".git") 63 | mkdirp(workDir, "nested1") 64 | mkdirp(workDir, "nested1", ".git") 65 | mkdirp(workDir, "nested1", "nestedInNested1") 66 | mkdirp(workDir, "nested1", "nestedInNested1", ".git") 67 | mkdirp(workDir, "nested2") 68 | touch(workDir, "nested2", ".git") // Means submodule. 69 | mkdirp(workDir, "nested2", "nestedInNested2") 70 | touch(workDir, "nested2", "nestedInNested2", ".git") // Means submodule. 71 | mkdirp(workDir, "others") 72 | 73 | return typedpath.NewRawPathUnsafe(workDir) 74 | } 75 | 76 | func mkdirp(cwd string, path ...string) { 77 | path = append([]string{cwd}, path...) 78 | if err := os.Mkdir(filepath.Join(path...), 0755); err != nil { 79 | panic(err.Error()) 80 | } 81 | } 82 | 83 | func touch(cwd string, path ...string) { 84 | path = append([]string{cwd}, path...) 85 | file, err := os.Create(filepath.Join(path...)) 86 | if err != nil { 87 | panic(err.Error()) 88 | } 89 | if err := file.Close(); err != nil { 90 | panic(err.Error()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /filecollector/repofinder/foundrepo.go: -------------------------------------------------------------------------------- 1 | package repofinder 2 | 3 | import "github.com/DeNA/unity-meta-check/util/typedpath" 4 | 5 | type FoundRepo struct { 6 | Type RepositoryType 7 | RawPath typedpath.RawPath 8 | } 9 | -------------------------------------------------------------------------------- /git/config.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/util/logging" 7 | "io" 8 | "os/exec" 9 | "sync" 10 | ) 11 | 12 | type GlobalConfig func(stdoutWriter io.WriteCloser, args ...string) error 13 | 14 | func NewGlobalConfig(logger logging.Logger) GlobalConfig { 15 | return func(stdoutWriter io.WriteCloser, args ...string) error { 16 | configWithOpts := append([]string{"config", "--global"}, args...) 17 | cmd := exec.Command("git", configWithOpts...) 18 | cmd.Stdout = stdoutWriter 19 | defer func() { _ = stdoutWriter.Close() }() 20 | 21 | stderr, err := cmd.StderrPipe() 22 | if err != nil { 23 | return err 24 | } 25 | 26 | var wg sync.WaitGroup 27 | wg.Add(1) 28 | go func() { 29 | defer wg.Done() 30 | scanner := bufio.NewScanner(stderr) 31 | for scanner.Scan() { 32 | logger.Debug(fmt.Sprintf("stderr: %s", scanner.Text())) 33 | } 34 | }() 35 | 36 | err = cmd.Run() 37 | wg.Wait() 38 | return err 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /git/config_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/logging" 5 | "github.com/DeNA/unity-meta-check/util/testutil" 6 | "testing" 7 | ) 8 | 9 | func TestGlobalConfig(t *testing.T) { 10 | spy := testutil.SpyWriteCloser(nil) 11 | spyLogger := logging.SpyLogger() 12 | 13 | globalConfig := NewGlobalConfig(spyLogger) 14 | 15 | if err := globalConfig(spy, "--default", "", "--get", "safe.directory"); err != nil { 16 | t.Errorf("want nil, got %#v", err) 17 | t.Log(spyLogger.Logs.String()) 18 | return 19 | } 20 | 21 | if !spy.IsClosed { 22 | t.Error("want true, but false") 23 | return 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /git/lsfiles.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/util/logging" 7 | "github.com/DeNA/unity-meta-check/util/typedpath" 8 | "io" 9 | "os/exec" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | type LsFiles func(repoDir typedpath.RawPath, options []string, stdoutWriter io.WriteCloser) error 15 | 16 | func NewLsFiles(logger logging.Logger) LsFiles { 17 | return func(repoDir typedpath.RawPath, options []string, stdoutWriter io.WriteCloser) error { 18 | subcmdWithOpts := append([]string{"-c", "core.quotepath=false", "ls-files"}, options...) 19 | logger.Debug(fmt.Sprintf("exec: git %s (on %q)", strings.Join(subcmdWithOpts, " "), repoDir)) 20 | 21 | cmd := exec.Command("git", subcmdWithOpts...) 22 | cmd.Dir = string(repoDir) 23 | cmd.Stdout = stdoutWriter 24 | defer func(){ _ = stdoutWriter.Close() }() 25 | 26 | stderr, err := cmd.StderrPipe() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | var wg sync.WaitGroup 32 | wg.Add(1) 33 | go func() { 34 | defer wg.Done() 35 | scanner := bufio.NewScanner(stderr) 36 | for scanner.Scan() { 37 | logger.Debug(fmt.Sprintf("stderr: %s", scanner.Text())) 38 | } 39 | }() 40 | 41 | err = cmd.Run() 42 | wg.Wait() 43 | return err 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /git/lsfiles_stub.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | func FakeLsFiles(lsFiles func(repoDir typedpath.RawPath)([]string, error)) LsFiles { 10 | return func(repoDir typedpath.RawPath, options []string, stdoutWriter io.WriteCloser) error { 11 | files, err := lsFiles(repoDir) 12 | if err != nil { 13 | return err 14 | } 15 | _, _ = io.WriteString(stdoutWriter, strings.Join(files, "\n")) 16 | _ = stdoutWriter.Close() 17 | return err 18 | } 19 | } 20 | 21 | func StubLsFiles(files []string, err error) LsFiles { 22 | return StubRawLsFiles(strings.Join(files, "\n") + "\n", err) 23 | } 24 | 25 | func StubRawLsFiles(stdout string, err error) LsFiles { 26 | return func(repoDir typedpath.RawPath, options []string, stdoutWriter io.WriteCloser) error { 27 | _, _ = io.WriteString(stdoutWriter, stdout) 28 | _ = stdoutWriter.Close() 29 | return err 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /git/lsfiles_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/logging" 5 | "github.com/DeNA/unity-meta-check/util/testutil" 6 | "testing" 7 | ) 8 | 9 | func TestLsFiles(t *testing.T) { 10 | spy := testutil.SpyWriteCloser(nil) 11 | spyLogger := logging.SpyLogger() 12 | 13 | lsFiles := NewLsFiles(spyLogger) 14 | 15 | if err := lsFiles(".", []string{}, spy); err != nil { 16 | t.Errorf("want nil, got %#v", err) 17 | return 18 | } 19 | 20 | stdout := spy.Captured.String() 21 | if len(stdout) == 0 { 22 | t.Log(spyLogger.Logs.String()) 23 | t.Error("want stdout > 0, but == 0") 24 | return 25 | } 26 | if !spy.IsClosed { 27 | t.Error("want true, but false") 28 | return 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /git/revparse.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/util/logging" 7 | "github.com/pkg/errors" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | type RevParse func(repoDir string, options ...string) (string, error) 13 | 14 | func NewRevParse(logger logging.Logger) RevParse { 15 | return func(repoDir string, options ...string) (string, error) { 16 | subcmdWithOpts := append([]string{"-c", "core.quotepath=false", "rev-parse"}, options...) 17 | logger.Debug(fmt.Sprintf("exec: git %s (on %q)", strings.Join(subcmdWithOpts, " "), repoDir)) 18 | 19 | stdoutBuf := &bytes.Buffer{} 20 | stderrBuf := &bytes.Buffer{} 21 | 22 | cmd := exec.Command("git", subcmdWithOpts...) 23 | cmd.Dir = repoDir 24 | cmd.Stdout = stdoutBuf 25 | cmd.Stderr = stderrBuf 26 | 27 | if err := cmd.Run(); err != nil { 28 | return "", errors.Wrapf(err, "failed to run git rev-parse:\nstderr:%s", stderrBuf.String()) 29 | } 30 | 31 | return strings.TrimSpace(stdoutBuf.String()), nil 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /git/revparse_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/logging" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestNewRevParse(t *testing.T) { 10 | spyLogger := logging.SpyLogger() 11 | 12 | revParse := NewRevParse(spyLogger) 13 | 14 | actual, err := revParse(".", "--show-toplevel") 15 | 16 | if err != nil { 17 | t.Log(spyLogger.Logs.String()) 18 | t.Errorf("want nil, got %#v", err) 19 | return 20 | } 21 | 22 | if !filepath.IsAbs(actual) { 23 | t.Log(spyLogger.Logs.String()) 24 | t.Errorf("want an absolute file path, got %s", actual) 25 | return 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DeNA/unity-meta-check 2 | 3 | go 1.22.2 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.5 7 | github.com/pkg/errors v0.9.1 8 | github.com/scylladb/go-set v1.0.2 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= 2 | github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= 3 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 4 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= 6 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 7 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 8 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 9 | github.com/scylladb/go-set v1.0.2 h1:SkvlMCKhP0wyyct6j+0IHJkBkSZL+TDzZ4E7f7BCcRE= 10 | github.com/scylladb/go-set v1.0.2/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= 11 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 12 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 13 | -------------------------------------------------------------------------------- /ignore/file.go: -------------------------------------------------------------------------------- 1 | package ignore 2 | 3 | import ( 4 | "bufio" 5 | "github.com/DeNA/unity-meta-check/util/globs" 6 | "github.com/DeNA/unity-meta-check/util/typedpath" 7 | "io" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func ReadFile(path typedpath.RawPath) ([]globs.Glob, error) { 13 | file, err := os.Open(string(path)) 14 | if err != nil { 15 | return nil, err 16 | } 17 | defer func(){ _ = file.Close() }() 18 | return Read(file) 19 | } 20 | 21 | func Read(reader io.Reader) ([]globs.Glob, error) { 22 | result := make([]globs.Glob, 0) 23 | scanner := bufio.NewScanner(reader) 24 | for scanner.Scan() { 25 | line := scanner.Text() 26 | elementsRaw := strings.SplitN(line, "#", 2) 27 | globPattern := strings.Trim(strings.TrimSpace(elementsRaw[0]), "/") 28 | if globPattern != "" { 29 | result = append(result, globs.Glob(globPattern)) 30 | } 31 | } 32 | return result, nil 33 | } 34 | -------------------------------------------------------------------------------- /ignore/file_test.go: -------------------------------------------------------------------------------- 1 | package ignore 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/globs" 5 | "github.com/google/go-cmp/cmp" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestRead(t *testing.T) { 12 | ignoreFile := ` 13 | # this is a comment 14 | foo 15 | bar/path # also this is a comment 16 | baz/ # this will be trimmed 17 | /qux # also this will be trimmed 18 | ` 19 | 20 | reader := strings.NewReader(ignoreFile) 21 | actual, err := Read(reader) 22 | 23 | if err != nil { 24 | t.Errorf("want nil, got %#v", err) 25 | return 26 | } 27 | 28 | expected := []globs.Glob{"foo", "bar/path", "baz", "qux"} 29 | if !reflect.DeepEqual(actual, expected) { 30 | t.Error(cmp.Diff(expected, actual)) 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ignore/testdata/Assets/Bar/Baz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/ignore/testdata/Assets/Bar/Baz -------------------------------------------------------------------------------- /ignore/testdata/Assets/Foo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/ignore/testdata/Assets/Foo -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/git" 7 | "github.com/DeNA/unity-meta-check/options" 8 | "github.com/DeNA/unity-meta-check/report" 9 | "github.com/DeNA/unity-meta-check/resultfilter" 10 | "github.com/DeNA/unity-meta-check/unity" 11 | "github.com/DeNA/unity-meta-check/unity/checker" 12 | "github.com/DeNA/unity-meta-check/util/cli" 13 | "github.com/DeNA/unity-meta-check/util/logging" 14 | "github.com/DeNA/unity-meta-check/util/ostestable" 15 | "github.com/DeNA/unity-meta-check/version" 16 | "os" 17 | ) 18 | 19 | func main() { 20 | var cmd cli.Command 21 | profModeSwitch := os.Getenv("UNITY_META_CHECK_PROFILE") 22 | if profModeSwitch == "cpu" { 23 | cmd = cli.NewCommandWithCPUProfile(NewMain()) 24 | } else if profModeSwitch == "heap" { 25 | cmd = cli.NewCommandWithHeapProfile(NewMain()) 26 | } else if profModeSwitch != "" { 27 | println(fmt.Sprintf("unsupported profile mode: %q", profModeSwitch)) 28 | os.Exit(1) 29 | } else { 30 | cmd = NewMain() 31 | } 32 | 33 | exitStatus := cmd(os.Args[1:], cli.GetProcessInout(), cli.NewEnv()) 34 | os.Exit(int(exitStatus)) 35 | } 36 | 37 | func NewMain() cli.Command { 38 | return func(args []string, procInout cli.ProcessInout, env cli.Env) cli.ExitStatus { 39 | opts, err := options.ParseArgs(args, procInout) 40 | if err != nil { 41 | if err != flag.ErrHelp { 42 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 43 | } 44 | return cli.ExitAbnormal 45 | } 46 | 47 | if opts.Version { 48 | _, _ = fmt.Fprintln(procInout.Stdout, version.Version) 49 | return cli.ExitNormal 50 | } 51 | 52 | logger := logging.NewLogger(opts.LogLevel, procInout.Stderr) 53 | 54 | check := checker.NewChecker( 55 | checker.NewStrategySelector( 56 | unity.NewFindPackages(logger), 57 | git.NewLsFiles(logger), 58 | logger, 59 | ), 60 | logger, 61 | ) 62 | result, err := check( 63 | opts.RootDirAbs, 64 | &checker.Options{ 65 | IgnoreCase: opts.IgnoreCase, 66 | IgnoreSubmodulesAndNested: opts.IgnoreSubmodulesAndNested, 67 | TargetType: opts.TargetType, 68 | }, 69 | ) 70 | if err != nil { 71 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 72 | return cli.ExitAbnormal 73 | } 74 | 75 | filterFunc := resultfilter.NewFilter(ostestable.NewGetwd(), logger) 76 | filtered, err := filterFunc(result, &resultfilter.Options{ 77 | IgnoreDangling: opts.IgnoreDangling, 78 | IgnoredGlobs: opts.IgnoredPaths, 79 | }) 80 | if err != nil { 81 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 82 | return cli.ExitAbnormal 83 | } 84 | 85 | if err := report.WriteResult(procInout.Stdout, filtered); err != nil { 86 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 87 | return cli.ExitAbnormal 88 | } 89 | 90 | if !filtered.Empty() { 91 | return cli.ExitAbnormal 92 | } 93 | return cli.ExitNormal 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /options/env.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | const GitHubTokenEnv = "GITHUB_TOKEN" 4 | -------------------------------------------------------------------------------- /options/ignorefile.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/ignore" 5 | "github.com/DeNA/unity-meta-check/util/globs" 6 | "github.com/DeNA/unity-meta-check/util/logging" 7 | "github.com/DeNA/unity-meta-check/util/typedpath" 8 | "os" 9 | ) 10 | 11 | const IgnoreFileBasename typedpath.BaseName = ".meta-check-ignore" 12 | 13 | type IgnoredGlobsBuilder func(ignoreFilePath typedpath.RawPath, rootDirAbs typedpath.RawPath) ([]globs.Glob, error) 14 | 15 | func NewIgnoredGlobsBuilder(logger logging.Logger) IgnoredGlobsBuilder { 16 | return func(ignoreFilePath typedpath.RawPath, rootDirAbs typedpath.RawPath) ([]globs.Glob, error) { 17 | ignoredGlobs, err := ignore.ReadFile(getIgnoreFilePath(ignoreFilePath, rootDirAbs)) 18 | if err != nil { 19 | // NOTE: If it is a default value, missing .meta-check-ignore is allowed because it is optional. 20 | // Otherwise, treat as an error if specified ignoreFilePath is missing. 21 | if ignoreFilePath != "" || !os.IsNotExist(err) { 22 | return nil, err 23 | } 24 | logger.Info("no .meta-check-ignore, so ignored paths are empty") 25 | ignoredGlobs = []globs.Glob{} 26 | } 27 | return ignoredGlobs, nil 28 | } 29 | } 30 | 31 | func getIgnoreFilePath(path typedpath.RawPath, rootDirAbs typedpath.RawPath) typedpath.RawPath { 32 | if path == "" { 33 | return rootDirAbs.JoinBaseName(IgnoreFileBasename) 34 | } else { 35 | return path 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /options/ignorefile_stub.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/globs" 5 | "github.com/DeNA/unity-meta-check/util/typedpath" 6 | ) 7 | 8 | func StubIgnoredPathBuilder(result []globs.Glob, err error) IgnoredGlobsBuilder { 9 | return func(typedpath.RawPath, typedpath.RawPath) ([]globs.Glob, error) { 10 | return result, err 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /options/ignorefile_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/globs" 5 | "github.com/DeNA/unity-meta-check/util/logging" 6 | "github.com/DeNA/unity-meta-check/util/typedpath" 7 | "github.com/google/go-cmp/cmp" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestNewIgnoredPathsBuilderSpecifiedAndIgnoreFileExists(t *testing.T) { 13 | spyLogger := logging.SpyLogger() 14 | buildIgnorePaths := NewIgnoredGlobsBuilder(spyLogger) 15 | 16 | actual, err := buildIgnorePaths( 17 | typedpath.NewRawPath("testdata", "ignorefile", "ProjectHasMetaCheckIgnore", ".meta-check-ignore"), 18 | "/path/to/any", 19 | ) 20 | 21 | if err != nil { 22 | t.Errorf("want nil, got %#v", err) 23 | return 24 | } 25 | 26 | expected := []globs.Glob{ 27 | "path/to/ignored", 28 | } 29 | if !reflect.DeepEqual(actual, expected) { 30 | t.Log(spyLogger.Logs.String()) 31 | t.Error(cmp.Diff(expected, actual)) 32 | return 33 | } 34 | } 35 | 36 | func TestNewIgnoredPathsBuilderSpecifiedButIgnoreFileDoesNotExist(t *testing.T) { 37 | spyLogger := logging.SpyLogger() 38 | buildIgnorePaths := NewIgnoredGlobsBuilder(spyLogger) 39 | 40 | _, err := buildIgnorePaths( 41 | typedpath.NewRawPath("testdata", "ignorefile", "ProjectDoesNotHaveMetaCheckIgnore", ".meta-check-ignore"), 42 | "/path/to/any", 43 | ) 44 | 45 | if err == nil { 46 | t.Log(spyLogger.Logs.String()) 47 | t.Error("want error, got nil") 48 | return 49 | } 50 | } 51 | 52 | func TestNewIgnoredPathsBuilderOmitButIgnoreFileExists(t *testing.T) { 53 | spyLogger := logging.SpyLogger() 54 | buildIgnorePaths := NewIgnoredGlobsBuilder(spyLogger) 55 | 56 | actual, err := buildIgnorePaths( 57 | "", 58 | typedpath.NewRawPath("testdata", "ignorefile", "ProjectHasMetaCheckIgnore"), 59 | ) 60 | 61 | if err != nil { 62 | t.Log(spyLogger.Logs.String()) 63 | t.Errorf("want nil, got %#v", err) 64 | return 65 | } 66 | 67 | expected := []globs.Glob{ 68 | "path/to/ignored", 69 | } 70 | if !reflect.DeepEqual(actual, expected) { 71 | t.Log(spyLogger.Logs.String()) 72 | t.Error(cmp.Diff(expected, actual)) 73 | return 74 | } 75 | } 76 | 77 | func TestNewIgnoredPathsBuilderOmitAndIgnoreFileDoesNotExist(t *testing.T) { 78 | spyLogger := logging.SpyLogger() 79 | buildIgnorePaths := NewIgnoredGlobsBuilder(spyLogger) 80 | 81 | actual, err := buildIgnorePaths( 82 | "", 83 | typedpath.NewRawPath("testdata", "meta-check-ignore", "ProjectDoesNotHaveMetaCheckIgnore"), 84 | ) 85 | 86 | if err != nil { 87 | t.Log(spyLogger.Logs.String()) 88 | t.Errorf("want nil, got %#v", err) 89 | return 90 | } 91 | 92 | expected := make([]globs.Glob, 0) 93 | if !reflect.DeepEqual(actual, expected) { 94 | t.Log(spyLogger.Logs.String()) 95 | t.Error(cmp.Diff(expected, actual)) 96 | return 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/unity/checker" 5 | "github.com/DeNA/unity-meta-check/util/globs" 6 | "github.com/DeNA/unity-meta-check/util/logging" 7 | "github.com/DeNA/unity-meta-check/util/typedpath" 8 | ) 9 | 10 | type Options struct { 11 | Version bool 12 | LogLevel logging.Severity 13 | TargetType checker.TargetType 14 | IgnoreDangling bool 15 | IgnoreCase bool 16 | IgnoreSubmodulesAndNested bool 17 | IgnoredPaths []globs.Glob 18 | RootDirAbs typedpath.RawPath 19 | } 20 | -------------------------------------------------------------------------------- /options/rootdir.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/git" 6 | "github.com/DeNA/unity-meta-check/util/logging" 7 | "github.com/DeNA/unity-meta-check/util/ostestable" 8 | "github.com/DeNA/unity-meta-check/util/typedpath" 9 | "github.com/pkg/errors" 10 | "path/filepath" 11 | ) 12 | 13 | type RootDirCompletion func() (typedpath.RawPath, error) 14 | 15 | func NewRootDirCompletion(gitRevParse git.RevParse, logger logging.Logger) RootDirCompletion { 16 | return func() (typedpath.RawPath, error) { 17 | assumedRootDir, err := gitRevParse(".", "--show-toplevel") 18 | if err != nil { 19 | return "", errors.Wrap(err, "rootDir not specified and seems not being in any git repositories") 20 | } 21 | 22 | logger.Debug(fmt.Sprintf("rootDir not specified, so assumed by $(git rev-parse --show-toplevel): %q", assumedRootDir)) 23 | return typedpath.NewRawPathUnsafe(assumedRootDir), nil 24 | } 25 | } 26 | 27 | type RootDirAbsValidator func(unsafeRootDir typedpath.RawPath) (typedpath.RawPath, error) 28 | 29 | func NewRootDirValidator(isDir ostestable.IsDir) RootDirAbsValidator { 30 | return func(unsafeRootDir typedpath.RawPath) (typedpath.RawPath, error) { 31 | rootDirAbsStr, err := filepath.Abs(string(unsafeRootDir)) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | rootDirAbs := typedpath.NewRawPathUnsafe(rootDirAbsStr) 37 | 38 | ok, err := isDir(rootDirAbs) 39 | if err != nil { 40 | return "", errors.Wrapf(err, "cannot check directory: %q", rootDirAbs) 41 | } 42 | if !ok { 43 | return "", fmt.Errorf("root directory must be a directory: %s", rootDirAbsStr) 44 | } 45 | 46 | return rootDirAbs, nil 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /options/rootdir_stub.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "strings" 6 | ) 7 | 8 | func StubRootDirCompletion(path typedpath.RawPath, err error) RootDirCompletion { 9 | return func() (typedpath.RawPath, error) { 10 | return path, err 11 | } 12 | } 13 | 14 | func StubRootDirValidator(path typedpath.RawPath, err error) RootDirAbsValidator { 15 | return func(_ typedpath.RawPath) (typedpath.RawPath, error) { 16 | return path, err 17 | } 18 | } 19 | 20 | func FakeRootDirValidator(cwd typedpath.RawPath) RootDirAbsValidator { 21 | return func(unsafeRootDir typedpath.RawPath) (typedpath.RawPath, error) { 22 | if strings.HasPrefix(string(unsafeRootDir), "/") { 23 | return unsafeRootDir, nil 24 | } 25 | 26 | return cwd.JoinRawPath(unsafeRootDir), nil 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /options/rootdir_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "testing" 6 | ) 7 | 8 | func TestFakeRootDirValidator(t *testing.T) { 9 | cases := map[string]struct{ 10 | Cwd typedpath.SlashPath 11 | RootDir typedpath.SlashPath 12 | Expected typedpath.SlashPath 13 | } { 14 | "already absolute": { 15 | RootDir: "/already/absolute", 16 | Expected: "/already/absolute", 17 | }, 18 | "only .": { 19 | Cwd: "/path/to/cwd", 20 | RootDir: ".", 21 | Expected: "/path/to/cwd", 22 | }, 23 | "relative start with ./": { 24 | Cwd: "/path/to/cwd", 25 | RootDir: "./rel", 26 | Expected: "/path/to/cwd/rel", 27 | }, 28 | "relative not start with ./": { 29 | Cwd: "/path/to/cwd", 30 | RootDir: "rel", 31 | Expected: "/path/to/cwd/rel", 32 | }, 33 | } 34 | 35 | for desc, c := range cases { 36 | t.Run(desc, func(t *testing.T) { 37 | validateRootDir := FakeRootDirValidator(c.Cwd.ToRaw()) 38 | actual, err := validateRootDir(c.RootDir.ToRaw()) 39 | if err != nil { 40 | t.Errorf("want nil, got %#v", err) 41 | return 42 | } 43 | 44 | if actual.ToSlash() != c.Expected { 45 | t.Errorf("want %q, got %q", c.Expected, actual.ToSlash()) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /options/testdata/ignorefile/ProjectDoesNotHaveMetaCheckIgnore/.gitempty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/options/testdata/ignorefile/ProjectDoesNotHaveMetaCheckIgnore/.gitempty -------------------------------------------------------------------------------- /options/testdata/ignorefile/ProjectHasMetaCheckIgnore/.meta-check-ignore: -------------------------------------------------------------------------------- 1 | path/to/ignored -------------------------------------------------------------------------------- /options/unityproject.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/unity" 6 | "github.com/DeNA/unity-meta-check/unity/checker" 7 | "github.com/DeNA/unity-meta-check/util/logging" 8 | "github.com/DeNA/unity-meta-check/util/typedpath" 9 | "github.com/pkg/errors" 10 | "os" 11 | ) 12 | 13 | type UnityProjectDetector func(rootDirAbs typedpath.RawPath) (checker.TargetType, error) 14 | 15 | func NewUnityProjectDetector(logger logging.Logger) UnityProjectDetector { 16 | return func(rootDirAbs typedpath.RawPath) (checker.TargetType, error) { 17 | isUnityProj, err := hasUnityProjectSpecificDirectory(rootDirAbs, logger) 18 | if err != nil { 19 | return "", errors.Wrap(err, "automatic check mode detection was failed") 20 | } 21 | 22 | if isUnityProj { 23 | return checker.TargetTypeIsUnityProjectRootDirectory, nil 24 | } 25 | return checker.TargetTypeIsUnityProjectSubDirectory, nil 26 | } 27 | } 28 | 29 | func hasUnityProjectSpecificDirectory(rootDirAbs typedpath.RawPath, logger logging.Logger) (bool, error) { 30 | assetsDir := rootDirAbs.JoinBaseName(unity.AssetsDirBaseName) 31 | _, err := os.Stat(string(assetsDir)) 32 | if err != nil { 33 | if os.IsNotExist(err) { 34 | logger.Debug(fmt.Sprintf("seems a not Unity Project because %s is not found.", assetsDir)) 35 | return false, nil 36 | } 37 | return false, err 38 | } 39 | logger.Debug(fmt.Sprintf("seems an Unity Project because %s is found.", assetsDir)) 40 | return true, nil 41 | } 42 | -------------------------------------------------------------------------------- /options/unityproject_stub.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/unity/checker" 5 | "github.com/DeNA/unity-meta-check/util/typedpath" 6 | ) 7 | 8 | func StubUnityProjectDetector(result checker.TargetType, err error) UnityProjectDetector { 9 | return func(_ typedpath.RawPath) (checker.TargetType, error) { 10 | return result, err 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /report/parser.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "bufio" 5 | "github.com/DeNA/unity-meta-check/unity/checker" 6 | "github.com/DeNA/unity-meta-check/util/typedpath" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | type Parser func(reader io.Reader) *checker.CheckResult 12 | 13 | func NewParser() Parser { 14 | return func(reader io.Reader) *checker.CheckResult { 15 | scanner := bufio.NewScanner(reader) 16 | 17 | missingMeta := make([]typedpath.SlashPath, 0) 18 | danglingMeta := make([]typedpath.SlashPath, 0) 19 | 20 | for scanner.Scan() { 21 | line := scanner.Text() 22 | 23 | if strings.HasPrefix(line, MissingMetaLinePrefix) { 24 | missingMeta = append(missingMeta, typedpath.NewSlashPathUnsafe(strings.TrimPrefix(line, MissingMetaLinePrefix))) 25 | continue 26 | } 27 | 28 | if strings.HasPrefix(line, DanglingMetaPrefix) { 29 | danglingMeta = append(danglingMeta, typedpath.NewSlashPathUnsafe(strings.TrimPrefix(line, DanglingMetaPrefix))) 30 | continue 31 | } 32 | } 33 | 34 | return &checker.CheckResult{ 35 | MissingMeta: missingMeta, 36 | DanglingMeta: danglingMeta, 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /report/parser_test.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/unity/checker" 7 | "github.com/DeNA/unity-meta-check/util/typedpath" 8 | "github.com/google/go-cmp/cmp" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func TestNewLineReporterParser(t *testing.T) { 14 | cases := []*checker.CheckResult{ 15 | checker.NewCheckResult([]typedpath.SlashPath{}, []typedpath.SlashPath{}), 16 | checker.NewCheckResult([]typedpath.SlashPath{ 17 | "path/to/missing1.meta", 18 | "path/to/missing2.meta", 19 | }, []typedpath.SlashPath{}), 20 | checker.NewCheckResult([]typedpath.SlashPath{}, []typedpath.SlashPath{ 21 | "path/to/dangling1.meta", 22 | "path/to/dangling2.meta", 23 | }), 24 | checker.NewCheckResult( 25 | []typedpath.SlashPath{ 26 | "path/to/missing1.meta", 27 | "path/to/missing2.meta", 28 | }, 29 | []typedpath.SlashPath{ 30 | "path/to/dangling1.meta", 31 | "path/to/dangling2.meta", 32 | }, 33 | ), 34 | } 35 | 36 | for _, result := range cases { 37 | t.Run(fmt.Sprintf("%v", result), func(t *testing.T) { 38 | buf := &bytes.Buffer{} 39 | 40 | if err := WriteResult(buf, result); err != nil { 41 | t.Errorf("want nil, got %#v", err) 42 | return 43 | } 44 | 45 | parse := NewParser() 46 | actual := parse(buf) 47 | expected := result 48 | 49 | if !reflect.DeepEqual(actual, expected) { 50 | t.Error(cmp.Diff(expected, actual)) 51 | return 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /report/printer.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/unity/checker" 6 | "io" 7 | ) 8 | 9 | const ( 10 | MissingMetaLinePrefix = "missing " 11 | DanglingMetaPrefix = "dangling " 12 | ) 13 | 14 | func WriteResult(writer io.Writer, result *checker.CheckResult) error { 15 | if len(result.MissingMeta) > 0 { 16 | for _, missing := range result.MissingMeta { 17 | if _, err := io.WriteString(writer, fmt.Sprintf("%s%s\n", MissingMetaLinePrefix, missing)); err != nil { 18 | return err 19 | } 20 | } 21 | } 22 | 23 | if len(result.DanglingMeta) > 0 { 24 | for _, dangling := range result.DanglingMeta { 25 | if _, err := io.WriteString(writer, fmt.Sprintf("%s%s\n", DanglingMetaPrefix, dangling)); err != nil { 26 | return err 27 | } 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /resultfilter/resultfilter.go: -------------------------------------------------------------------------------- 1 | package resultfilter 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/unity/checker" 6 | "github.com/DeNA/unity-meta-check/util/globs" 7 | "github.com/DeNA/unity-meta-check/util/logging" 8 | "github.com/DeNA/unity-meta-check/util/ostestable" 9 | "github.com/DeNA/unity-meta-check/util/typedpath" 10 | ) 11 | 12 | type Options struct { 13 | IgnoreDangling bool 14 | IgnoredGlobs []globs.Glob 15 | IgnoreCase bool 16 | } 17 | 18 | type Filter func(result *checker.CheckResult, opts *Options) (*checker.CheckResult, error) 19 | 20 | func NewFilter(getwd ostestable.Getwd, logger logging.Logger) Filter { 21 | return func(result *checker.CheckResult, opts *Options) (*checker.CheckResult, error) { 22 | var ignored bool 23 | var matched globs.Glob 24 | rawWd, err := getwd() 25 | if err != nil { 26 | return nil, err 27 | } 28 | wd := rawWd.ToSlash() 29 | 30 | newMissingMeta := make([]typedpath.SlashPath, 0) 31 | for _, missingMeta := range result.MissingMeta { 32 | ignored, matched, err = globs.MatchAny(missingMeta, opts.IgnoredGlobs, wd) 33 | if err != nil { 34 | return nil, err 35 | } 36 | if ignored { 37 | logger.Debug(fmt.Sprintf("ignored missing: %q by %q", missingMeta, matched)) 38 | continue 39 | } 40 | newMissingMeta = append(newMissingMeta, missingMeta) 41 | } 42 | 43 | newDanglingMeta := make([]typedpath.SlashPath, 0) 44 | if !opts.IgnoreDangling { 45 | for _, danglingMeta := range result.DanglingMeta { 46 | ignored, matched, err = globs.MatchAny(danglingMeta, opts.IgnoredGlobs, wd) 47 | if err != nil { 48 | return nil, err 49 | } 50 | if ignored { 51 | logger.Debug(fmt.Sprintf("ignored dangling: %q by %q", danglingMeta, matched)) 52 | continue 53 | } 54 | newDanglingMeta = append(newDanglingMeta, danglingMeta) 55 | } 56 | } 57 | 58 | return checker.NewCheckResult(newMissingMeta, newDanglingMeta), nil 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/compat: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | 5 | throw() { 6 | local message="$*" 7 | printf "error: %s\n" "$message" 1>&2 8 | false 9 | } 10 | 11 | 12 | error_help() { 13 | local message="$*" 14 | printf "error: %s\n" "$message" 1>&2 15 | usage 16 | false 17 | } 18 | 19 | 20 | usage() { 21 | echo 'usage: compat ' 22 | echo 23 | echo 'ARGUMENTS' 24 | echo ' absolute path to old unity-meta-check executable' 25 | echo ' absolute path to new unity-meta-check executable' 26 | echo ' path to directory where compatibility tests running on' 27 | echo ' arguments for unity-meta-check to give to both old and new' 28 | echo 29 | echo 'EXAMPLES' 30 | echo ' compat path/to/old/unity-meta-check path/to/new-unity-meta-check path/to/Unity/Project' 31 | } 32 | 33 | 34 | main() { 35 | local old_bin="${1:-}" 36 | local new_bin="${2:-}" 37 | local work_dir="${3:-}" 38 | shift 3 39 | 40 | "$old_bin" -version >/dev/null || error_help "old binary does not exist: $old_bin" 41 | "$new_bin" -version >/dev/null || error_help "new binary does not exist: $new_bin" 42 | 43 | [[ -d "$work_dir" ]] || error_help "working directory does not exist: $work_dir" 44 | (cd "$work_dir" 45 | git rev-parse --is-inside-work-tree >/dev/null || error_help "working directory is not in a git repository" 46 | 47 | local tmpdir 48 | tmpdir="$(mktemp -d "/tmp/unity-meta-check-compat.XXXXXX")" 49 | 50 | set +e 51 | "$old_bin" -silent "$@" > "${tmpdir}/old" 2>&1 52 | local old_status=$? 53 | 54 | "$new_bin" -silent "$@" > "${tmpdir}/new" 2>&1 55 | local new_status=$? 56 | set -e 57 | 58 | if [[ $old_status -eq $new_status ]]; then 59 | echo "ok: match exit status (old: $old_status == new: $new_status)" 60 | rm -r "$tmpdir" 61 | else 62 | echo "ng: exit status mismatch (old: $old_status != new: $new_status)" 63 | diff -u "${tmpdir}/old" "${tmpdir}/new" 64 | false 65 | fi 66 | ) 67 | } 68 | 69 | 70 | case "$*" in 71 | --help | -h) 72 | usage 73 | false ;; 74 | *) 75 | main "$@" ;; 76 | esac 77 | -------------------------------------------------------------------------------- /test/compat-revisions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | BASE_DIR="$(cd "$(dirname "$0")"; pwd)" 5 | 6 | 7 | throw() { 8 | local message="$*" 9 | printf "error: %s\n" "$message" 1>&2 10 | false 11 | } 12 | 13 | 14 | error_help() { 15 | local message="$*" 16 | printf "error: %s\n" "$message" 1>&2 17 | usage 18 | false 19 | } 20 | 21 | 22 | usage() { 23 | echo 'usage: compat-revisions []' 24 | echo 25 | echo 'ARGUMENTS' 26 | echo ' absolute path to old unity-meta-check executable' 27 | echo ' absolute path to new unity-meta-check executable' 28 | echo ' path to directory where compatibility tests running on' 29 | echo ' number of revisions from HEAD to test (default: 10)' 30 | echo ' arguments for unity-meta-check to give to both old and new' 31 | echo 32 | echo 'EXAMPLES' 33 | echo ' compat-revisions path/to/old/unity-meta-check path/to/new-unity-meta-check path/to/Unity/Project 100' 34 | } 35 | 36 | 37 | main() { 38 | local old_bin="${1:-}" 39 | local new_bin="${2:-}" 40 | local work_dir="${3:-}" 41 | local num="${4:-10}" 42 | shift 4 43 | 44 | "$old_bin" -version >/dev/null || error_help "old binary does not exist: $old_bin" 45 | "$new_bin" -version >/dev/null || error_help "new binary does not exist: $new_bin" 46 | 47 | [[ -d "$work_dir" ]] || error_help "working directory does not exist: $work_dir" 48 | (cd "$work_dir" 49 | git rev-parse --is-inside-work-tree >/dev/null || error_help "working directory is not in a git repository" 50 | 51 | local failed=0 52 | while read -r rev; do 53 | git checkout --quiet "$rev" 54 | git submodule sync --quiet 55 | git submodule update --quiet --init --recursive 56 | printf "%s: " "$rev" 57 | 58 | if ! "${BASE_DIR}/compat" "$old_bin" "$new_bin" "$work_dir" "$@"; then 59 | failed=1 60 | fi 61 | done < <(git rev-list -n "$num" HEAD) 62 | 63 | [[ $failed -eq 0 ]] 64 | ) 65 | } 66 | 67 | 68 | case "$*" in 69 | --help | -h) 70 | usage 71 | false ;; 72 | *) 73 | main "$@" ;; 74 | esac 75 | -------------------------------------------------------------------------------- /test/perf-compat: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | 5 | throw() { 6 | local message="$*" 7 | printf "error: %s\n" "$message" 1>&2 8 | false 9 | } 10 | 11 | 12 | error_help() { 13 | local message="$*" 14 | printf "error: %s\n" "$message" 1>&2 15 | usage 16 | false 17 | } 18 | 19 | 20 | usage() { 21 | echo 'usage: compat ' 22 | echo 23 | echo 'ARGUMENTS' 24 | echo ' path to old unity-meta-check executable' 25 | echo ' path to new unity-meta-check executable' 26 | echo ' path to directory where compatibility tests running on' 27 | echo 28 | echo 'EXAMPLES' 29 | echo ' compat path/to/old/unity-meta-check path/to/new-unity-meta-check path/to/Unity/Project' 30 | } 31 | 32 | 33 | main() { 34 | local old_bin="${1:-}" 35 | local new_bin="${2:-}" 36 | local work_dir="${3:-}" 37 | 38 | "$old_bin" -version >/dev/null || error_help "old binary does not exist: $old_bin" 39 | "$new_bin" -version >/dev/null || error_help "new binary does not exist: $new_bin" 40 | 41 | [[ -d "$work_dir" ]] || error_help "working directory does not exist: $work_dir" 42 | (cd "$work_dir" 43 | git rev-parse --is-inside-work-tree >/dev/null || error_help "working directory is not in a git repository" 44 | 45 | set +e 46 | "$old_bin" -silent >/dev/null 2>&1 47 | echo -n "old:" 48 | time "$old_bin" -silent 2>&1 49 | 50 | "$new_bin" -silent >/dev/null 2>&1 51 | echo -n "new:" 52 | time "$new_bin" -silent 2>&1 53 | set -e 54 | ) 55 | } 56 | 57 | 58 | main "$@" 59 | -------------------------------------------------------------------------------- /tool/gh-action/action-yaml-gen/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/tool/gh-action/action-yaml-gen/yaml" 7 | "github.com/DeNA/unity-meta-check/util/cli" 8 | "github.com/pkg/errors" 9 | "io" 10 | "os" 11 | ) 12 | 13 | func Main(args []string, procInout cli.ProcessInout, _ cli.Env) cli.ExitStatus { 14 | if len(args) != 1 { 15 | _, _ = fmt.Fprintf(procInout.Stderr, "error: must specify 1 argument, but come %d arguments\n", len(args)) 16 | return cli.ExitAbnormal 17 | } 18 | 19 | flags := flag.NewFlagSet("gh-action-yaml-gen", flag.ContinueOnError) 20 | flags.SetOutput(io.Discard) 21 | 22 | if err := flags.Parse(args); err != nil { 23 | _, _ = fmt.Fprintln(procInout.Stderr, "usage: gh-action-yaml-gen ") 24 | return cli.ExitAbnormal 25 | } 26 | 27 | actionYAMLPath := args[0] 28 | if err := writeYAML(actionYAMLPath); err != nil { 29 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 30 | return cli.ExitAbnormal 31 | } 32 | 33 | return cli.ExitNormal 34 | } 35 | 36 | func writeYAML(actionYAMLPath string) error { 37 | f, err := os.OpenFile(actionYAMLPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) 38 | if err != nil { 39 | return errors.Wrapf(err, "cannot open: %q", actionYAMLPath) 40 | } 41 | defer func(f *os.File) { 42 | _ = f.Close() 43 | }(f) 44 | 45 | if _, err := yaml.WriteTo(f); err != nil { 46 | return errors.Wrapf(err, "cannot write YAML to: %q", actionYAMLPath) 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /tool/gh-action/action-yaml-gen/cmd/cmd_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/cli" 5 | "github.com/DeNA/unity-meta-check/util/testutil" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestNewMain(t *testing.T) { 15 | tmpDir, err := ioutil.TempDir(os.TempDir(), "action-yaml-gen-test.*") 16 | if err != nil { 17 | t.Errorf("want nil, got %#v", err) 18 | return 19 | } 20 | 21 | stdin := io.NopCloser(strings.NewReader("")) 22 | stdout := testutil.SpyWriteCloser(nil) 23 | stderr := testutil.SpyWriteCloser(nil) 24 | procInput := cli.ProcessInout{ 25 | Stdin: stdin, 26 | Stdout: stdout, 27 | Stderr: stderr, 28 | } 29 | env := cli.StubEnv(map[string]string{}) 30 | 31 | actual := Main([]string{path.Join(tmpDir, "action.yml")}, procInput, env) 32 | 33 | if actual != cli.ExitNormal { 34 | t.Log(stdout.Captured.String()) 35 | t.Log(stderr.Captured.String()) 36 | t.Errorf("want %d, got %d", cli.ExitAbnormal, actual) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tool/gh-action/action-yaml-gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/tool/gh-action/action-yaml-gen/cmd" 5 | "github.com/DeNA/unity-meta-check/util/cli" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | exitStatus := cmd.Main(os.Args[1:], cli.GetProcessInout(), cli.NewEnv()) 11 | os.Exit(int(exitStatus)) 12 | } 13 | -------------------------------------------------------------------------------- /tool/gh-action/action-yaml-gen/testdata/action.yml: -------------------------------------------------------------------------------- 1 | ../../../../action.yml -------------------------------------------------------------------------------- /tool/gh-action/action-yaml-gen/yaml/inputs.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ( 8 | Name string 9 | Desc string 10 | 11 | InputDef interface { 12 | Name() Name 13 | Desc() Desc 14 | Required() bool 15 | DefaultValueAsYAML() string 16 | } 17 | ) 18 | 19 | type StringInputDef struct { 20 | name Name 21 | desc Desc 22 | required bool 23 | defaultValue string 24 | } 25 | 26 | func (s StringInputDef) Name() Name { 27 | return s.name 28 | } 29 | 30 | func (s StringInputDef) Desc() Desc { 31 | return s.desc 32 | } 33 | 34 | func (s StringInputDef) Required() bool { 35 | return s.required 36 | } 37 | 38 | func (s StringInputDef) DefaultValueAsYAML() string { 39 | return fmt.Sprintf("%q", s.defaultValue) 40 | } 41 | 42 | type BoolInputDef struct { 43 | name Name 44 | desc Desc 45 | required bool 46 | defaultValue bool 47 | } 48 | 49 | func (b BoolInputDef) Name() Name { 50 | return b.name 51 | } 52 | 53 | func (b BoolInputDef) Desc() Desc { 54 | return b.desc 55 | } 56 | 57 | func (b BoolInputDef) Required() bool { 58 | return b.required 59 | } 60 | 61 | func (b BoolInputDef) DefaultValueAsYAML() string { 62 | if b.defaultValue { 63 | return `"true"` 64 | } 65 | return `"false"` 66 | } 67 | 68 | type IntInputDef struct { 69 | name Name 70 | desc Desc 71 | required bool 72 | defaultValue int 73 | } 74 | 75 | func (u IntInputDef) Name() Name { 76 | return u.name 77 | } 78 | 79 | func (u IntInputDef) Desc() Desc { 80 | return u.desc 81 | } 82 | 83 | func (u IntInputDef) Required() bool { 84 | return u.required 85 | } 86 | 87 | func (u IntInputDef) DefaultValueAsYAML() string { 88 | return fmt.Sprintf("%q", fmt.Sprintf("%d", u.defaultValue)) 89 | } 90 | -------------------------------------------------------------------------------- /tool/gh-action/action-yaml-gen/yaml/inputs_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var _ InputDef = StringInputDef{} 8 | var _ InputDef = BoolInputDef{} 9 | var _ InputDef = IntInputDef{} 10 | 11 | func TestInputDefs(t *testing.T) { 12 | for i, inputDef := range BuildMetadata().Inputs { 13 | if inputDef.Name() == "" { 14 | t.Errorf("InputDefs[%d] must have a non-empty name", i) 15 | } 16 | 17 | if inputDef.Desc() == "" { 18 | t.Errorf("%q must have a non-empty description", inputDef.Name()) 19 | } 20 | 21 | if !inputDef.Required() && inputDef.DefaultValueAsYAML() == "" { 22 | t.Errorf("optional input %q must have a non-empty default value representation for YAML", inputDef.Name()) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tool/gh-action/action-yaml-gen/yaml/runs.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ImageName = "ghcr.io/dena/unity-meta-check/unity-meta-check-gh-action" 8 | 9 | func ImageWithTag(version string) DockerImage { 10 | return DockerImage(fmt.Sprintf("%s:%s", ImageName, version)) 11 | } 12 | 13 | type ( 14 | DockerImage string 15 | BrandingIcon string 16 | BrandingColor string 17 | 18 | DockerAction struct { 19 | // Image is the Docker image to use as the container to run the action. The value can be the Docker base image name, 20 | // a local Dockerfile in your repository, or a public image in Docker Hub or another registry. 21 | // To reference a Dockerfile local to your repository, the file must be named Dockerfile and you must use a path 22 | // relative to your action metadata file. The docker application will execute this file. 23 | // SEE: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsimage 24 | Image DockerImage 25 | 26 | // Env specifies a key/value map of environment variables to set in the container environment. 27 | // SEE: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsenv 28 | Env map[string]string 29 | 30 | // Args is an array of strings that define the inputs for a Docker container. Inputs can include hardcoded strings. 31 | // GitHub passes the args to the container's ENTRYPOINT when the container starts up. 32 | // The args are used in place of the CMD instruction in a Dockerfile. If you use CMD in your Dockerfile, use the guidelines ordered by preference: 33 | // 1. Document required arguments in the action's README and omit them from the CMD instruction. 34 | // 2. Use defaults that allow using the action without specifying any args. 35 | // 3. If the action exposes a --help flag, or something similar, use that to make your action self-documenting. 36 | // If you need to pass environment variables into an action, make sure your action runs a command shell to perform variable substitution. 37 | // For example, if your entrypoint attribute is set to "sh -c", args will be run in a command shell. 38 | // Alternatively, if your Dockerfile uses an ENTRYPOINT to run the same command ("sh -c"), 39 | // args will execute in a command shell. 40 | // 41 | // For more information about using the CMD instruction with GitHub Actions, see "Dockerfile support for GitHub Actions." 42 | // SEE: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsargs 43 | Args []string 44 | } 45 | ) 46 | -------------------------------------------------------------------------------- /tool/gh-action/action-yaml-gen/yaml/runs_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | const tagNamePrefix = "# image-name: " 13 | 14 | func TestImage(t *testing.T) { 15 | expected, err := getActualImageName("../../../../.github/images/Dockerfile") 16 | if err != nil { 17 | t.Errorf("want nil, got %#v", err) 18 | return 19 | } 20 | 21 | if ImageName != expected { 22 | t.Errorf("want %q, got %q", expected, ImageName) 23 | } 24 | } 25 | 26 | func getActualImageName(dockerfilePath string) (DockerImage, error) { 27 | dockerfile, err := os.OpenFile(dockerfilePath, os.O_RDONLY, 0) 28 | if err != nil { 29 | return "", err 30 | } 31 | defer func(dockerfile *os.File) { 32 | _ = dockerfile.Close() 33 | }(dockerfile) 34 | 35 | scanner := bufio.NewScanner(dockerfile) 36 | if !scanner.Scan() { 37 | return "", errors.New("want read a first line, got no lines") 38 | } 39 | 40 | firstLine := scanner.Text() 41 | 42 | if !strings.HasPrefix(firstLine, tagNamePrefix) { 43 | return "", fmt.Errorf("want a tag name prefix comment at first line, got no prefix: %q", firstLine) 44 | } 45 | 46 | return DockerImage(strings.TrimPrefix(firstLine, tagNamePrefix)), nil 47 | } 48 | -------------------------------------------------------------------------------- /tool/gh-action/action-yaml-gen/yaml/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "bytes" 5 | "github.com/DeNA/unity-meta-check/version" 6 | "github.com/google/go-cmp/cmp" 7 | "os" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | // NOTE: This test is fragile, but we can use like Golden Testing. 13 | func TestRecentActionYAML(t *testing.T) { 14 | actualRawBytes, err := os.ReadFile("../testdata/action.yml") 15 | if err != nil { 16 | t.Errorf("want nil, got %v", err) 17 | return 18 | } 19 | 20 | // NOTE: On Windows, Git may replace \n to \r\n on specific configurations. 21 | actual := strings.NewReplacer("\r", "", "%VERSION%", version.Version).Replace(string(actualRawBytes)) 22 | 23 | buf := &bytes.Buffer{} 24 | if _, err := WriteTo(buf); err != nil { 25 | t.Errorf("want nil, got %#v", err) 26 | return 27 | } 28 | 29 | if buf.String() != actual { 30 | t.Error(cmp.Diff(buf.String(), actual)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tool/gh-action/inputs/env.go: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/options" 6 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/github" 7 | "github.com/DeNA/unity-meta-check/util/cli" 8 | "github.com/DeNA/unity-meta-check/util/typedpath" 9 | "strings" 10 | ) 11 | 12 | type ActionEnv struct { 13 | GitHubToken github.Token 14 | EventPath typedpath.RawPath 15 | Workspace typedpath.RawPath 16 | APIURL string 17 | } 18 | 19 | // SEE: https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables 20 | const ( 21 | // GitHubEventPath is the path of the file with the complete webhook event payload. For example, /github/workflow/event.json. 22 | GitHubEventPath = "GITHUB_EVENT_PATH" 23 | 24 | // GitHubWorkspace is the GitHub workspace directory path. The workspace directory is a copy of your repository 25 | // if your workflow uses the actions/checkout action. If you don't use the actions/checkout action, 26 | // the directory will be empty. For example, /home/runner/work/my-repo-name/my-repo-name. 27 | GitHubWorkspace = "GITHUB_WORKSPACE" 28 | 29 | // GitHubAPIURL returns the API URL. For example: https://api.github.com. 30 | GitHubAPIURL = "GITHUB_API_URL" 31 | ) 32 | 33 | func GetActionEnv(env cli.Env) ActionEnv { 34 | gitHubToken := env(options.GitHubTokenEnv) 35 | eventPath := env(GitHubEventPath) 36 | workspace := env(GitHubWorkspace) 37 | apiURL := env(GitHubAPIURL) 38 | 39 | return ActionEnv{ 40 | GitHubToken: github.Token(gitHubToken), 41 | EventPath: typedpath.NewRawPathUnsafe(eventPath), 42 | Workspace: typedpath.NewRawPathUnsafe(workspace), 43 | APIURL: apiURL, 44 | } 45 | } 46 | 47 | func MaskedActionEnv(env ActionEnv) string { 48 | return fmt.Sprintf(` 49 | TOKEN=%q (len=%d) 50 | GITHUB_EVENT_PATH=%q 51 | GITHUB_WORKSPACE=%q 52 | GITHUB_API_URL=%q`[1:], strings.Repeat("*", len(env.GitHubToken)), len(env.GitHubToken), env.EventPath, env.Workspace, env.APIURL) 53 | } 54 | -------------------------------------------------------------------------------- /tool/gh-action/inputs/event.go: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/github" 7 | "github.com/DeNA/unity-meta-check/util/logging" 8 | "github.com/DeNA/unity-meta-check/util/typedpath" 9 | "github.com/pkg/errors" 10 | "os" 11 | ) 12 | 13 | type ReadEventPayloadFunc func(path typedpath.RawPath) (*PushOrPullRequestEventPayload, error) 14 | 15 | func NewReadEventPayload(logger logging.Logger) ReadEventPayloadFunc { 16 | return func(path typedpath.RawPath) (*PushOrPullRequestEventPayload, error) { 17 | payloadBytes, err := os.ReadFile(string(path)) 18 | if err != nil { 19 | return nil, errors.Wrapf(err, "cannot read file: %q", path) 20 | } 21 | 22 | logger.Debug(fmt.Sprintf("event=%s", string(payloadBytes))) 23 | 24 | var payload PushOrPullRequestEventPayload 25 | if err := json.Unmarshal(payloadBytes, &payload); err != nil { 26 | return nil, errors.Wrapf(err, "cannot decode file: %q\n%s", path, string(payloadBytes)) 27 | } 28 | 29 | return &payload, nil 30 | } 31 | } 32 | 33 | // PushOrPullRequestEventPayload is a payload for pull request events. 34 | // SEE: https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request 35 | type PushOrPullRequestEventPayload struct { 36 | PullRequest *PullRequest `json:"pull_request,omitempty"` 37 | Repository *Repository `json:"repository,omitempty"` 38 | } 39 | 40 | // PullRequest is a payload for pull requests. 41 | // SEE: https://docs.github.com/en/rest/reference/pulls#get-a-pull-request 42 | type PullRequest struct { 43 | Number github.PullNumber `json:"number"` 44 | } 45 | 46 | // Repository is a payload for repository. 47 | // SEE: https://docs.github.com/en/rest/reference/repos#get-a-repository 48 | type Repository struct { 49 | Name github.Repo `json:"name"` 50 | Owner User `json:"owner"` 51 | } 52 | 53 | // User is a payload for users. 54 | type User struct { 55 | Login github.Owner `json:"login"` 56 | } 57 | -------------------------------------------------------------------------------- /tool/gh-action/inputs/event_stub.go: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | ) 6 | 7 | func StubReadEventPayload(payload *PushOrPullRequestEventPayload, err error) ReadEventPayloadFunc { 8 | return func(_ typedpath.RawPath) (*PushOrPullRequestEventPayload, error) { 9 | return payload, err 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tool/gh-action/inputs/event_test.go: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/google/go-cmp/cmp" 6 | "os" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestEventPayload(t *testing.T) { 12 | cases := map[string]struct { 13 | Path string 14 | Expected PushOrPullRequestEventPayload 15 | }{ 16 | "pull request": { 17 | Path: "./testdata/pr-event-payload-example.json", 18 | Expected: PushOrPullRequestEventPayload{ 19 | PullRequest: &PullRequest{ 20 | Number: 2, 21 | }, 22 | Repository: &Repository{ 23 | Name: "Hello-World", 24 | Owner: User{Login: "Codertocat"}, 25 | }, 26 | }, 27 | }, 28 | "push": { 29 | Path: "./testdata/push-event-payload-example.json", 30 | Expected: PushOrPullRequestEventPayload{ 31 | PullRequest: nil, 32 | Repository: &Repository{ 33 | Name: "Hello-World", 34 | Owner: User{Login: "Codertocat"}, 35 | }, 36 | }, 37 | }, 38 | } 39 | 40 | for desc, c := range cases { 41 | t.Run(desc, func(t *testing.T) { 42 | jsonBytes, err := os.ReadFile(c.Path) 43 | if err != nil { 44 | t.Errorf("want nil, got %#v", err) 45 | return 46 | } 47 | 48 | var payload PushOrPullRequestEventPayload 49 | if err := json.Unmarshal(jsonBytes, &payload); err != nil { 50 | t.Errorf("want nil, got %#v", err) 51 | return 52 | } 53 | 54 | if !reflect.DeepEqual(payload, c.Expected) { 55 | t.Error(cmp.Diff(c.Expected, payload)) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tool/gh-action/inputs/testdata/inputs-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "log_level": "DEBUG", 3 | "ignored_file_path": ".meta-check-ignore", 4 | "enable_autofix": "true", 5 | "autofix_globs": ".", 6 | "enable_junit": "true", 7 | "junit_xml_path": "junit.xml", 8 | "enable_pr_comment": "true", 9 | "pr_comment_lang": "ja", 10 | "pr_comment_send_success": "true", 11 | "target_path": "/home/runner/work/unity-meta-check-playground/unity-meta-check-playground", 12 | "target_type": "auto-detect", 13 | "ignore_dangling": "false", 14 | "ignore_case": "false", 15 | "ignore_submodules_and_nested": "false", 16 | "pr_comment_tmpl_file": "", 17 | "pr_comment_pull_number": "123", 18 | "pr_comment_event_path": "/home/runner/work/_temp/_github_workflow/event.json" 19 | } -------------------------------------------------------------------------------- /tool/gh-action/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/tool/gh-action/cmd" 5 | "github.com/DeNA/unity-meta-check/util/cli" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | exitStatus := cmd.Main(os.Args[1:], cli.GetProcessInout(), cli.NewEnv()) 11 | os.Exit(int(exitStatus)) 12 | } 13 | -------------------------------------------------------------------------------- /tool/gh-action/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "github.com/DeNA/unity-meta-check/tool/gh-action/inputs" 8 | "github.com/DeNA/unity-meta-check/util/cli" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type Options struct { 13 | Version bool 14 | Inputs inputs.Inputs 15 | Env inputs.ActionEnv 16 | } 17 | 18 | type Parser func(args []string, procInout cli.ProcessInout, env cli.Env) (*Options, error) 19 | 20 | func NewParser() Parser { 21 | return func(args []string, procInout cli.ProcessInout, env cli.Env) (*Options, error) { 22 | flags := flag.NewFlagSet("gh-action", flag.ContinueOnError) 23 | flags.SetOutput(procInout.Stderr) 24 | flags.Usage = func() { 25 | _, _ = fmt.Fprintln(flags.Output(), "usage: gh-action -inputs-json ") 26 | flags.PrintDefaults() 27 | } 28 | 29 | version := flags.Bool("version", false, "print version") 30 | inputsJSON := flags.String("inputs-json", "", `JSON string of "inputs" context value of GitHub Actions`) 31 | 32 | if err := flags.Parse(args); err != nil { 33 | return nil, err 34 | } 35 | 36 | if *version { 37 | return &Options{Version: *version}, nil 38 | } 39 | 40 | if flags.NArg() > 0 { 41 | return nil, fmt.Errorf("0 arguments required, but come %d arguments: %#v", flags.NArg(), flags.Args()) 42 | } 43 | 44 | var unsafeInputs inputs.Inputs 45 | if err := json.Unmarshal([]byte(*inputsJSON), &unsafeInputs); err != nil { 46 | return nil, errors.Wrapf(err, "malformed JSON of inputs:\n%q", *inputsJSON) 47 | } 48 | 49 | return &Options{ 50 | Inputs: unsafeInputs, 51 | Env: inputs.GetActionEnv(env), 52 | }, nil 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tool/gh-action/options/options_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/DeNA/unity-meta-check/tool/gh-action/inputs" 6 | "github.com/DeNA/unity-meta-check/util/cli" 7 | "github.com/DeNA/unity-meta-check/util/testutil" 8 | "github.com/DeNA/unity-meta-check/util/typedpath" 9 | "github.com/google/go-cmp/cmp" 10 | "io" 11 | "reflect" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func TestParse(t *testing.T) { 17 | in := inputs.Inputs{} 18 | inputsJson, err := json.Marshal(in) 19 | if err != nil { 20 | t.Errorf("want nil, got %#v", err) 21 | return 22 | } 23 | 24 | cases := map[string]struct { 25 | Args []string 26 | Env map[string]string 27 | Expected *Options 28 | }{ 29 | "easiest case": { 30 | Args: []string{"-inputs-json", string(inputsJson)}, 31 | Env: map[string]string{ 32 | "GITHUB_TOKEN": "T0K3N", 33 | "GITHUB_WORKSPACE": string(typedpath.NewRootRawPath("github", "workspace")), 34 | "GITHUB_EVENT_PATH": string(typedpath.NewRootRawPath("github", "workflows", "event.json")), 35 | "GITHUB_API_URL": "https://api.github.com", 36 | }, 37 | Expected: &Options{ 38 | Inputs: in, 39 | Env: inputs.ActionEnv{ 40 | GitHubToken: "T0K3N", 41 | Workspace: typedpath.NewRootRawPath("github", "workspace"), 42 | EventPath: typedpath.NewRootRawPath("github", "workflows", "event.json"), 43 | APIURL: "https://api.github.com", 44 | }, 45 | }, 46 | }, 47 | "-version": { 48 | Args: []string{"-version"}, 49 | Expected: &Options{ 50 | Version: true, 51 | }, 52 | }, 53 | } 54 | 55 | for desc, c := range cases { 56 | t.Run(desc, func(t *testing.T) { 57 | stdin := io.NopCloser(strings.NewReader("")) 58 | stdout := testutil.SpyWriteCloser(nil) 59 | stderr := testutil.SpyWriteCloser(nil) 60 | procInout := cli.ProcessInout{ 61 | Stdin: stdin, 62 | Stdout: stdout, 63 | Stderr: stderr, 64 | } 65 | 66 | parse := NewParser() 67 | opts, err := parse(c.Args, procInout, cli.StubEnv(c.Env)) 68 | if err != nil { 69 | t.Errorf("want nil, got %#v", err) 70 | return 71 | } 72 | 73 | if !reflect.DeepEqual(opts, c.Expected) { 74 | t.Error(cmp.Diff(c.Expected, opts)) 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tool/gh-action/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/report" 6 | "github.com/DeNA/unity-meta-check/resultfilter" 7 | "github.com/DeNA/unity-meta-check/tool/unity-meta-autofix/autofix" 8 | prcomment "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/github" 9 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-junit/junit" 10 | "github.com/DeNA/unity-meta-check/unity/checker" 11 | "github.com/DeNA/unity-meta-check/util/logging" 12 | "io" 13 | ) 14 | 15 | type Runner func(opts *Options) (bool, error) 16 | 17 | func NewRunner( 18 | check checker.Checker, 19 | filter resultfilter.Filter, 20 | writeJunitXML junit.WriteToFileFunc, 21 | send prcomment.SendFunc, 22 | doAutofix autofix.AutoFixer, 23 | w io.Writer, 24 | logger logging.Logger, 25 | ) Runner { 26 | return func(opts *Options) (bool, error) { 27 | logger.Debug(fmt.Sprintf("check: %#v", opts.CheckerOpts)) 28 | resultNotFiltered, err := check(opts.RootDirAbs, opts.CheckerOpts) 29 | if err != nil { 30 | return false, err 31 | } 32 | 33 | logger.Debug(fmt.Sprintf("filter result: %#v", opts.FilterOpts)) 34 | resultFiltered, err := filter(resultNotFiltered, opts.FilterOpts) 35 | if err != nil { 36 | return false, err 37 | } 38 | 39 | if opts.EnableAutofix { 40 | logger.Debug(fmt.Sprintf("autofix: %#v", opts.AutofixOpts)) 41 | skipped, err := doAutofix(resultFiltered, opts.AutofixOpts) 42 | if err != nil { 43 | return false, err 44 | } 45 | resultFiltered = skipped 46 | } else { 47 | logger.Debug(`skip autofix because "enable_autofix" is false`) 48 | } 49 | 50 | logger.Debug("print check result") 51 | if err := report.WriteResult(w, resultFiltered); err != nil { 52 | return false, err 53 | } 54 | 55 | if opts.EnableJUnit { 56 | logger.Debug(fmt.Sprintf("write junit report: %q", opts.JUnitOutPath)) 57 | if err := writeJunitXML(resultFiltered, opts.JUnitOutPath); err != nil { 58 | return false, err 59 | } 60 | } else { 61 | logger.Debug(`skip write junit report because "enable_junit" is false`) 62 | } 63 | 64 | if opts.EnablePRComment { 65 | logger.Debug(fmt.Sprintf("send pull request comment if necessary:\n%s", prcomment.MaskOptions(opts.PRCommentOpts))) 66 | if err := send(resultFiltered, opts.PRCommentOpts); err != nil { 67 | return false, err 68 | } 69 | } else { 70 | logger.Debug(`skip send a pull request comment because "enable_pr_comment" is false`) 71 | } 72 | 73 | return resultFiltered.Empty(), nil 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tool/gh-action/runner/testdata/InvalidProject/Packages/manifest.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tool/gh-action/runner/testdata/InvalidSubDir/dangling.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/gh-action/runner/testdata/InvalidSubDir/dangling.meta -------------------------------------------------------------------------------- /tool/gh-action/runner/testdata/InvalidSubDir/missing: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/gh-action/runner/testdata/InvalidSubDir/missing -------------------------------------------------------------------------------- /tool/gh-action/runner/testdata/ValidProject/Packages/manifest.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tool/gh-action/runner/testdata/ValidSubDir/ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/gh-action/runner/testdata/ValidSubDir/ok -------------------------------------------------------------------------------- /tool/gh-action/runner/testdata/ValidSubDir/ok.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/gh-action/runner/testdata/ValidSubDir/ok.meta -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/autofix_stub.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "errors" 5 | "github.com/DeNA/unity-meta-check/unity/checker" 6 | ) 7 | 8 | func StubAutoFixer(skipped *checker.CheckResult, err error) AutoFixer { 9 | return func(_ *checker.CheckResult, _ *Options) (*checker.CheckResult, error) { 10 | return skipped, err 11 | } 12 | } 13 | 14 | type AutoFixerCallArgs struct { 15 | Result *checker.CheckResult 16 | Options *Options 17 | } 18 | 19 | func SpyAutoFixer(inherited AutoFixer, callArgs *[]AutoFixerCallArgs) AutoFixer { 20 | if inherited == nil { 21 | inherited = StubAutoFixer(nil, errors.New("SPY_AUTO_FIXER")) 22 | } 23 | return func(result *checker.CheckResult, opts *Options) (*checker.CheckResult, error) { 24 | *callArgs = append(*callArgs, AutoFixerCallArgs{ 25 | Result: result, 26 | Options: opts, 27 | }) 28 | return inherited(result, opts) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/create.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/unity/meta" 7 | "github.com/DeNA/unity-meta-check/util/logging" 8 | "github.com/DeNA/unity-meta-check/util/typedpath" 9 | "os" 10 | ) 11 | 12 | type MetaCreator func(metaType MetaType, originalPath typedpath.RawPath) error 13 | 14 | func NewMetaCreator(dryRun bool, guidGen meta.GUIDGen, logger logging.Logger) MetaCreator { 15 | return func(metaType MetaType, missingMeta typedpath.RawPath) error { 16 | guid, err := guidGen() 17 | if err != nil { 18 | return err 19 | } 20 | 21 | var metaGen meta.Gen 22 | switch metaType { 23 | case MetaTypeDefaultImporterFolder: 24 | metaGen = meta.DefaultImporterFolderGen{GUID: guid} 25 | case MetaTypeTextScriptImporter: 26 | metaGen = meta.TextScriptImporterGen{GUID: guid} 27 | case MetaTypeMonoImporter: 28 | metaGen = meta.MonoImporterGen{GUID: guid} 29 | case MetaTypeDefaultImporter: 30 | metaGen = meta.DefaultImporterGen{GUID: guid} 31 | default: 32 | return fmt.Errorf("unsupported meta type: %q", metaType) 33 | } 34 | 35 | _, err = os.Stat(string(missingMeta)) 36 | if err == nil { 37 | return fmt.Errorf("file exists: %s", missingMeta) 38 | } 39 | if !os.IsNotExist(err) { 40 | return err 41 | } 42 | 43 | if dryRun { 44 | return createMetaDryRun(missingMeta, metaGen, logger) 45 | } 46 | return createMeta(missingMeta, metaGen) 47 | } 48 | } 49 | 50 | func createMetaDryRun(missingMeta typedpath.RawPath, metaGen meta.Gen, logger logging.Logger) error { 51 | buf := &bytes.Buffer{} 52 | if _, err := metaGen.WriteTo(buf); err != nil { 53 | return err 54 | } 55 | logger.Info(fmt.Sprintf("would write to %q:\n%s", missingMeta, buf.String())) 56 | return nil 57 | } 58 | 59 | func createMeta(missingMeta typedpath.RawPath, metaGen meta.Gen) error { 60 | file, err := os.OpenFile(string(missingMeta), os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644) 61 | if err != nil { 62 | return err 63 | } 64 | defer func(){ _ = file.Close() }() 65 | 66 | _, err = metaGen.WriteTo(file) 67 | if err != nil { 68 | return err 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/create_stub.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | ) 6 | 7 | func StubMetaCreator(err error) MetaCreator { 8 | return func(MetaType, typedpath.RawPath) error { 9 | return err 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/create_test.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/unity/meta" 5 | "github.com/DeNA/unity-meta-check/util/logging" 6 | "github.com/DeNA/unity-meta-check/util/typedpath" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | ) 11 | 12 | func TestNewMetaCreator(t *testing.T) { 13 | cases := []MetaType{ 14 | MetaTypeDefaultImporterFolder, 15 | MetaTypeTextScriptImporter, 16 | } 17 | 18 | for _, metaType := range cases { 19 | t.Run(string(metaType), func(t *testing.T) { 20 | spyLogger := logging.SpyLogger() 21 | 22 | workDir, err := os.MkdirTemp(os.TempDir(), "") 23 | if err != nil { 24 | panic(err.Error()) 25 | } 26 | missingMeta := typedpath.RawPath(filepath.Join(workDir, "Missing.meta")) 27 | 28 | createMeta := NewMetaCreator(false, meta.StubGUIDGen(meta.AnyGUID(), nil), spyLogger) 29 | if err := createMeta(MetaTypeDefaultImporterFolder, missingMeta); err != nil { 30 | t.Log(spyLogger.Logs.String()) 31 | t.Errorf("want nil, got %#v", err) 32 | return 33 | } 34 | 35 | if _, err := os.Stat(string(missingMeta)); err != nil { 36 | t.Log(spyLogger.Logs.String()) 37 | t.Errorf("want nil, got %#v", err) 38 | return 39 | } 40 | 41 | _ = os.RemoveAll(workDir) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/metatype.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/DeNA/unity-meta-check/unity" 8 | "github.com/DeNA/unity-meta-check/util/ostestable" 9 | "github.com/DeNA/unity-meta-check/util/typedpath" 10 | ) 11 | 12 | type MetaType string 13 | 14 | const ( 15 | MetaTypeDefaultImporterFolder MetaType = "MetaTypeDefaultImporterFolder" 16 | MetaTypeTextScriptImporter MetaType = "MetaTypeTextScriptImporter" 17 | MetaTypeMonoImporter MetaType = "MetaTypeMonoImporter" 18 | MetaTypeDefaultImporter MetaType = "MetaTypeDefaultImporter" 19 | ) 20 | 21 | type MetaTypeDetector func(missingMeta typedpath.RawPath) (MetaType, error) 22 | 23 | func NewMetaTypeDetector(isDir ostestable.IsDir) MetaTypeDetector { 24 | return func(missingMeta typedpath.RawPath) (MetaType, error) { 25 | originalPath := unity.TrimMetaFromRaw(missingMeta) 26 | dir, err := isDir(originalPath) 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | ext := originalPath.Ext() 32 | if dir { 33 | if ext != "" { 34 | return "", fmt.Errorf("should not create .meta because the directory may require special .meta: %s", originalPath) 35 | } 36 | return MetaTypeDefaultImporterFolder, nil 37 | } 38 | 39 | switch strings.ToLower(ext) { 40 | case ".json", ".bytes", ".csv", ".pb", ".txt", ".xml", ".proto", ".md", ".asmdef": 41 | return MetaTypeTextScriptImporter, nil 42 | case ".cs": 43 | return MetaTypeMonoImporter, nil 44 | case ".yaml", ".yml": 45 | return MetaTypeDefaultImporter, nil 46 | default: 47 | switch originalPath.Base() { 48 | case "LICENSE": 49 | return MetaTypeTextScriptImporter, nil 50 | default: 51 | return "", fmt.Errorf("should not create .meta because the extension is not supported now: %s", originalPath) 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/metatype_stub.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | ) 6 | 7 | func StubMetaTypeDetector(result MetaType, err error) MetaTypeDetector { 8 | return func(typedpath.RawPath) (MetaType, error) { 9 | return result, err 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/metatype_test.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/util/ostestable" 6 | "github.com/DeNA/unity-meta-check/util/typedpath" 7 | "testing" 8 | ) 9 | 10 | func TestNewMetaTypeDetectorValid(t *testing.T) { 11 | cases := []struct { 12 | MissingMeta typedpath.RawPath 13 | IsDir bool 14 | Expected MetaType 15 | }{ 16 | { 17 | MissingMeta: typedpath.NewRawPath("Assets", "Dir.meta"), 18 | IsDir: true, 19 | Expected: MetaTypeDefaultImporterFolder, 20 | }, 21 | { 22 | MissingMeta: typedpath.NewRawPath("Assets", "file.json.meta"), 23 | IsDir: false, 24 | Expected: MetaTypeTextScriptImporter, 25 | }, 26 | { 27 | MissingMeta: typedpath.NewRawPath("Assets", "FILE.JSON.meta"), 28 | IsDir: false, 29 | Expected: MetaTypeTextScriptImporter, 30 | }, 31 | { 32 | MissingMeta: typedpath.NewRawPath("Assets", "README.md.meta"), 33 | IsDir: false, 34 | Expected: MetaTypeTextScriptImporter, 35 | }, 36 | { 37 | MissingMeta: typedpath.NewRawPath("Assets", "LICENSE.meta"), 38 | IsDir: false, 39 | Expected: MetaTypeTextScriptImporter, 40 | }, 41 | } 42 | 43 | for _, c := range cases { 44 | t.Run(fmt.Sprintf("%q (isDir=%t) -> %q", c.MissingMeta, c.IsDir, c.Expected), func(t *testing.T) { 45 | detectMetaType := NewMetaTypeDetector(ostestable.StubIsDir(c.IsDir, nil)) 46 | 47 | actual, err := detectMetaType(c.MissingMeta) 48 | if err != nil { 49 | t.Errorf("want nil, got %#v", err) 50 | return 51 | } 52 | 53 | if actual != c.Expected { 54 | t.Errorf("want %q, got %q", c.Expected, actual) 55 | return 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestNewMetaTypeDetectorInvalid(t *testing.T) { 62 | cases := []struct { 63 | MissingMeta typedpath.RawPath 64 | IsDir bool 65 | }{ 66 | { 67 | MissingMeta: typedpath.NewRawPath("Assets", "Some.pkg.meta"), 68 | IsDir: true, 69 | }, 70 | { 71 | MissingMeta: typedpath.NewRawPath("Assets", "file.unknown.meta"), 72 | IsDir: false, 73 | }, 74 | { 75 | MissingMeta: typedpath.NewRawPath("Assets", "no-extension.meta"), 76 | IsDir: false, 77 | }, 78 | } 79 | 80 | for _, c := range cases { 81 | t.Run(fmt.Sprintf("%q (isDir=%t) -> error", c.MissingMeta, c.IsDir), func(t *testing.T) { 82 | detectMetaType := NewMetaTypeDetector(ostestable.StubIsDir(c.IsDir, nil)) 83 | 84 | _, err := detectMetaType(c.MissingMeta) 85 | if err == nil { 86 | t.Errorf("want error, got nil") 87 | return 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/options.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/globs" 5 | "github.com/DeNA/unity-meta-check/util/ostestable" 6 | "github.com/DeNA/unity-meta-check/util/typedpath" 7 | ) 8 | 9 | type Options struct { 10 | RootDirAbs typedpath.RawPath 11 | RootDirRel typedpath.RawPath 12 | AllowedGlobs []globs.Glob 13 | } 14 | 15 | type OptionsBuilder func(rootDirAbs typedpath.RawPath, allowedGlobs []globs.Glob) (*Options, error) 16 | 17 | func NewOptionsBuilder(getwd ostestable.Getwd) OptionsBuilder { 18 | return func(rootDirAbs typedpath.RawPath, allowedGlobs []globs.Glob) (*Options, error) { 19 | cwdAbs, err := getwd() 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | rootDirRel, err := cwdAbs.Rel(rootDirAbs) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return &Options{ 30 | RootDirAbs: rootDirAbs, 31 | RootDirRel: rootDirRel, 32 | AllowedGlobs: allowedGlobs, 33 | }, nil 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/options_stub.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/globs" 5 | "github.com/DeNA/unity-meta-check/util/typedpath" 6 | ) 7 | 8 | func StubOptionsBuilderWithRootDirAbsAndRel(rootDirRel typedpath.RawPath) OptionsBuilder { 9 | return func(rootDirAbs typedpath.RawPath, allowedGlobs []globs.Glob) (*Options, error) { 10 | return &Options{ 11 | RootDirAbs: rootDirAbs, 12 | RootDirRel: rootDirRel, 13 | AllowedGlobs: allowedGlobs, 14 | }, nil 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/remover.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/unity" 6 | "github.com/DeNA/unity-meta-check/util/typedpath" 7 | "os" 8 | ) 9 | 10 | type MetaRemover func(danglingMeta typedpath.RawPath) error 11 | 12 | func NewMetaRemover(dryRun bool) MetaRemover { 13 | return func(danglingMeta typedpath.RawPath) error { 14 | stat, err := os.Stat(string(danglingMeta)) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | if !unity.IsMeta(danglingMeta.ToSlash()) || stat.IsDir() { 20 | return fmt.Errorf("must be a meta file: %s", danglingMeta) 21 | } 22 | 23 | if dryRun { 24 | return nil 25 | } 26 | return os.Remove(string(danglingMeta)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/remover_stub.go: -------------------------------------------------------------------------------- 1 | package autofix 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | ) 6 | 7 | func StubMetaRemover(err error) MetaRemover { 8 | return func(typedpath.RawPath) error { 9 | return err 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/testdata/InvalidProject/Assets/Dangling.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/unity-meta-autofix/autofix/testdata/InvalidProject/Assets/Dangling.meta -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/testdata/InvalidProject/Assets/Missing: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/unity-meta-autofix/autofix/testdata/InvalidProject/Assets/Missing -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/testdata/ValidProject/Assets/OK: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/unity-meta-autofix/autofix/testdata/ValidProject/Assets/OK -------------------------------------------------------------------------------- /tool/unity-meta-autofix/autofix/testdata/ValidProject/Assets/OK.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/unity-meta-autofix/autofix/testdata/ValidProject/Assets/OK.meta -------------------------------------------------------------------------------- /tool/unity-meta-autofix/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | options2 "github.com/DeNA/unity-meta-check/options" 7 | "github.com/DeNA/unity-meta-check/report" 8 | "github.com/DeNA/unity-meta-check/tool/unity-meta-autofix/autofix" 9 | "github.com/DeNA/unity-meta-check/tool/unity-meta-autofix/options" 10 | "github.com/DeNA/unity-meta-check/unity/meta" 11 | "github.com/DeNA/unity-meta-check/util/cli" 12 | "github.com/DeNA/unity-meta-check/util/logging" 13 | "github.com/DeNA/unity-meta-check/util/ostestable" 14 | "github.com/DeNA/unity-meta-check/version" 15 | ) 16 | 17 | func NewMain() cli.Command { 18 | return func(args []string, procInout cli.ProcessInout, env cli.Env) cli.ExitStatus { 19 | parseOpts := options.NewParser(options2.NewRootDirValidator(ostestable.NewIsDir())) 20 | opts, err := parseOpts(args, procInout) 21 | if err != nil { 22 | if err != flag.ErrHelp { 23 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 24 | } 25 | return cli.ExitAbnormal 26 | } 27 | 28 | if opts.Version { 29 | _, _ = fmt.Fprintln(procInout.Stdout, version.Version) 30 | return cli.ExitNormal 31 | } 32 | 33 | parse := report.NewParser() 34 | result := parse(procInout.Stdin) 35 | 36 | logger := logging.NewLogger(opts.LogLevel, procInout.Stderr) 37 | 38 | buildOpts := autofix.NewOptionsBuilder(ostestable.NewGetwd()) 39 | autofixOpts, err := buildOpts(opts.RootDirAbs, opts.AllowedGlobs) 40 | if err != nil { 41 | logger.Error(err.Error()) 42 | return cli.ExitNormal 43 | } 44 | 45 | autofixFunc := autofix.NewAutoFixer( 46 | opts.DryRun, 47 | ostestable.NewGetwd(), 48 | autofix.NewMetaTypeDetector(ostestable.NewIsDir()), 49 | autofix.NewMetaCreator(opts.DryRun, meta.RandomGUIDGenerator(), logger), 50 | autofix.NewMetaRemover(opts.DryRun), 51 | logger, 52 | ) 53 | 54 | skipped, err := autofixFunc(result, autofixOpts) 55 | if err != nil { 56 | logger.Error(err.Error()) 57 | return cli.ExitAbnormal 58 | } 59 | 60 | if err := report.WriteResult(procInout.Stdout, skipped); err != nil { 61 | logger.Error(err.Error()) 62 | return cli.ExitAbnormal 63 | } 64 | 65 | return cli.ExitNormal 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/cmd/cmd_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/cli" 5 | "github.com/DeNA/unity-meta-check/util/testutil" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestValidDryRun(t *testing.T) { 12 | main := NewMain() 13 | stdout := testutil.SpyWriteCloser(nil) 14 | stderr := testutil.SpyWriteCloser(nil) 15 | procInout := cli.ProcessInout{ 16 | Stdin: strings.NewReader(""), 17 | Stdout: stdout, 18 | Stderr: stderr, 19 | } 20 | 21 | rootDir := filepath.Join("testdata", "ValidProject") 22 | actual := main([]string{"-debug", "-dry-run", "-root-dir", rootDir, "Assets/*"}, procInout, cli.AnyEnv()) 23 | 24 | expected := cli.ExitNormal 25 | if actual != expected { 26 | t.Logf("stdout:\n%s", stdout.Captured.String()) 27 | t.Logf("stderr:\n%s", stderr.Captured.String()) 28 | t.Errorf("want %#v, got %#v", expected, actual) 29 | return 30 | } 31 | } 32 | 33 | func TestInvalidDryRun(t *testing.T) { 34 | main := NewMain() 35 | stdout := testutil.SpyWriteCloser(nil) 36 | stderr := testutil.SpyWriteCloser(nil) 37 | procInout := cli.ProcessInout{ 38 | Stdin: strings.NewReader(`missing Missing.meta 39 | dangling Dangling.meta`), 40 | Stdout: stdout, 41 | Stderr: stderr, 42 | } 43 | 44 | rootDir := filepath.Join("testdata", "InvalidProject") 45 | actual := main([]string{"-debug", "-dry-run", "-root-dir", rootDir, "Assets/*"}, procInout, cli.AnyEnv()) 46 | 47 | expected := cli.ExitNormal 48 | if actual != expected { 49 | t.Logf("stdout:\n%s", stdout.Captured.String()) 50 | t.Logf("stderr:\n%s", stderr.Captured.String()) 51 | t.Errorf("want %#v, got %#v", expected, actual) 52 | return 53 | } 54 | } 55 | 56 | func TestVersion(t *testing.T) { 57 | main := NewMain() 58 | stdout := testutil.SpyWriteCloser(nil) 59 | stderr := testutil.SpyWriteCloser(nil) 60 | procInout := cli.ProcessInout{ 61 | Stdin: strings.NewReader(""), 62 | Stdout: stdout, 63 | Stderr: stderr, 64 | } 65 | 66 | actual := main([]string{"-version"}, procInout, cli.AnyEnv()) 67 | 68 | expected := cli.ExitNormal 69 | if actual != expected { 70 | t.Logf("stdout:\n%s", stdout.Captured.String()) 71 | t.Logf("stderr:\n%s", stderr.Captured.String()) 72 | t.Errorf("want %#v, got %#v", expected, actual) 73 | return 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/cmd/testdata/InvalidProject/Assets/Dangling.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/unity-meta-autofix/cmd/testdata/InvalidProject/Assets/Dangling.meta -------------------------------------------------------------------------------- /tool/unity-meta-autofix/cmd/testdata/InvalidProject/Assets/Missing: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/unity-meta-autofix/cmd/testdata/InvalidProject/Assets/Missing -------------------------------------------------------------------------------- /tool/unity-meta-autofix/cmd/testdata/ValidProject/Assets/OK: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/unity-meta-autofix/cmd/testdata/ValidProject/Assets/OK -------------------------------------------------------------------------------- /tool/unity-meta-autofix/cmd/testdata/ValidProject/Assets/OK.meta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/tool/unity-meta-autofix/cmd/testdata/ValidProject/Assets/OK.meta -------------------------------------------------------------------------------- /tool/unity-meta-autofix/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/tool/unity-meta-autofix/cmd" 5 | "github.com/DeNA/unity-meta-check/util/cli" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | main := cmd.NewMain() 11 | exitStatus := main(os.Args[1:], cli.GetProcessInout(), cli.NewEnv()) 12 | os.Exit(int(exitStatus)) 13 | } 14 | -------------------------------------------------------------------------------- /tool/unity-meta-autofix/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "github.com/DeNA/unity-meta-check/options" 8 | "github.com/DeNA/unity-meta-check/util/cli" 9 | "github.com/DeNA/unity-meta-check/util/globs" 10 | "github.com/DeNA/unity-meta-check/util/logging" 11 | "github.com/DeNA/unity-meta-check/util/typedpath" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | type Options struct { 17 | Version bool 18 | LogLevel logging.Severity 19 | DryRun bool 20 | AllowedGlobs []globs.Glob 21 | RootDirAbs typedpath.RawPath 22 | } 23 | 24 | type Parser func(args []string, procInout cli.ProcessInout) (*Options, error) 25 | 26 | func NewParser(validateRootDirAbs options.RootDirAbsValidator) Parser { 27 | return func(args []string, procInout cli.ProcessInout) (*Options, error) { 28 | opts := Options{} 29 | 30 | flags := flag.NewFlagSet("unity-meta-autofix", flag.ContinueOnError) 31 | flags.SetOutput(procInout.Stderr) 32 | flags.Usage = func() { 33 | _, _ = fmt.Fprint(procInout.Stderr, `usage: unity-meta-autofix [] [...] 34 | 35 | Fix missing or dangling .meta. Currently autofix is only limited support. 36 | 37 | ARGUMENTS 38 | 39 | glob pattern to path where autofix allowed on. 40 | 41 | OPTIONS 42 | `) 43 | flags.PrintDefaults() 44 | 45 | _, _ = fmt.Fprint(procInout.Stderr, ` 46 | EXAMPLE USAGES 47 | $ unity-meta-check | unity-meta-autofix -dry-run path/to/autofix 48 | $ unity-meta-check | unity-meta-autofix | 49 | `) 50 | } 51 | 52 | var silent, debug bool 53 | var rootDir string 54 | flags.BoolVar(&opts.Version, "version", false, "print version") 55 | flags.BoolVar(&debug, "debug", false, "set log level to DEBUG (default INFO)") 56 | flags.BoolVar(&silent, "silent", false, "set log level to WARN (default INFO)") 57 | flags.BoolVar(&opts.DryRun, "dry-run", false, "dry run") 58 | flags.StringVar(&rootDir, "root-dir", ".", "directory path to where unity-meta-check checked at") 59 | 60 | if err := flags.Parse(args); err != nil { 61 | return nil, err 62 | } 63 | 64 | if opts.Version { 65 | return &opts, nil 66 | } 67 | 68 | targetPaths := flags.Args() 69 | if len(targetPaths) == 0 { 70 | return nil, errors.New("must specify at least one target path") 71 | } 72 | opts.AllowedGlobs = make([]globs.Glob, len(targetPaths)) 73 | for i, targetPath := range targetPaths { 74 | opts.AllowedGlobs[i] = globs.Glob(strings.Trim(filepath.ToSlash(targetPath), "/")) 75 | } 76 | 77 | rootDirAbs, err := validateRootDirAbs(typedpath.NewRawPathUnsafe(rootDir)) 78 | if err != nil { 79 | return nil, err 80 | } 81 | opts.RootDirAbs = rootDirAbs 82 | 83 | opts.LogLevel = cli.GetLogLevel(debug, silent) 84 | 85 | return &opts, nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/report" 7 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/github" 8 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/options" 9 | "github.com/DeNA/unity-meta-check/util/cli" 10 | "github.com/DeNA/unity-meta-check/util/logging" 11 | "github.com/DeNA/unity-meta-check/version" 12 | "io" 13 | ) 14 | 15 | func NewMain() cli.Command { 16 | return func(args []string, procInout cli.ProcessInout, env cli.Env) cli.ExitStatus { 17 | opts, err := options.BuildOptions(args, procInout, env) 18 | if err != nil { 19 | if err != flag.ErrHelp { 20 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 21 | } 22 | return cli.ExitAbnormal 23 | } 24 | 25 | if opts.Version { 26 | _, _ = fmt.Fprintln(procInout.Stdout, version.Version) 27 | return cli.ExitNormal 28 | } 29 | 30 | logger := logging.NewLogger(opts.LogLevel, procInout.Stderr) 31 | 32 | send := github.NewSendFunc( 33 | github.NewPullRequestCommentSender(github.NewHttp(), logger), 34 | ) 35 | 36 | parse := report.NewParser() 37 | result := parse(io.TeeReader(procInout.Stdin, procInout.Stdout)) 38 | 39 | if err := send(result, &github.Options{ 40 | Tmpl: opts.Tmpl, 41 | SendIfSuccess: opts.SendIfSuccess, 42 | Token: opts.Token, 43 | APIEndpoint: opts.APIEndpoint, 44 | Owner: opts.Owner, 45 | Repo: opts.Repo, 46 | PullNumber: opts.PullNumber, 47 | }); err != nil { 48 | logger.Error(err.Error()) 49 | return cli.ExitAbnormal 50 | } 51 | 52 | if !result.Empty() { 53 | return cli.ExitAbnormal 54 | } 55 | return cli.ExitNormal 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/github/api.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/DeNA/unity-meta-check/util/logging" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "path" 12 | "strings" 13 | ) 14 | 15 | type PullRequestCommentSender func(githubEndpoint APIEndpoint, token Token, owner Owner, repo Repo, pullNumber PullNumber, comment string) error 16 | 17 | type PullRequestComment struct { 18 | Body string `json:"body"` 19 | } 20 | 21 | func NewPullRequestCommentSender(send HttpFunc, logger logging.Logger) PullRequestCommentSender { 22 | return func(githubEndpoint APIEndpoint, token Token, owner Owner, repo Repo, pullNumber PullNumber, comment string) error { 23 | apiUrl := &url.URL{ 24 | Scheme: githubEndpoint.Scheme, 25 | Host: githubEndpoint.Host, 26 | Path: path.Join(githubEndpoint.Path, "repos", string(owner), string(repo), "issues", fmt.Sprintf("%d", pullNumber), "comments"), 27 | } 28 | logger.Debug(fmt.Sprintf("url: %s", apiUrl.String())) 29 | 30 | body, err := json.Marshal(PullRequestComment{Body: comment}) 31 | if err != nil { 32 | return err 33 | } 34 | logger.Debug(fmt.Sprintf("comment body: %s", body)) 35 | 36 | req, err := http.NewRequest(http.MethodPost, apiUrl.String(), bytes.NewReader(body)) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | logger.Debug(fmt.Sprintf("token: %s", strings.Repeat("*", len(token)))) 42 | req.Header.Add("Accept", "application/vnd.github.v3+json") 43 | req.Header.Add("Authorization", fmt.Sprintf("token %s", token)) 44 | req.Header.Add("Content-Type", "application/json") 45 | 46 | res, err := send(req) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | resBody, err := io.ReadAll(res.Body) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | if res.StatusCode != http.StatusCreated { 57 | return fmt.Errorf("unexpected status code: %d\n%s", res.StatusCode, string(resBody)) 58 | } 59 | 60 | logger.Debug(fmt.Sprintf("response: %s", resBody)) 61 | return nil 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/github/api_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/logging" 5 | "github.com/DeNA/unity-meta-check/util/testutil" 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func TestNewPullRequestCommentSender(t *testing.T) { 11 | spyLogger := logging.SpyLogger() 12 | 13 | testEnv, err := testutil.GetTestEnv() 14 | if err != nil { 15 | t.Log(err.Error()) 16 | t.Skip("no environment variables for tests") 17 | return 18 | } 19 | token := testEnv.Token 20 | 21 | endpoint, err := url.Parse("https://api.github.com") 22 | if err != nil { 23 | t.Errorf("want nil, got %#v", err) 24 | return 25 | } 26 | 27 | send := NewPullRequestCommentSender(NewHttp(), spyLogger) 28 | if err := send(endpoint, Token(token), "dena", "unity-meta-check-playground", 1, "TEST FROM unity-meta-check-github-pr-comment"); err != nil { 29 | t.Log(spyLogger.Logs.String()) 30 | t.Errorf("want nil, got %#v", err) 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/github/http.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import "net/http" 4 | 5 | type HttpFunc func(request *http.Request) (*http.Response, error) 6 | 7 | func NewHttp() HttpFunc { 8 | return func(request *http.Request) (*http.Response, error) { 9 | return http.DefaultClient.Do(request) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/github/send.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/l10n" 7 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/markdown" 8 | "github.com/DeNA/unity-meta-check/unity/checker" 9 | "strings" 10 | ) 11 | 12 | type Options struct { 13 | Tmpl *l10n.Template 14 | SendIfSuccess bool 15 | Token Token 16 | APIEndpoint APIEndpoint 17 | Owner Owner 18 | Repo Repo 19 | PullNumber PullNumber 20 | } 21 | 22 | type SendFunc func(result *checker.CheckResult, opts *Options) error 23 | 24 | func NewSendFunc(postComment PullRequestCommentSender) SendFunc { 25 | return func(result *checker.CheckResult, opts *Options) error { 26 | buf := &bytes.Buffer{} 27 | if err := markdown.WriteMarkdown(result, opts.Tmpl, buf); err != nil { 28 | return err 29 | } 30 | 31 | if !result.Empty() || opts.SendIfSuccess { 32 | if err := postComment(opts.APIEndpoint, opts.Token, opts.Owner, opts.Repo, opts.PullNumber, buf.String()); err != nil { 33 | return err 34 | } 35 | } 36 | 37 | return nil 38 | } 39 | } 40 | 41 | func MaskOptions(opts *Options) string { 42 | return fmt.Sprintf(` 43 | Tmpl=%#v 44 | SendIfSuccess=%t 45 | Token=%q (len=%d) 46 | APIEndpoint=%#v 47 | Owner=%q 48 | Repo=%q 49 | PullNumber=%d`[1:], 50 | opts.Tmpl, 51 | opts.SendIfSuccess, 52 | strings.Repeat("*", len(opts.Token)), 53 | len(opts.Token), 54 | opts.APIEndpoint, 55 | opts.Owner, 56 | opts.Repo, 57 | opts.PullNumber, 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/github/send_stub.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/unity/checker" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func StubSendFunc(err error) SendFunc { 9 | return func(_ *checker.CheckResult, _ *Options) error { 10 | return err 11 | } 12 | } 13 | 14 | type SendFuncCallArgs struct { 15 | Result *checker.CheckResult 16 | Options *Options 17 | } 18 | 19 | func SpySendFunc(inherited SendFunc, callArgs *[]SendFuncCallArgs) SendFunc { 20 | if inherited == nil { 21 | inherited = StubSendFunc(errors.New("SPY_SEND_FUNC")) 22 | } 23 | return func(result *checker.CheckResult, opts *Options) error { 24 | *callArgs = append(*callArgs, SendFuncCallArgs{ 25 | Result: result, 26 | Options: opts, 27 | }) 28 | return inherited(result, opts) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/github/types.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | type APIEndpoint *url.URL 10 | type Token string 11 | type Owner string 12 | type Repo string 13 | type PullNumber int 14 | 15 | func ValidateOwner(unsafeOwner string) (Owner, error) { 16 | if unsafeOwner == "" { 17 | return "", errors.New("owner must be not empty") 18 | } 19 | return Owner(unsafeOwner), nil 20 | } 21 | 22 | func ValidateRepo(unsafeRepo string) (Repo, error) { 23 | if unsafeRepo == "" { 24 | return "", errors.New("repo must be not empty") 25 | } 26 | return Repo(unsafeRepo), nil 27 | } 28 | 29 | func ValidatePullNumber(unsafePullNumber int) (PullNumber, error) { 30 | if unsafePullNumber <= 0 { 31 | return 0, fmt.Errorf("pull number must be a positive integer: %d", unsafePullNumber) 32 | } 33 | return PullNumber(unsafePullNumber), nil 34 | } 35 | 36 | func ValidateToken(unsafeToken string) (Token, error) { 37 | if unsafeToken == "" { 38 | return "", fmt.Errorf("GitHub Personal Token must not be empty") 39 | } 40 | return Token(unsafeToken), nil 41 | } 42 | 43 | func ValidateAPIEndpoint(unsafeAPIEndpoint string) (APIEndpoint, error) { 44 | apiEndpoint, err := url.Parse(unsafeAPIEndpoint) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return apiEndpoint, nil 49 | } -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/l10n/lang.go: -------------------------------------------------------------------------------- 1 | package l10n 2 | 3 | type Lang string 4 | 5 | const ( 6 | LangEn Lang = "en" 7 | LangJa Lang = "ja" 8 | ) 9 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/l10n/template.go: -------------------------------------------------------------------------------- 1 | package l10n 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/DeNA/unity-meta-check/util/typedpath" 8 | "io" 9 | "os" 10 | ) 11 | 12 | type Template struct { 13 | SuccessMessage string `json:"success"` 14 | FailureMessage string `json:"failure"` 15 | StatusHeader string `json:"header_status"` 16 | FilePathHeader string `json:"header_file_path"` 17 | StatusMissing string `json:"status_missing"` 18 | StatusDangling string `json:"status_dangling"` 19 | } 20 | 21 | var En = Template{ 22 | SuccessMessage: "No missing/dangling .meta found. Perfect!", 23 | FailureMessage: `Some missing or dangling .meta found. Fix commits are needed.`, 24 | StatusHeader: "Status", 25 | FilePathHeader: "File", 26 | StatusMissing: "Not committed", 27 | StatusDangling: "Not removed", 28 | } 29 | 30 | var Ja = Template{ 31 | SuccessMessage: "commit忘れ・消し忘れの .meta はありませんでした。素晴らしい!", 32 | FailureMessage: "commit忘れ・消し忘れの .meta が見つかりました。修正コミットが必要です。", 33 | StatusHeader: "状態", 34 | FilePathHeader: "ファイル", 35 | StatusMissing: "commit されていない", 36 | StatusDangling: "消されていない", 37 | } 38 | 39 | func GetTemplate(lang Lang) (*Template, error) { 40 | switch lang { 41 | case LangEn: 42 | return &En, nil 43 | case LangJa: 44 | return &Ja, nil 45 | default: 46 | return nil, fmt.Errorf("unsupported lang: %s", lang) 47 | } 48 | } 49 | 50 | type TemplateFileReader func(path typedpath.RawPath) (*Template, error) 51 | 52 | func ReadTemplateFile(path typedpath.RawPath) (*Template, error) { 53 | file, err := os.Open(string(path)) 54 | if err != nil { 55 | return nil, err 56 | } 57 | defer func(){ _ = file.Close() }() 58 | 59 | return ReadTemplate(file) 60 | } 61 | 62 | func ReadTemplate(reader io.Reader) (*Template, error) { 63 | bytes, err := io.ReadAll(reader) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | var tmpl Template 69 | if err := json.Unmarshal(bytes, &tmpl); err != nil { 70 | return nil, err 71 | } 72 | 73 | return &tmpl, nil 74 | } 75 | 76 | func WriteTemplateExample(writer io.Writer) { 77 | bytes, err := json.MarshalIndent(En, "", " ") 78 | if err != nil { 79 | panic(err.Error()) 80 | } 81 | _, _ = writer.Write(bytes) 82 | } 83 | 84 | func ValidateTemplate(tmpl *Template) error { 85 | if tmpl.StatusHeader == "" { 86 | return errors.New(`empty "header_status"`) 87 | } 88 | if tmpl.FilePathHeader == "" { 89 | return errors.New(`empty "header_file_path"`) 90 | } 91 | if tmpl.StatusMissing == "" { 92 | return errors.New(`empty "status_missing"`) 93 | } 94 | if tmpl.StatusDangling == "" { 95 | return errors.New(`empty "status_dangling"`) 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/l10n/template_stub.go: -------------------------------------------------------------------------------- 1 | package l10n 2 | 3 | import "github.com/DeNA/unity-meta-check/util/typedpath" 4 | 5 | func StubTemplateFileReader(tmpl *Template, err error) TemplateFileReader { 6 | return func(_ typedpath.RawPath) (*Template, error) { 7 | return tmpl, err 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/l10n/template_test.go: -------------------------------------------------------------------------------- 1 | package l10n 2 | 3 | import ( 4 | "bytes" 5 | "github.com/google/go-cmp/cmp" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestReadL10n(t *testing.T) { 12 | json := `{ 13 | "header_status": "STATUS_HEADER", 14 | "header_file_path": "FILE_PATH_HEADER", 15 | "status_missing": "STATUS_MISSING", 16 | "status_dangling": "STATUS_DANGLING" 17 | }` 18 | actual, err := ReadTemplate(strings.NewReader(json)) 19 | 20 | if err != nil { 21 | t.Errorf("want nil, got %#v", err) 22 | return 23 | } 24 | 25 | expected := &Template{ 26 | StatusHeader: "STATUS_HEADER", 27 | FilePathHeader: "FILE_PATH_HEADER", 28 | StatusMissing: "STATUS_MISSING", 29 | StatusDangling: "STATUS_DANGLING", 30 | } 31 | if !reflect.DeepEqual(actual, expected) { 32 | t.Error(cmp.Diff(expected, actual)) 33 | return 34 | } 35 | } 36 | 37 | 38 | func TestWriteTemplateExample(t *testing.T) { 39 | buf := &bytes.Buffer{} 40 | 41 | WriteTemplateExample(buf) 42 | 43 | if buf.Len() == 0 { 44 | t.Error("want greater than 0, but 0") 45 | return 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/cmd" 5 | "github.com/DeNA/unity-meta-check/util/cli" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | main := cmd.NewMain() 11 | exitStatus := main(os.Args[1:], cli.GetProcessInout(), cli.NewEnv()) 12 | os.Exit(int(exitStatus)) 13 | } 14 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/markdown/markdown.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/l10n" 7 | "github.com/DeNA/unity-meta-check/unity/checker" 8 | "io" 9 | "strings" 10 | ) 11 | 12 | func WriteMarkdown(result *checker.CheckResult, tmpl *l10n.Template, writer io.Writer) error { 13 | if result.Empty() { 14 | _, _ = fmt.Fprintln(writer, tmpl.SuccessMessage) 15 | return nil 16 | } 17 | 18 | _, _ = fmt.Fprintln(writer, tmpl.FailureMessage) 19 | _, _ = io.WriteString(writer, "\n") 20 | 21 | if err := WriteTableRow(writer, tmpl.StatusHeader, tmpl.FilePathHeader); err != nil { 22 | return err 23 | } 24 | 25 | if err := WriteTableSep(writer, 2); err != nil { 26 | return err 27 | } 28 | 29 | for _, missingMeta := range result.MissingMeta { 30 | if err := WriteTableRow(writer, tmpl.StatusMissing, FormatAsInlineCode(string(missingMeta))); err != nil { 31 | return err 32 | } 33 | } 34 | 35 | for _, danglingMeta := range result.DanglingMeta { 36 | if err := WriteTableRow(writer, tmpl.StatusDangling, FormatAsInlineCode(string(danglingMeta))); err != nil { 37 | return err 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func WriteTableSep(writer io.Writer, num int) error { 45 | if num == 0 { 46 | return errors.New("must include at least one column") 47 | } 48 | 49 | _, _ = io.WriteString(writer, "|") 50 | _, _ = io.WriteString(writer, strings.Repeat(":--|", num)) 51 | _, _ = io.WriteString(writer, "\n") 52 | return nil 53 | } 54 | 55 | func WriteTableRow(writer io.Writer, cols ...string) error { 56 | if len(cols) == 0 { 57 | return errors.New("must include at least one column") 58 | } 59 | 60 | _, _ = io.WriteString(writer, "| ") 61 | _, _ = io.WriteString(writer, cols[0]) 62 | for _, col := range cols[1:] { 63 | _, _ = io.WriteString(writer, " | ") 64 | _, _ = io.WriteString(writer, col) 65 | } 66 | _, _ = io.WriteString(writer, " |") 67 | _, _ = io.WriteString(writer, "\n") 68 | 69 | return nil 70 | } 71 | 72 | func FormatAsInlineCode(s string) string { 73 | return fmt.Sprintf("`%s`", s) 74 | } 75 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/markdown/markdown_test.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-github-pr-comment/l10n" 7 | "github.com/DeNA/unity-meta-check/unity/checker" 8 | "github.com/DeNA/unity-meta-check/util/typedpath" 9 | "github.com/google/go-cmp/cmp" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestWriteMarkdown(t *testing.T) { 15 | cases := []struct{ 16 | Result *checker.CheckResult 17 | Expected string 18 | }{ 19 | { 20 | Result: &checker.CheckResult{ 21 | MissingMeta: []typedpath.SlashPath{}, 22 | DanglingMeta: []typedpath.SlashPath{}, 23 | }, 24 | Expected: "SUCCESS_MESSAGE\n", 25 | }, 26 | { 27 | Result: &checker.CheckResult{ 28 | MissingMeta: []typedpath.SlashPath{ 29 | "path/to/missing.meta", 30 | }, 31 | DanglingMeta: []typedpath.SlashPath{}, 32 | }, 33 | Expected: strings.Join([]string{ 34 | "FAILURE_MESSAGE", 35 | "", 36 | "| HEADER_STATUS | HEADER_FILE_PATH |", 37 | "|:--|:--|", 38 | "| STATUS_MISSING | `path/to/missing.meta` |", 39 | "", 40 | }, "\n"), 41 | }, 42 | { 43 | Result: &checker.CheckResult{ 44 | MissingMeta: []typedpath.SlashPath{}, 45 | DanglingMeta: []typedpath.SlashPath{ 46 | "path/to/dangling.meta", 47 | }, 48 | }, 49 | Expected: strings.Join([]string{ 50 | "FAILURE_MESSAGE", 51 | "", 52 | "| HEADER_STATUS | HEADER_FILE_PATH |", 53 | "|:--|:--|", 54 | "| STATUS_DANGLING | `path/to/dangling.meta` |", 55 | "", 56 | }, "\n"), 57 | }, 58 | } 59 | 60 | for _, c := range cases { 61 | t.Run(fmt.Sprintf("%v", c), func(t *testing.T) { 62 | buf := &bytes.Buffer{} 63 | 64 | err := WriteMarkdown(c.Result, &l10n.Template{ 65 | SuccessMessage: "SUCCESS_MESSAGE", 66 | FailureMessage: "FAILURE_MESSAGE", 67 | StatusHeader: "HEADER_STATUS", 68 | FilePathHeader: "HEADER_FILE_PATH", 69 | StatusMissing: "STATUS_MISSING", 70 | StatusDangling: "STATUS_DANGLING", 71 | }, buf) 72 | if err != nil { 73 | t.Errorf("want nil, got %#v", err) 74 | return 75 | } 76 | 77 | actual := buf.String() 78 | if actual != c.Expected { 79 | t.Error(cmp.Diff(c.Expected, actual)) 80 | return 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tool/unity-meta-check-github-pr-comment/options/testdata/example-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": "SUCCESS", 3 | "failure": "FAILURE", 4 | "header_status": "HEADER_STATUS", 5 | "header_file_path": "HEADER_FILE_PATH", 6 | "status_missing": "STATUS_MISSING", 7 | "status_dangling": "STATUS_DANGLING" 8 | } -------------------------------------------------------------------------------- /tool/unity-meta-check-junit/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/report" 7 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-junit/junit" 8 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-junit/options" 9 | "github.com/DeNA/unity-meta-check/util/cli" 10 | "github.com/DeNA/unity-meta-check/version" 11 | "io" 12 | ) 13 | 14 | func NewMain() cli.Command { 15 | return func(args []string, procInout cli.ProcessInout, env cli.Env) cli.ExitStatus { 16 | opts, err := options.BuildOptions(args, procInout) 17 | if err != nil { 18 | if err != flag.ErrHelp { 19 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 20 | } 21 | return cli.ExitAbnormal 22 | } 23 | 24 | if opts.Version { 25 | _, _ = fmt.Fprintln(procInout.Stdout, version.Version) 26 | return cli.ExitNormal 27 | } 28 | 29 | parse := report.NewParser() 30 | result := parse(io.TeeReader(procInout.Stdin, procInout.Stdout)) 31 | 32 | if err := junit.WriteToFile(result, opts.OutPath); err != nil { 33 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 34 | return cli.ExitAbnormal 35 | } 36 | 37 | if !result.Empty() { 38 | return cli.ExitAbnormal 39 | } 40 | return cli.ExitNormal 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tool/unity-meta-check-junit/cmd/cmd_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/cli" 5 | "github.com/DeNA/unity-meta-check/util/testutil" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestValid(t *testing.T) { 14 | stdout := testutil.SpyWriteCloser(nil) 15 | stderr := testutil.SpyWriteCloser(nil) 16 | 17 | procInout := cli.ProcessInout{ 18 | Stdin: strings.NewReader(""), 19 | Stdout: stdout, 20 | Stderr: stderr, 21 | } 22 | 23 | tmpDir, err := os.MkdirTemp(os.TempDir(), "") 24 | if err != nil { 25 | t.Errorf("want nil, got %#v", err) 26 | return 27 | } 28 | 29 | main := NewMain() 30 | actual := main([]string{filepath.Join(tmpDir, "valid.xml")}, procInout, cli.AnyEnv()) 31 | 32 | expected := cli.ExitNormal 33 | if actual != expected { 34 | t.Logf("stdout:\n%s", stdout.Captured.String()) 35 | t.Logf("stderr:\n%s", stderr.Captured.String()) 36 | t.Errorf("want %#v, got %#v", expected, actual) 37 | return 38 | } 39 | } 40 | 41 | func TestInvalid(t *testing.T) { 42 | stdout := testutil.SpyWriteCloser(nil) 43 | stderr := testutil.SpyWriteCloser(nil) 44 | 45 | procInout := cli.ProcessInout{ 46 | Stdin: strings.NewReader(`missing path/to/missing.meta 47 | dangling path/to/dangling.meta`), 48 | Stdout: stdout, 49 | Stderr: stderr, 50 | } 51 | 52 | tmpDir, err := ioutil.TempDir(os.TempDir(), "") 53 | if err != nil { 54 | t.Errorf("want nil, got %#v", err) 55 | return 56 | } 57 | 58 | main := NewMain() 59 | actual := main([]string{filepath.Join(tmpDir, "invalid.xml")}, procInout, cli.AnyEnv()) 60 | 61 | expected := cli.ExitAbnormal 62 | if actual != expected { 63 | t.Logf("stdout:\n%s", stdout.Captured.String()) 64 | t.Logf("stderr:\n%s", stderr.Captured.String()) 65 | t.Errorf("want %#v, got %#v", expected, actual) 66 | return 67 | } 68 | } 69 | 70 | func TestVersion(t *testing.T) { 71 | main := NewMain() 72 | stdout := testutil.SpyWriteCloser(nil) 73 | stderr := testutil.SpyWriteCloser(nil) 74 | procInout := cli.ProcessInout{ 75 | Stdin: strings.NewReader(""), 76 | Stdout: stdout, 77 | Stderr: stderr, 78 | } 79 | 80 | actual := main([]string{"-version"}, procInout, cli.AnyEnv()) 81 | 82 | expected := cli.ExitNormal 83 | if actual != expected { 84 | t.Logf("stdout:\n%s", stdout.Captured.String()) 85 | t.Logf("stderr:\n%s", stderr.Captured.String()) 86 | t.Errorf("want %#v, got %#v", expected, actual) 87 | return 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tool/unity-meta-check-junit/junit/junit_stub.go: -------------------------------------------------------------------------------- 1 | package junit 2 | 3 | import ( 4 | "errors" 5 | "github.com/DeNA/unity-meta-check/unity/checker" 6 | "github.com/DeNA/unity-meta-check/util/typedpath" 7 | ) 8 | 9 | func StubWriteToFileFunc(err error) WriteToFileFunc { 10 | return func(_ *checker.CheckResult, _ typedpath.RawPath) error { 11 | return err 12 | } 13 | } 14 | 15 | type WriteToFileCallArgs struct { 16 | CheckResult *checker.CheckResult 17 | OutPath typedpath.RawPath 18 | } 19 | 20 | func SpyWriteToFileFunc(inherited WriteToFileFunc, callArgs *[]WriteToFileCallArgs) WriteToFileFunc { 21 | if inherited == nil { 22 | inherited = StubWriteToFileFunc(errors.New("SPY_WRITE_TO_FILE_FUNC")) 23 | } 24 | return func(result *checker.CheckResult, outPath typedpath.RawPath) error { 25 | *callArgs = append(*callArgs, WriteToFileCallArgs{ 26 | CheckResult: result, 27 | OutPath: outPath, 28 | }) 29 | return inherited(result, outPath) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tool/unity-meta-check-junit/junit/junit_test.go: -------------------------------------------------------------------------------- 1 | package junit 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/unity/checker" 7 | "github.com/DeNA/unity-meta-check/util/typedpath" 8 | "github.com/google/go-cmp/cmp" 9 | "runtime" 10 | "testing" 11 | ) 12 | 13 | func TestWrite(t *testing.T) { 14 | cases := map[string]struct { 15 | Result *checker.CheckResult 16 | Expected string 17 | }{ 18 | "empty (boundary)": { 19 | Result: checker.NewCheckResult([]typedpath.SlashPath{}, []typedpath.SlashPath{}), 20 | Expected: fmt.Sprintf(` 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | `, runtime.Version()), 30 | }, 31 | "both missing and dangling (easy to test)": { 32 | Result: checker.NewCheckResult( 33 | []typedpath.SlashPath{ 34 | typedpath.NewSlashPathUnsafe("path/to/missing.meta"), 35 | }, 36 | []typedpath.SlashPath{ 37 | typedpath.NewSlashPathUnsafe("path/to/dangling.meta"), 38 | }, 39 | ), 40 | Expected: fmt.Sprintf(` 41 | 42 | 43 | 44 | 45 | 46 | 47 | File or directory exists: path/to/missing But .meta is missing: path/to/missing.meta 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | File or directory does not exist: path/to/dangling But .meta is present: path/to/dangling.meta 56 | 57 | 58 | 59 | `, runtime.Version(), runtime.Version()), 60 | }, 61 | } 62 | 63 | for name, c := range cases { 64 | t.Run(name, func(t *testing.T) { 65 | 66 | buf := &bytes.Buffer{} 67 | if err := Write(c.Result, buf); err != nil { 68 | t.Errorf("want nil, got %#v", err) 69 | return 70 | } 71 | 72 | actual := buf.String() 73 | if actual != c.Expected { 74 | t.Error(cmp.Diff(c.Expected, actual)) 75 | return 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tool/unity-meta-check-junit/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/tool/unity-meta-check-junit/cmd" 5 | "github.com/DeNA/unity-meta-check/util/cli" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | main := cmd.NewMain() 11 | exitStatus := main(os.Args[1:], cli.GetProcessInout(), cli.NewEnv()) 12 | os.Exit(int(exitStatus)) 13 | } 14 | -------------------------------------------------------------------------------- /tool/unity-meta-check-junit/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "github.com/DeNA/unity-meta-check/util/cli" 8 | "github.com/DeNA/unity-meta-check/util/typedpath" 9 | ) 10 | 11 | type Options struct { 12 | Version bool 13 | OutPath typedpath.RawPath 14 | } 15 | 16 | func BuildOptions(args []string, procInout cli.ProcessInout) (*Options, error) { 17 | opts := &Options{} 18 | 19 | flags := flag.NewFlagSet("unity-meta-check-junit", flag.ContinueOnError) 20 | flags.SetOutput(procInout.Stderr) 21 | flags.Usage = func() { 22 | _, _ = fmt.Fprint(procInout.Stderr, `usage: unity-meta-check-junit [] [] 23 | 24 | Save a JUnit report file for the result from unity-meta-check via STDIN. 25 | 26 | 27 | output path to write JUnit report 28 | 29 | OPTIONS 30 | `) 31 | flags.PrintDefaults() 32 | 33 | _, _ = fmt.Fprint(procInout.Stderr, ` 34 | EXAMPLE USAGES 35 | $ unity-meta-check | unity-meta-check-junit path/to/junit-report.xml 36 | $ unity-meta-check | unity-meta-check-junit path/to/junit-report.xml | 37 | `) 38 | } 39 | flags.BoolVar(&opts.Version, "version", false, "print version") 40 | 41 | if err := flags.Parse(args); err != nil { 42 | return nil, err 43 | } 44 | 45 | if opts.Version { 46 | return opts, nil 47 | } 48 | 49 | outPaths := flags.Args() 50 | if len(outPaths) < 1 { 51 | return nil, errors.New("must specify a file path to output JUnit report") 52 | } 53 | if len(outPaths) > 1 { 54 | return nil, errors.New("too much arguments") 55 | } 56 | 57 | outPath := typedpath.NewRawPathUnsafe(args[0]) 58 | opts.OutPath = outPath 59 | return opts, nil 60 | } 61 | -------------------------------------------------------------------------------- /tool/unity-meta-check-meta-audit/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "github.com/DeNA/unity-meta-check/unity" 8 | "github.com/DeNA/unity-meta-check/util/cli" 9 | "github.com/DeNA/unity-meta-check/util/typedpath" 10 | "github.com/scylladb/go-set/strset" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "sync" 15 | ) 16 | 17 | func main() { 18 | main := NewMain() 19 | exitStatus := main(os.Args[1:], cli.GetProcessInout(), cli.NewEnv()) 20 | os.Exit(int(exitStatus)) 21 | } 22 | 23 | type metaHistogram struct { 24 | extensions map[string]uint 25 | } 26 | 27 | func NewMain() cli.Command { 28 | return func(args []string, procInout cli.ProcessInout, env cli.Env) cli.ExitStatus { 29 | ignore := strset.New(args...) 30 | 31 | var wg sync.WaitGroup 32 | var mu sync.Mutex 33 | result := make(map[string]*metaHistogram, 1000000) 34 | metaPathCh := make(chan typedpath.RawPath, 4) 35 | var err error 36 | 37 | wg.Add(1) 38 | go func() { 39 | defer wg.Done() 40 | defer close(metaPathCh) 41 | if err2 := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 42 | if !strings.HasSuffix(path, unity.MetaSuffix) { 43 | return nil 44 | } 45 | ext := strings.ToLower(filepath.Ext(strings.TrimSuffix(path, unity.MetaSuffix))) 46 | if !ignore.Has(ext) { 47 | metaPathCh <- typedpath.RawPath(path) 48 | } 49 | return nil 50 | }); err2 != nil { 51 | err = err2 52 | } 53 | }() 54 | 55 | for i := 0; i < 4; i++ { 56 | wg.Add(1) 57 | go func() { 58 | defer wg.Done() 59 | 60 | buf := &bytes.Buffer{} 61 | for metaPath := range metaPathCh { 62 | file, err := os.Open(string(metaPath)) 63 | if err != nil { 64 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 65 | return 66 | } 67 | 68 | buf.Reset() 69 | scanner := bufio.NewScanner(file) 70 | for scanner.Scan() { 71 | line := scanner.Text() 72 | if strings.HasPrefix(line, "guid: ") { 73 | buf.WriteString("guid: _\n") 74 | continue 75 | } 76 | if strings.HasPrefix(line, "timeCreated: ") { 77 | buf.WriteString("timeCreated: _\n") 78 | continue 79 | } 80 | buf.WriteString(line) 81 | buf.WriteByte('\n') 82 | } 83 | _ = file.Close() 84 | masked := buf.String() 85 | ext := filepath.Ext(string(unity.TrimMetaFromRaw(metaPath))) 86 | 87 | mu.Lock() 88 | hist, ok := result[masked] 89 | if ok { 90 | hist.extensions[ext] += 1 91 | } else { 92 | result[masked] = &metaHistogram{ 93 | extensions: map[string]uint{ext: 1}, 94 | } 95 | } 96 | mu.Unlock() 97 | } 98 | }() 99 | } 100 | 101 | wg.Wait() 102 | if err != nil { 103 | _, _ = fmt.Fprintln(procInout.Stderr, err.Error()) 104 | return cli.ExitAbnormal 105 | } 106 | 107 | for masked, hist := range result { 108 | _, _ = fmt.Fprintf(procInout.Stdout, "%v ==================\n%s\n", hist.extensions, masked) 109 | } 110 | 111 | return cli.ExitNormal 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /unity/checker/checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/filecollector" 5 | "github.com/DeNA/unity-meta-check/util/errutil" 6 | "github.com/DeNA/unity-meta-check/util/logging" 7 | "github.com/DeNA/unity-meta-check/util/typedpath" 8 | "sync" 9 | ) 10 | 11 | type Checker func(rootDirAbs typedpath.RawPath, opts *Options) (*CheckResult, error) 12 | 13 | func NewChecker(selectStrategy StrategySelector, logger logging.Logger) Checker { 14 | return func(rootDirAbs typedpath.RawPath, opts *Options) (*CheckResult, error) { 15 | strategy, err := selectStrategy(rootDirAbs, opts) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | check := newCheckerByStrategy(strategy, logger) 21 | return check(rootDirAbs, opts) 22 | } 23 | } 24 | 25 | func newCheckerByStrategy(strategy Strategy, logger logging.Logger) Checker { 26 | return func(rootDirAbs typedpath.RawPath, opts *Options) (*CheckResult, error) { 27 | ch := make(chan filecollector.Entry) 28 | 29 | var wg sync.WaitGroup 30 | var errsMu sync.Mutex 31 | errs := make([]error, 0) 32 | 33 | wg.Add(1) 34 | go func() { 35 | defer wg.Done() 36 | defer close(ch) 37 | if err := strategy.CollectFiles(rootDirAbs, &filecollector.Options{IgnoreCase: opts.IgnoreCase}, ch); err != nil { 38 | errsMu.Lock() 39 | errs = append(errs, err) 40 | errsMu.Unlock() 41 | return 42 | } 43 | }() 44 | 45 | check := NewCheckingWorker(strategy.RequiresMeta, logger) 46 | result, err := check(rootDirAbs, opts.IgnoreCase, ch) 47 | if err != nil { 48 | errsMu.Lock() 49 | errs = append(errs, err) 50 | errsMu.Unlock() 51 | } 52 | if len(errs) > 0 { 53 | return nil, errutil.NewErrors(errs) 54 | } 55 | return result, nil 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /unity/checker/checker_stub.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | ) 6 | 7 | func StubChecker(result *CheckResult, err error) Checker { 8 | return func(_ typedpath.RawPath, _ *Options) (*CheckResult, error) { 9 | return result, err 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /unity/checker/checker_test.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/filecollector" 5 | "github.com/DeNA/unity-meta-check/unity" 6 | "github.com/DeNA/unity-meta-check/util/logging" 7 | "github.com/DeNA/unity-meta-check/util/typedpath" 8 | "github.com/google/go-cmp/cmp" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func TestCheck(t *testing.T) { 14 | strategy := Strategy{ 15 | CollectFiles: filecollector.StubSuccessfulFileAggregator([]filecollector.Entry{ 16 | {Path: "Assets/MissingMeta", IsDir: false}, 17 | {Path: "Assets/DanglingMeta.meta", IsDir: false}, 18 | }), 19 | RequiresMeta: unity.ConstMetaNecessity(true), 20 | } 21 | spyLogger := logging.SpyLogger() 22 | opts := &Options{ 23 | IgnoreCase: false, 24 | TargetType: TargetTypeIsUnityProjectRootDirectory, 25 | } 26 | 27 | checker := newCheckerByStrategy(strategy, spyLogger) 28 | actual, err := checker("/path/to/Project", opts) 29 | 30 | if err != nil { 31 | t.Log(spyLogger.Logs.String()) 32 | t.Errorf("want nil, got %s", err.Error()) 33 | return 34 | } 35 | 36 | expected := &CheckResult{ 37 | MissingMeta: []typedpath.SlashPath{ 38 | "Assets/MissingMeta.meta", 39 | }, 40 | DanglingMeta: []typedpath.SlashPath{ 41 | "Assets/DanglingMeta.meta", 42 | }, 43 | } 44 | if !reflect.DeepEqual(actual, expected) { 45 | t.Log(spyLogger.Logs.String()) 46 | t.Error(cmp.Diff(expected, actual)) 47 | return 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /unity/checker/options.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | type TargetType string 4 | 5 | const ( 6 | // TargetTypeIsUnityProjectRootDirectory means the target root directory point to a root directory of the Unity project. 7 | TargetTypeIsUnityProjectRootDirectory TargetType = "TargetTypeIsUnityProjectRootDirectory" 8 | 9 | // TargetTypeIsUnityProjectSubDirectory means the target root directory point to a sub directory that need meta files of the Unity project. 10 | // NOTE: this including UPM packages (because Packages/com.example.foo/ is a sub directory that need meta files of Unity projects). 11 | TargetTypeIsUnityProjectSubDirectory TargetType = "TargetTypeIsUnityProjectSubDirectory" 12 | ) 13 | 14 | // Options for Checker. 15 | // NOTE: We should keep this options simple because the newWorker logic is already complicated. 16 | // Implement on ResultFilter if you want to filter the CheckResult. 17 | type Options struct { 18 | IgnoreCase bool 19 | IgnoreSubmodulesAndNested bool 20 | TargetType TargetType 21 | } -------------------------------------------------------------------------------- /unity/checker/result.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import "github.com/DeNA/unity-meta-check/util/typedpath" 4 | 5 | type CheckResult struct { 6 | MissingMeta []typedpath.SlashPath 7 | DanglingMeta []typedpath.SlashPath 8 | } 9 | 10 | func (c CheckResult) Empty() bool { 11 | return c.Len() == 0 12 | } 13 | 14 | func (c CheckResult) Len() int { 15 | return len(c.MissingMeta) + len(c.DanglingMeta) 16 | } 17 | 18 | func NewCheckResult(missingMeta []typedpath.SlashPath, danglingMeta []typedpath.SlashPath) *CheckResult { 19 | return &CheckResult{ 20 | MissingMeta: missingMeta, 21 | DanglingMeta: danglingMeta, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /unity/checker/strategy.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/filecollector" 6 | "github.com/DeNA/unity-meta-check/filecollector/repofinder" 7 | "github.com/DeNA/unity-meta-check/git" 8 | "github.com/DeNA/unity-meta-check/unity" 9 | "github.com/DeNA/unity-meta-check/util/logging" 10 | "github.com/DeNA/unity-meta-check/util/typedpath" 11 | ) 12 | 13 | type Strategy struct { 14 | CollectFiles filecollector.FileAggregator 15 | RequiresMeta unity.MetaNecessity 16 | } 17 | 18 | type StrategySelector func(rootDirAbs typedpath.RawPath, opts *Options) (Strategy, error) 19 | 20 | // NewStrategySelector returns a checker strategy for either Unity projects or UPM packages. 21 | func NewStrategySelector(findPackages unity.FindPackages, lsFiles git.LsFiles, logger logging.Logger) StrategySelector { 22 | return func(rootDirAbs typedpath.RawPath, opts *Options) (Strategy, error) { 23 | switch opts.TargetType { 24 | case TargetTypeIsUnityProjectRootDirectory: 25 | foundPackages, err := findPackages(rootDirAbs) 26 | if err != nil { 27 | return Strategy{}, fmt.Errorf("cannot find local packages: %#v", err) 28 | } 29 | 30 | findRepo := NewRepoFinderForUnityProj(rootDirAbs, opts, foundPackages) 31 | return Strategy{ 32 | CollectFiles: filecollector.NewFileAggregator(lsFiles, findRepo, logger), 33 | RequiresMeta: unity.NewMetaNecessityInUnityProject(unity.FoundPackagesToSlashRelPaths(foundPackages)), 34 | }, nil 35 | 36 | case TargetTypeIsUnityProjectSubDirectory: 37 | findRepo := NewRepoFinderFactoryForUPM(rootDirAbs, opts) 38 | return Strategy{ 39 | CollectFiles: filecollector.NewFileAggregator(lsFiles, findRepo, logger), 40 | RequiresMeta: unity.NewMetaNecessityInUnityProjectSubDir(), 41 | }, nil 42 | 43 | default: 44 | return Strategy{}, fmt.Errorf("unsupported checking type: %q", opts.TargetType) 45 | } 46 | } 47 | } 48 | 49 | func NewRepoFinderForUnityProj(rootDirAbs typedpath.RawPath, opts *Options, foundPackages []*unity.FoundPackage) repofinder.RepoFinder { 50 | if opts.IgnoreSubmodulesAndNested { 51 | return repofinder.StubRepoFinder(nil, nil) 52 | } 53 | 54 | repoFinders := make([]repofinder.RepoFinder, len(foundPackages)+1) 55 | repoFinders[0] = repofinder.New(rootDirAbs, typedpath.RawPath(unity.AssetsDirBaseName)) 56 | i := 1 57 | for _, foundPkg := range foundPackages { 58 | repoFinders[i] = repofinder.New(rootDirAbs, foundPkg.RelPath) 59 | i++ 60 | } 61 | findRepo := repofinder.Compose(repoFinders) 62 | return findRepo 63 | } 64 | 65 | func NewRepoFinderFactoryForUPM(rootDirAbs typedpath.RawPath, opts *Options) repofinder.RepoFinder { 66 | if opts.IgnoreSubmodulesAndNested { 67 | return repofinder.StubRepoFinder(nil, nil) 68 | } 69 | 70 | return repofinder.New(rootDirAbs, ".") 71 | } 72 | -------------------------------------------------------------------------------- /unity/checker/testdata/EmptyManifest/Packages/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": {} 3 | } 4 | -------------------------------------------------------------------------------- /unity/checker/testdata/LocalsManifest/LocalPackages/com.example.local/.gitempty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/unity/checker/testdata/LocalsManifest/LocalPackages/com.example.local/.gitempty -------------------------------------------------------------------------------- /unity/checker/testdata/LocalsManifest/Packages/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.example.local": "file:../LocalPackages/com.example.local" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /unity/checker/testdata/NoLocalsManifest/Packages/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.example": "1.0.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /unity/manifest.go: -------------------------------------------------------------------------------- 1 | package unity 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/DeNA/unity-meta-check/util/typedpath" 6 | "os" 7 | ) 8 | 9 | type ManifestJson struct { 10 | Dependencies map[string]string `json:"dependencies"` 11 | } 12 | 13 | var ManifestBasename typedpath.BaseName = "manifest.json" 14 | 15 | func ReadManifest(path typedpath.RawPath) (*ManifestJson, error) { 16 | bytes, err := os.ReadFile(string(path)) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return parseManifestJson(bytes) 21 | } 22 | 23 | func parseManifestJson(bytes []byte) (*ManifestJson, error) { 24 | var manifestJson ManifestJson 25 | if err := json.Unmarshal(bytes, &manifestJson); err != nil { 26 | return nil, err 27 | } 28 | return &manifestJson, nil 29 | } 30 | -------------------------------------------------------------------------------- /unity/manifest_test.go: -------------------------------------------------------------------------------- 1 | package unity 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestReadManifestJson(t *testing.T) { 10 | json := []byte(`{ 11 | "scopedRegistries": [], 12 | "dependencies": { 13 | "foo": "1.2.3", 14 | "bar.baz": "file:../Bar/Buz" 15 | } 16 | }`) 17 | 18 | actual, err := parseManifestJson(json) 19 | if err != nil { 20 | t.Errorf("want nil, got %#v", err) 21 | return 22 | } 23 | 24 | expected := &ManifestJson{Dependencies: map[string]string{ 25 | "foo": "1.2.3", 26 | "bar.baz": "file:../Bar/Buz", 27 | }} 28 | 29 | if !reflect.DeepEqual(actual, expected) { 30 | t.Error(cmp.Diff(expected, actual)) 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /unity/meta/defaultimportergen.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | type DefaultImporterGen struct { 10 | GUID *GUID 11 | } 12 | 13 | var _ Gen = MonoImporterGen{} 14 | 15 | func (t DefaultImporterGen) WriteTo(writer io.Writer) (int64, error) { 16 | content := strings.TrimLeft(fmt.Sprintf(` 17 | fileFormatVersion: 2 18 | guid: %s 19 | DefaultImporter: 20 | externalObjects: {} 21 | userData: 22 | assetBundleName: 23 | assetBundleVariant: 24 | `, t.GUID.String()), "\n") 25 | n, err := io.WriteString(writer, content) 26 | return int64(n), err 27 | } 28 | -------------------------------------------------------------------------------- /unity/meta/defaultimportergen_test.go: -------------------------------------------------------------------------------- 1 | package meta_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/DeNA/unity-meta-check/unity/meta" 8 | "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestDefaultoImporterGen_WriteTo(t *testing.T) { 12 | gen := meta.DefaultImporterGen{GUID: meta.ZeroGUID()} 13 | 14 | buf := &bytes.Buffer{} 15 | _, err := gen.WriteTo(buf) 16 | if err != nil { 17 | t.Errorf("want nil, got %#v", err) 18 | return 19 | } 20 | 21 | actual := buf.String() 22 | expected := `fileFormatVersion: 2 23 | guid: 00000000000000000000000000000000 24 | DefaultImporter: 25 | externalObjects: {} 26 | userData: 27 | assetBundleName: 28 | assetBundleVariant: 29 | ` 30 | if actual != expected { 31 | t.Error(cmp.Diff(expected, actual)) 32 | return 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /unity/meta/foldergen.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | type DefaultImporterFolderGen struct { 10 | GUID *GUID 11 | } 12 | 13 | var _ Gen = DefaultImporterFolderGen{} 14 | 15 | func (f DefaultImporterFolderGen) WriteTo(writer io.Writer) (int64, error) { 16 | content := strings.TrimLeft(fmt.Sprintf(` 17 | fileFormatVersion: 2 18 | guid: %s 19 | folderAsset: yes 20 | DefaultImporter: 21 | externalObjects: {} 22 | userData: 23 | assetBundleName: 24 | assetBundleVariant: 25 | `, f.GUID.String()), "\n") 26 | n, err := io.WriteString(writer, content) 27 | return int64(n), err 28 | } 29 | -------------------------------------------------------------------------------- /unity/meta/foldergen_test.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "bytes" 5 | "github.com/google/go-cmp/cmp" 6 | "testing" 7 | ) 8 | 9 | func TestFolderGen_WriteTo(t *testing.T) { 10 | meta := DefaultImporterFolderGen{ZeroGUID()} 11 | 12 | buf := &bytes.Buffer{} 13 | _, err := meta.WriteTo(buf) 14 | if err != nil { 15 | t.Errorf("want nil, got %#v", err) 16 | return 17 | } 18 | 19 | actual := buf.String() 20 | expected := `fileFormatVersion: 2 21 | guid: 00000000000000000000000000000000 22 | folderAsset: yes 23 | DefaultImporter: 24 | externalObjects: {} 25 | userData: 26 | assetBundleName: 27 | assetBundleVariant: 28 | ` 29 | if actual != expected { 30 | t.Error(cmp.Diff(expected, actual)) 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /unity/meta/gen.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Gen interface { 8 | WriteTo(writer io.Writer) (int64, error) 9 | } 10 | 11 | -------------------------------------------------------------------------------- /unity/meta/guid.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | ) 7 | 8 | type GUID struct { 9 | bytes []byte 10 | } 11 | 12 | const GUIDByteLength = 16 13 | 14 | func NewGUID(bytes []byte) (*GUID, error) { 15 | if len(bytes) != GUIDByteLength { 16 | return nil, fmt.Errorf("length of GUID must be %d bytes", GUIDByteLength) 17 | } 18 | return &GUID{bytes}, nil 19 | } 20 | 21 | func (g GUID) String() string { 22 | s := make([]byte, hex.EncodedLen(GUIDByteLength)) 23 | hex.Encode(s, g.bytes) 24 | return string(s) 25 | } 26 | -------------------------------------------------------------------------------- /unity/meta/guid_stub.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | func AnyGUID() *GUID { 4 | guid, err := NewGUID([]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}) 5 | if err != nil { 6 | panic(err.Error()) 7 | } 8 | return guid 9 | } 10 | 11 | func ZeroGUID() *GUID { 12 | guid, err := NewGUID(make([]byte, GUIDByteLength)) 13 | if err != nil { 14 | panic(err.Error()) 15 | } 16 | return guid 17 | } 18 | -------------------------------------------------------------------------------- /unity/meta/guid_test.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewGUID(t *testing.T) { 8 | guid, err := NewGUID([]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}) 9 | if err != nil { 10 | t.Errorf("want nil, got %#v", err) 11 | return 12 | } 13 | 14 | actual := guid.String() 15 | expected := "000102030405060708090a0b0c0d0e0f" 16 | if actual != expected { 17 | t.Errorf("want %q, got %q", expected, actual) 18 | return 19 | } 20 | } 21 | 22 | func TestRandomGUIDGenerator(t *testing.T) { 23 | guidGen := RandomGUIDGenerator() 24 | _, err := guidGen() 25 | if err != nil { 26 | t.Errorf("want nil, got %#v", err) 27 | return 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /unity/meta/guidgen.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import "crypto/rand" 4 | 5 | type GUIDGen func() (*GUID, error) 6 | 7 | func RandomGUIDGenerator() GUIDGen { 8 | return func() (*GUID, error) { 9 | bytes := make([]byte, GUIDByteLength) 10 | _, err := rand.Read(bytes) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return NewGUID(bytes) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /unity/meta/guidgen_stub.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | func StubGUIDGen(guid *GUID, err error) GUIDGen { 4 | return func() (*GUID, error) { 5 | return guid, err 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /unity/meta/monoimportergen.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | type MonoImporterGen struct { 10 | GUID *GUID 11 | } 12 | 13 | var _ Gen = MonoImporterGen{} 14 | 15 | func (t MonoImporterGen) WriteTo(writer io.Writer) (int64, error) { 16 | content := strings.TrimLeft(fmt.Sprintf(` 17 | fileFormatVersion: 2 18 | guid: %s 19 | MonoImporter: 20 | externalObjects: {} 21 | userData: 22 | assetBundleName: 23 | assetBundleVariant: 24 | serializedVersion: 2 25 | defaultReferences: [] 26 | executionOrder: 0 27 | icon: {instanceID: 0} 28 | `, t.GUID.String()), "\n") 29 | n, err := io.WriteString(writer, content) 30 | return int64(n), err 31 | } 32 | -------------------------------------------------------------------------------- /unity/meta/monoimportergen_test.go: -------------------------------------------------------------------------------- 1 | package meta_test 2 | 3 | import ( 4 | "bytes" 5 | "github.com/DeNA/unity-meta-check/unity/meta" 6 | "github.com/google/go-cmp/cmp" 7 | "testing" 8 | ) 9 | 10 | func TestMonoImporterGen_WriteTo(t *testing.T) { 11 | gen := meta.MonoImporterGen{GUID: meta.ZeroGUID()} 12 | 13 | buf := &bytes.Buffer{} 14 | _, err := gen.WriteTo(buf) 15 | if err != nil { 16 | t.Errorf("want nil, got %#v", err) 17 | return 18 | } 19 | 20 | actual := buf.String() 21 | expected := `fileFormatVersion: 2 22 | guid: 00000000000000000000000000000000 23 | MonoImporter: 24 | externalObjects: {} 25 | userData: 26 | assetBundleName: 27 | assetBundleVariant: 28 | serializedVersion: 2 29 | defaultReferences: [] 30 | executionOrder: 0 31 | icon: {instanceID: 0} 32 | ` 33 | if actual != expected { 34 | t.Error(cmp.Diff(expected, actual)) 35 | return 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /unity/meta/textscriptimportergen.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | type TextScriptImporterGen struct { 10 | GUID *GUID 11 | } 12 | 13 | var _ Gen = TextScriptImporterGen{} 14 | 15 | func (t TextScriptImporterGen) WriteTo(writer io.Writer) (int64, error) { 16 | content := strings.TrimLeft(fmt.Sprintf(` 17 | fileFormatVersion: 2 18 | guid: %s 19 | TextScriptImporter: 20 | externalObjects: {} 21 | userData: 22 | assetBundleName: 23 | assetBundleVariant: 24 | `, t.GUID.String()), "\n") 25 | n, err := io.WriteString(writer, content) 26 | return int64(n), err 27 | } 28 | 29 | -------------------------------------------------------------------------------- /unity/meta/textscriptimportergen_test.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "bytes" 5 | "github.com/google/go-cmp/cmp" 6 | "testing" 7 | ) 8 | 9 | func TestTextScriptImporterGen_WriteTo(t *testing.T) { 10 | meta := TextScriptImporterGen{ZeroGUID()} 11 | 12 | buf := &bytes.Buffer{} 13 | _, err := meta.WriteTo(buf) 14 | if err != nil { 15 | t.Errorf("want nil, got %#v", err) 16 | return 17 | } 18 | 19 | actual := buf.String() 20 | expected := `fileFormatVersion: 2 21 | guid: 00000000000000000000000000000000 22 | TextScriptImporter: 23 | externalObjects: {} 24 | userData: 25 | assetBundleName: 26 | assetBundleVariant: 27 | ` 28 | if actual != expected { 29 | t.Error(cmp.Diff(expected, actual)) 30 | return 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /unity/metanecessity_stub.go: -------------------------------------------------------------------------------- 1 | package unity 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | ) 6 | 7 | func ConstMetaNecessity(result bool) MetaNecessity { 8 | return func(typedpath.SlashPath) bool { 9 | return result 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /unity/package_test.go: -------------------------------------------------------------------------------- 1 | package unity 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/util/logging" 6 | "github.com/DeNA/unity-meta-check/util/typedpath" 7 | "github.com/google/go-cmp/cmp" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestNewFindPackages(t *testing.T) { 13 | cwd, err := typedpath.Getwd() 14 | if err != nil { 15 | panic(err.Error()) 16 | } 17 | 18 | cases := []struct{ 19 | RootDirAbs typedpath.RawPath 20 | Expected []*FoundPackage 21 | } { 22 | { 23 | cwd.JoinRawPath(typedpath.NewRawPath("testdata", "EmptyManifest")), 24 | []*FoundPackage{}, 25 | }, 26 | { 27 | cwd.JoinRawPath(typedpath.NewRawPath("testdata", "NoLocalsManifest")), 28 | []*FoundPackage{ 29 | { 30 | FilePrefix: false, 31 | AbsPath: cwd.JoinRawPath(typedpath.NewRawPath("testdata", "NoLocalsManifest", "Packages", "com.example.exists")), 32 | RelPath: typedpath.NewRawPath("Packages", "com.example.exists"), 33 | }, 34 | }, 35 | }, 36 | { 37 | cwd.JoinRawPath(typedpath.NewRawPath("testdata", "LocalsManifest")), 38 | []*FoundPackage{ 39 | { 40 | FilePrefix: true, 41 | AbsPath: cwd.JoinRawPath(typedpath.NewRawPath("testdata", "LocalsManifest", "LocalPackages", "com.example.local")), 42 | RelPath: typedpath.NewRawPath("LocalPackages", "com.example.local"), 43 | }, 44 | }, 45 | }, 46 | } 47 | 48 | for _, c := range cases { 49 | t.Run(fmt.Sprintf("%s/Packages/manifest.json -> %v", c.RootDirAbs, c.Expected), func(t *testing.T) { 50 | spyLogger := logging.SpyLogger() 51 | findPackages := NewFindPackages(spyLogger) 52 | 53 | actual, err := findPackages(c.RootDirAbs) 54 | if err != nil { 55 | t.Log(spyLogger.Logs.String()) 56 | t.Errorf("want nil, got %#v", err) 57 | return 58 | } 59 | 60 | if !reflect.DeepEqual(actual, c.Expected) { 61 | t.Log(spyLogger.Logs.String()) 62 | t.Error(cmp.Diff(c.Expected, actual)) 63 | return 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /unity/testdata/EmptyManifest/Packages/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": {} 3 | } 4 | -------------------------------------------------------------------------------- /unity/testdata/LocalsManifest/LocalPackages/com.example.local/.gitempty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/unity/testdata/LocalsManifest/LocalPackages/com.example.local/.gitempty -------------------------------------------------------------------------------- /unity/testdata/LocalsManifest/Packages/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.example.local": "file:../LocalPackages/com.example.local" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /unity/testdata/NoLocalsManifest/Packages/com.example.exists/.gitempty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeNA/unity-meta-check/d682fd262e2c7b142bc4f1882aa930acf37a7d7e/unity/testdata/NoLocalsManifest/Packages/com.example.exists/.gitempty -------------------------------------------------------------------------------- /unity/testdata/NoLocalsManifest/Packages/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.example.exists": "1.0.0", 4 | "com.example.does.not.exist": "1.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /util/chanutil/fromslice.go: -------------------------------------------------------------------------------- 1 | package chanutil 2 | 3 | func FromSlice[T interface{}](ss []T) <-chan T { 4 | ch := make(chan T) 5 | go func() { 6 | defer close(ch) 7 | for _, str := range ss { 8 | ch <- str 9 | } 10 | }() 11 | return ch 12 | } 13 | -------------------------------------------------------------------------------- /util/chanutil/toslice.go: -------------------------------------------------------------------------------- 1 | package chanutil 2 | 3 | func ToSlice[T interface{}](ch <-chan T) []T { 4 | result := make([]T, 0) 5 | for x := range ch { 6 | result = append(result, x) 7 | } 8 | return result 9 | } 10 | -------------------------------------------------------------------------------- /util/cli/cmd.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type Command func(args []string, procInout ProcessInout, env Env) ExitStatus 4 | 5 | type ExitStatus int 6 | 7 | const ( 8 | // ExitNormal means exit successfully. 9 | // SEE: http://tldp.org/LDP/abs/html/exitcodes.html 10 | ExitNormal ExitStatus = 0 11 | 12 | // ExitAbnormal means exit not successfully. 13 | // SEE: http://tldp.org/LDP/abs/html/exitcodes.html 14 | ExitAbnormal ExitStatus = 1 15 | ) 16 | -------------------------------------------------------------------------------- /util/cli/env.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "os" 4 | 5 | type Env func(key string) string 6 | 7 | func NewEnv() Env { 8 | return func(key string) string { 9 | return os.Getenv(key) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /util/cli/env_stub.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | func AnyEnv() Env { 4 | return ConstEnv("ANY_ENV") 5 | } 6 | 7 | func ConstEnv(result string) Env { 8 | return func(string) string { 9 | return result 10 | } 11 | } 12 | 13 | func StubEnv(m map[string]string) Env { 14 | return func(n string) string { 15 | return m[n] 16 | } 17 | } -------------------------------------------------------------------------------- /util/cli/loglevel.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "github.com/DeNA/unity-meta-check/util/logging" 4 | 5 | func GetLogLevel(debug, silent bool) logging.Severity { 6 | if debug { 7 | return logging.SeverityDebug 8 | } 9 | if silent { 10 | return logging.SeverityWarn 11 | } 12 | return logging.SeverityInfo 13 | } 14 | -------------------------------------------------------------------------------- /util/cli/opt/flag.go: -------------------------------------------------------------------------------- 1 | package opt 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | type ( 9 | Type string 10 | Name string 11 | Desc string 12 | 13 | Option interface { 14 | Required() bool 15 | Name() Name 16 | Desc() Desc 17 | Type() Type 18 | DefaultValueText() string 19 | } 20 | ) 21 | 22 | const ( 23 | TypeString Type = "string" 24 | TypeBool Type = "bool" 25 | ) 26 | 27 | func NewOptionalStringOption(name Name, desc Desc, defaultValue string) StringOption { 28 | return StringOption{ 29 | required: false, 30 | name: name, 31 | desc: desc, 32 | DefaultValue: defaultValue, 33 | } 34 | } 35 | 36 | func NewRequiredStringOption(name Name, desc Desc) StringOption { 37 | return StringOption{ 38 | required: true, 39 | name: name, 40 | desc: desc, 41 | } 42 | } 43 | 44 | type StringOption struct { 45 | required bool 46 | name Name 47 | desc Desc 48 | DefaultValue string 49 | } 50 | 51 | func (s StringOption) Name() Name { 52 | return s.name 53 | } 54 | 55 | func (s StringOption) Desc() Desc { 56 | return s.desc 57 | } 58 | 59 | func (s StringOption) Type() Type { 60 | return TypeString 61 | } 62 | 63 | func (s StringOption) Required() bool { 64 | return s.required 65 | } 66 | 67 | func (s StringOption) DefaultValueText() string { 68 | return fmt.Sprintf("%q", s.DefaultValue) 69 | } 70 | 71 | func NewOptionalBoolOption(name Name, desc Desc, defaultValue bool) BoolOption { 72 | return BoolOption{ 73 | required: false, 74 | name: name, 75 | desc: desc, 76 | DefaultValue: defaultValue, 77 | } 78 | } 79 | 80 | func NewRequiredBoolOption(name Name, desc Desc) BoolOption { 81 | return BoolOption{ 82 | required: true, 83 | name: name, 84 | desc: desc, 85 | } 86 | } 87 | 88 | type BoolOption struct { 89 | required bool 90 | name Name 91 | desc Desc 92 | DefaultValue bool 93 | } 94 | 95 | func (b BoolOption) Name() Name { 96 | return b.name 97 | } 98 | 99 | func (b BoolOption) Desc() Desc { 100 | return b.desc 101 | } 102 | 103 | func (b BoolOption) Type() Type { 104 | return TypeBool 105 | } 106 | 107 | func (b BoolOption) Required() bool { 108 | return b.required 109 | } 110 | 111 | func (b BoolOption) DefaultValueText() string { 112 | return fmt.Sprintf("%t", b.DefaultValue) 113 | } 114 | 115 | func Sort(flags []Option) { 116 | sort.Slice(flags, func(i, j int) bool { 117 | if flags[i].Required() != flags[i].Required() { 118 | return flags[i].Required() 119 | } 120 | return flags[i].Name() > flags[j].Name() 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /util/cli/opt/flag/flag.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/DeNA/unity-meta-check/util/cli/opt" 7 | ) 8 | 9 | func descSuffix(required bool, desc opt.Desc) string { 10 | if required { 11 | return fmt.Sprintf("%s (required)", desc) 12 | } 13 | return fmt.Sprintf("%s (optional)", desc) 14 | } 15 | 16 | func DefineString(f *flag.FlagSet, o opt.StringOption) *string { 17 | return f.String(string(o.Name()), o.DefaultValue, descSuffix(o.Required(), o.Desc())) 18 | } 19 | 20 | func DefineBool(f *flag.FlagSet, o opt.BoolOption) *bool { 21 | return f.Bool(string(o.Name()), o.DefaultValue, descSuffix(o.Required(), o.Desc())) 22 | } 23 | -------------------------------------------------------------------------------- /util/cli/opt/flag/flag_test.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "github.com/DeNA/unity-meta-check/util/cli/opt" 7 | "github.com/google/go-cmp/cmp" 8 | "testing" 9 | ) 10 | 11 | func TestDefineHelp(t *testing.T) { 12 | t.Run("required string option (easies case)", func(t *testing.T) { 13 | buf := &bytes.Buffer{} 14 | flags := flag.NewFlagSet("test", flag.ContinueOnError) 15 | flags.SetOutput(buf) 16 | 17 | o := opt.NewRequiredStringOption("req-str", "required string option") 18 | DefineString(flags, o) 19 | 20 | flags.PrintDefaults() 21 | 22 | expected := ` 23 | -req-str string 24 | required string option (required) 25 | `[1:] 26 | 27 | if expected != buf.String() { 28 | t.Error(cmp.Diff(expected, buf.String())) 29 | } 30 | }) 31 | 32 | t.Run("optional string option (easies case)", func(t *testing.T) { 33 | buf := &bytes.Buffer{} 34 | flags := flag.NewFlagSet("test", flag.ContinueOnError) 35 | flags.SetOutput(buf) 36 | 37 | o := opt.NewOptionalStringOption("opt-str", "optional string option", "DEFAULT") 38 | DefineString(flags, o) 39 | 40 | flags.PrintDefaults() 41 | 42 | expected := ` 43 | -opt-str string 44 | optional string option (optional) (default "DEFAULT") 45 | `[1:] 46 | 47 | if expected != buf.String() { 48 | t.Error(cmp.Diff(expected, buf.String())) 49 | } 50 | }) 51 | 52 | t.Run("required bool option (easies case)", func(t *testing.T) { 53 | buf := &bytes.Buffer{} 54 | flags := flag.NewFlagSet("test", flag.ContinueOnError) 55 | flags.SetOutput(buf) 56 | 57 | o := opt.NewRequiredBoolOption("req-bool", "required bool option") 58 | DefineBool(flags, o) 59 | 60 | flags.PrintDefaults() 61 | 62 | expected := ` 63 | -req-bool 64 | required bool option (required) 65 | `[1:] 66 | 67 | if expected != buf.String() { 68 | t.Error(cmp.Diff(expected, buf.String())) 69 | } 70 | }) 71 | 72 | t.Run("optional bool option (easies case)", func(t *testing.T) { 73 | buf := &bytes.Buffer{} 74 | flags := flag.NewFlagSet("test", flag.ContinueOnError) 75 | flags.SetOutput(buf) 76 | 77 | o := opt.NewOptionalBoolOption("opt-bool", "optional bool option", true) 78 | DefineBool(flags, o) 79 | 80 | flags.PrintDefaults() 81 | 82 | expected := ` 83 | -opt-bool 84 | optional bool option (optional) (default true) 85 | `[1:] 86 | 87 | if expected != buf.String() { 88 | t.Error(cmp.Diff(expected, buf.String())) 89 | } 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /util/cli/opt/flag_test.go: -------------------------------------------------------------------------------- 1 | package opt 2 | 3 | var _ Option = StringOption{} 4 | var _ Option = BoolOption{} 5 | -------------------------------------------------------------------------------- /util/cli/opt/ghactions/ghactions.go: -------------------------------------------------------------------------------- 1 | package ghactions 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/util/cli/opt" 6 | "io" 7 | ) 8 | 9 | func WriteTo(w io.Writer, flags ...opt.Option) (i int64, err error) { 10 | opt.Sort(flags) 11 | 12 | for _, f := range flags { 13 | var j int 14 | if f.Required() { 15 | j, err = fmt.Fprintf(w, ` 16 | %q: 17 | description: %q 18 | required: true 19 | `[1:], f.Name(), f.Desc()) 20 | } else { 21 | var defVal string 22 | defVal, err = defaultValue(f) 23 | if err != nil { 24 | return 25 | } 26 | 27 | j, err = fmt.Fprintf(w, ` 28 | %q: 29 | description: %q 30 | required: false 31 | default: %s 32 | `[1:], f.Name(), f.Desc(), defVal) 33 | } 34 | 35 | i += int64(j) 36 | if err != nil { 37 | return 38 | } 39 | } 40 | 41 | return 42 | } 43 | 44 | func defaultValue(f opt.Option) (string, error) { 45 | switch f.(type) { 46 | case opt.StringOption: 47 | return fmt.Sprintf("%q", f.(opt.StringOption).DefaultValue), nil 48 | case opt.BoolOption: 49 | return fmt.Sprintf("%v", f.(opt.BoolOption).DefaultValue), nil 50 | default: 51 | return "", fmt.Errorf("unknown opt.Option type: %#v", f) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /util/cli/opt/ghactions/ghactions_test.go: -------------------------------------------------------------------------------- 1 | package ghactions 2 | 3 | import ( 4 | "bytes" 5 | "github.com/DeNA/unity-meta-check/util/cli/opt" 6 | "github.com/google/go-cmp/cmp" 7 | "testing" 8 | ) 9 | 10 | func TestWriteTo(t *testing.T) { 11 | cases := map[string]struct { 12 | Options []opt.Option 13 | Expected string 14 | }{ 15 | "empty (boundary)": { 16 | Options: []opt.Option{}, 17 | Expected: ``, 18 | }, 19 | "required string option (easiest case)": { 20 | Options: []opt.Option{ 21 | opt.NewRequiredStringOption("req-str", "required string option"), 22 | }, 23 | Expected: ` 24 | "req-str": 25 | description: "required string option" 26 | required: true 27 | `[1:], 28 | }, 29 | "optional string option (easiest case)": { 30 | Options: []opt.Option{ 31 | opt.NewOptionalStringOption("opt-str", "optional string option", "DEFAULT"), 32 | }, 33 | Expected: ` 34 | "opt-str": 35 | description: "optional string option" 36 | required: false 37 | default: "DEFAULT" 38 | `[1:], 39 | }, 40 | "required bool option (easiest case)": { 41 | Options: []opt.Option{ 42 | opt.NewRequiredBoolOption("req-bool", "required bool option"), 43 | }, 44 | Expected: ` 45 | "req-bool": 46 | description: "required bool option" 47 | required: true 48 | `[1:], 49 | }, 50 | "optional bool option (easiest case)": { 51 | Options: []opt.Option{ 52 | opt.NewOptionalBoolOption("opt-bool", "optional bool option", true), 53 | }, 54 | Expected: ` 55 | "opt-bool": 56 | description: "optional bool option" 57 | required: false 58 | default: true 59 | `[1:], 60 | }, 61 | } 62 | 63 | for desc, c := range cases { 64 | t.Run(desc, func(t *testing.T) { 65 | buf := &bytes.Buffer{} 66 | 67 | _, err := WriteTo(buf, c.Options...) 68 | if err != nil { 69 | t.Errorf("want nil, got %#v", err) 70 | return 71 | } 72 | 73 | if buf.String() != c.Expected { 74 | t.Error(cmp.Diff(c.Expected, buf.String())) 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /util/cli/opt/markdown/markdown.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/util/cli/opt" 6 | "io" 7 | ) 8 | 9 | func WriteTo(w io.Writer, opts ...opt.Option) (i int64, err error) { 10 | opt.Sort(opts) 11 | 12 | j, err := fmt.Fprintln(w, ` 13 | | Option | Description | Required or Default Value | 14 | |:-------|:------------|:--------------------------|`[1:]) 15 | i += int64(j) 16 | if err != nil { 17 | return 18 | } 19 | 20 | for _, o := range opts { 21 | var required string 22 | if o.Required() { 23 | required = "required" 24 | } else { 25 | required = fmt.Sprintf("optional (default: `%s`)", o.DefaultValueText()) 26 | } 27 | 28 | var oText string 29 | oText, err = optionText(o) 30 | if err != nil { 31 | return 32 | } 33 | 34 | var j int 35 | j, err = fmt.Fprintf(w, "| `%s` | %s | %s |\n", oText, o.Desc(), required) 36 | i += int64(j) 37 | if err != nil { 38 | return 39 | } 40 | } 41 | 42 | return 43 | } 44 | 45 | func optionText(o opt.Option) (string, error) { 46 | switch o.(type) { 47 | case opt.StringOption: 48 | return fmt.Sprintf("--%s ", o.Name()), nil 49 | case opt.BoolOption: 50 | return fmt.Sprintf("--%s", o.Name()), nil 51 | default: 52 | return "", fmt.Errorf("unknown option type: %#v", o) 53 | } 54 | } -------------------------------------------------------------------------------- /util/cli/opt/markdown/markdown_test.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "bytes" 5 | "github.com/DeNA/unity-meta-check/util/cli/opt" 6 | "github.com/google/go-cmp/cmp" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestWriteTo(t *testing.T) { 12 | cases := map[string]struct { 13 | Options []opt.Option 14 | Expected string 15 | }{ 16 | "empty (boundary case)": { 17 | Options: []opt.Option{}, 18 | Expected: strings.Join([]string{ 19 | "| Option | Description | Required or Default Value |\n", 20 | "|:-------|:------------|:--------------------------|\n", 21 | }, ""), 22 | }, 23 | "optional string option (easiest case)": { 24 | Options: []opt.Option{ 25 | opt.NewOptionalStringOption("opt-str", "optional string option", "DEFAULT"), 26 | }, 27 | Expected: strings.Join([]string{ 28 | "| Option | Description | Required or Default Value |\n", 29 | "|:-------|:------------|:--------------------------|\n", 30 | "| `--opt-str ` | optional string option | optional (default: `\"DEFAULT\"`) |\n", 31 | }, ""), 32 | }, 33 | "required string option (easiest case)": { 34 | Options: []opt.Option{ 35 | opt.NewRequiredStringOption("req-str", "required string option"), 36 | }, 37 | Expected: strings.Join([]string{ 38 | "| Option | Description | Required or Default Value |\n", 39 | "|:-------|:------------|:--------------------------|\n", 40 | "| `--req-str ` | required string option | required |\n", 41 | }, ""), 42 | }, 43 | "optional bool option (easiest case)": { 44 | Options: []opt.Option{ 45 | opt.NewOptionalBoolOption("opt-bool", "optional bool option", true), 46 | }, 47 | Expected: strings.Join([]string{ 48 | "| Option | Description | Required or Default Value |\n", 49 | "|:-------|:------------|:--------------------------|\n", 50 | "| `--opt-bool` | optional bool option | optional (default: `true`) |\n", 51 | }, ""), 52 | }, 53 | "required bool option (easiest case)": { 54 | Options: []opt.Option{ 55 | opt.NewRequiredBoolOption("req-bool", "required bool option"), 56 | }, 57 | Expected: strings.Join([]string{ 58 | "| Option | Description | Required or Default Value |\n", 59 | "|:-------|:------------|:--------------------------|\n", 60 | "| `--req-bool` | required bool option | required |\n", 61 | }, ""), 62 | }, 63 | } 64 | 65 | for desc, c := range cases { 66 | t.Run(desc, func(t *testing.T) { 67 | buf := &bytes.Buffer{} 68 | 69 | _, err := WriteTo(buf, c.Options...) 70 | if err != nil { 71 | t.Errorf("want nil, got %#v", err) 72 | return 73 | } 74 | 75 | if buf.String() != c.Expected { 76 | t.Error(cmp.Diff(c.Expected, buf.String())) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /util/cli/processinout.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type ProcessInout struct { 9 | Stdin io.Reader 10 | Stdout io.WriteCloser 11 | Stderr io.WriteCloser 12 | } 13 | 14 | func GetProcessInout() ProcessInout { 15 | return ProcessInout{ 16 | Stdin: os.Stdin, 17 | Stdout: os.Stdout, 18 | Stderr: os.Stderr, 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /util/cli/processinout_stub.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/testutil" 5 | ) 6 | 7 | func AnyProcInout() ProcessInout { 8 | return ProcessInout{ 9 | Stdin: &testutil.ErrorReadCloserStub{}, 10 | Stdout: &testutil.NullWriteCloser{}, 11 | Stderr: &testutil.NullWriteCloser{}, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /util/cli/prof.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "runtime/pprof" 9 | ) 10 | 11 | func NewCommandWithCPUProfile(cmd Command) Command { 12 | profFile, err := newProfFile() 13 | if err != nil { 14 | panic(err.Error()) 15 | } 16 | 17 | return func(args []string, procInout ProcessInout, env Env) ExitStatus { 18 | if err := pprof.StartCPUProfile(profFile); err != nil { 19 | panic(err.Error()) 20 | } 21 | 22 | exitStatus := cmd(args, procInout, env) 23 | 24 | pprof.StopCPUProfile() 25 | _ = profFile.Close() 26 | return exitStatus 27 | } 28 | } 29 | 30 | func NewCommandWithHeapProfile(cmd Command) Command { 31 | profFile, err := newProfFile() 32 | if err != nil { 33 | panic(err.Error()) 34 | } 35 | 36 | return func(args []string, procInout ProcessInout, env Env) ExitStatus { 37 | exitStatus := cmd(args, procInout, env) 38 | 39 | if err := pprof.Lookup("heap").WriteTo(profFile, 0); err != nil { 40 | panic(err.Error()) 41 | } 42 | _ = profFile.Close() 43 | return exitStatus 44 | } 45 | } 46 | 47 | func newProfFile() (io.WriteCloser, error) { 48 | tmpDir, err := os.MkdirTemp(os.TempDir(), "") 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | profPath := filepath.Join(tmpDir, "unity-meta-check.prof") 54 | fmt.Printf("profile path: %s\n", profPath) 55 | 56 | profFile, err := os.OpenFile(profPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return profFile, nil 61 | } 62 | -------------------------------------------------------------------------------- /util/cstrset/cistrset.go: -------------------------------------------------------------------------------- 1 | package cstrset 2 | 3 | import ( 4 | "github.com/scylladb/go-set/strset" 5 | "strings" 6 | ) 7 | 8 | type CaseInsensitiveSet map[string]string 9 | 10 | var _ Set = &CaseInsensitiveSet{} 11 | 12 | func NewCaseInsensitive(items... string) *CaseInsensitiveSet { 13 | s := CaseInsensitiveSet(make(map[string]string, len(items))) 14 | for _, item := range items { 15 | s.Add(item) 16 | } 17 | return &s 18 | } 19 | 20 | func NewCaseInsensitiveWithSize(size int) *CaseInsensitiveSet { 21 | s := CaseInsensitiveSet(make(map[string]string, size)) 22 | return &s 23 | } 24 | 25 | func (s CaseInsensitiveSet) Has(e string) bool { 26 | _, ok := s[strings.ToLower(e)] 27 | return ok 28 | } 29 | 30 | func (s *CaseInsensitiveSet) Add(e string) { 31 | (*s)[strings.ToLower(e)] = e 32 | } 33 | 34 | func (s *CaseInsensitiveSet) Difference(other Set) *strset.Set { 35 | return difference(s, other.(*CaseInsensitiveSet)) 36 | } 37 | 38 | func (s CaseInsensitiveSet) Len() int { 39 | return len(s) 40 | } 41 | 42 | func difference(a, b *CaseInsensitiveSet) *strset.Set { 43 | values := strset.NewWithSize(len(*a)) 44 | strset.Difference(a.keys(), b.keys()).Each(func(item string) bool { 45 | values.Add((*a)[item]) 46 | return true 47 | }) 48 | return values 49 | } 50 | 51 | func (s *CaseInsensitiveSet) keys() *strset.Set { 52 | result := strset.NewWithSize(len(*s)) 53 | for key := range *s { 54 | result.Add(key) 55 | } 56 | return result 57 | } 58 | -------------------------------------------------------------------------------- /util/cstrset/csstrset.go: -------------------------------------------------------------------------------- 1 | package cstrset 2 | 3 | import "github.com/scylladb/go-set/strset" 4 | 5 | type CaseSensitiveSet struct { 6 | s *strset.Set 7 | } 8 | 9 | var _ Set = &CaseSensitiveSet{} 10 | 11 | func NewCaseSensitive(items... string) *CaseSensitiveSet { 12 | return &CaseSensitiveSet{strset.New(items...)} 13 | } 14 | 15 | func NewCaseSensitiveWithSize(size int) *CaseSensitiveSet { 16 | return &CaseSensitiveSet{strset.NewWithSize(size)} 17 | } 18 | 19 | func (s *CaseSensitiveSet) Has(e string) bool { 20 | return s.s.Has(e) 21 | } 22 | 23 | func (s *CaseSensitiveSet) Add(e string) { 24 | s.s.Add(e) 25 | } 26 | 27 | func (s *CaseSensitiveSet) Difference(other Set) *strset.Set { 28 | return strset.Difference(s.s, other.(*CaseSensitiveSet).s) 29 | } 30 | 31 | func (s CaseSensitiveSet) Len() int { 32 | return s.s.Size() 33 | } 34 | -------------------------------------------------------------------------------- /util/cstrset/cstrset.go: -------------------------------------------------------------------------------- 1 | package cstrset 2 | 3 | import "github.com/scylladb/go-set/strset" 4 | 5 | type Set interface { 6 | Add(string) 7 | Has(string) bool 8 | Difference(Set) *strset.Set 9 | Len() int 10 | } 11 | -------------------------------------------------------------------------------- /util/errutil/errors.go: -------------------------------------------------------------------------------- 1 | package errutil 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | type Errors []error 9 | 10 | func NewErrors(errs []error) error { 11 | if len(errs) == 0 { 12 | panic("empty errors must be not an error") 13 | } 14 | return Errors(errs) 15 | } 16 | 17 | var _ error = Errors{} 18 | 19 | func (s Errors) Error() string { 20 | buf := &bytes.Buffer{} 21 | for _, e := range s { 22 | _, _ = fmt.Fprintln(buf, e.Error()) 23 | } 24 | return buf.String() 25 | } 26 | -------------------------------------------------------------------------------- /util/globs/glob.go: -------------------------------------------------------------------------------- 1 | package globs 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/pathutil" 5 | "github.com/DeNA/unity-meta-check/util/typedpath" 6 | "path" 7 | ) 8 | 9 | type Glob string 10 | 11 | func (g Glob) ToSlash() typedpath.SlashPath { 12 | return typedpath.NewSlashPathUnsafe(string(g)) 13 | } 14 | 15 | func MatchAny(p typedpath.SlashPath, globs []Glob, cwd typedpath.SlashPath) (bool, Glob, error) { 16 | pAbs := joinCwdIfRel(cwd, p) 17 | ancestors := pathutil.AllAncestorsAndSelf(pAbs) 18 | for _, ancestor := range ancestors { 19 | for _, relGlob := range globs { 20 | glob := joinCwdIfRel(cwd, relGlob.ToSlash()) 21 | ok, err := path.Match(string(glob), string(ancestor)) 22 | if err != nil { 23 | return false, "", err 24 | } 25 | if ok { 26 | return true, relGlob, nil 27 | } 28 | } 29 | } 30 | return false, "", nil 31 | } 32 | 33 | func joinCwdIfRel(cwd typedpath.SlashPath, path typedpath.SlashPath) typedpath.SlashPath { 34 | if path.IsAbs() { 35 | return path 36 | } 37 | return cwd.JoinSlashPath(path) 38 | } -------------------------------------------------------------------------------- /util/globs/glob_test.go: -------------------------------------------------------------------------------- 1 | package globs 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "testing" 6 | ) 7 | 8 | func TestMatchAny(t *testing.T) { 9 | cases := map[string]struct { 10 | Path typedpath.SlashPath 11 | Globs []Glob 12 | Cwd typedpath.SlashPath 13 | Expected bool 14 | }{ 15 | "empty globs (boundary case)": { 16 | Path: "path/to/file", 17 | Cwd: "/cwd", 18 | Globs: []Glob{}, 19 | Expected: false, 20 | }, 21 | "easiest case": { 22 | Path: "path/to/file", 23 | Cwd: "/cwd", 24 | Globs: []Glob{ 25 | "path", 26 | }, 27 | Expected: true, 28 | }, 29 | "asterisk pattern": { 30 | Path: "path/to/file", 31 | Cwd: "/cwd", 32 | Globs: []Glob{ 33 | "path/*", 34 | }, 35 | Expected: true, 36 | }, 37 | "only dot (edge case)": { 38 | Path: "path/to/file", 39 | Cwd: "/cwd", 40 | Globs: []Glob{ 41 | ".", 42 | }, 43 | Expected: true, 44 | }, 45 | "only asterisk (edge case)": { 46 | Path: "path/to/file", 47 | Cwd: "/cwd", 48 | Globs: []Glob{ 49 | "*", 50 | }, 51 | Expected: true, 52 | }, 53 | "dot asterisk (edge case)": { 54 | Path: "path/to/file", 55 | Cwd: "/cwd", 56 | Globs: []Glob{ 57 | "./*", 58 | }, 59 | Expected: true, 60 | }, 61 | "empty glob (edge case)": { 62 | Path: "path/to/file", 63 | Cwd: "/cwd", 64 | Globs: []Glob{ 65 | "", 66 | }, 67 | Expected: true, 68 | }, 69 | "only asterisk not match absolute path because the glob based on relative": { 70 | Path: "/path/to/file", 71 | Cwd: "/cwd", 72 | Globs: []Glob{ 73 | "*", 74 | }, 75 | Expected: false, 76 | }, 77 | "empty glob not match absolute path because the glob based on relative": { 78 | Path: "/path/to/file", 79 | Cwd: "/cwd", 80 | Globs: []Glob{ 81 | "", 82 | }, 83 | Expected: false, 84 | }, 85 | "only dot not match absolute path": { 86 | Path: "/path/to/file", 87 | Cwd: "/cwd", 88 | Globs: []Glob{ 89 | ".", 90 | }, 91 | Expected: false, 92 | }, 93 | "slash asterisk match absolute path": { 94 | Path: "/path/to/file", 95 | Cwd: "/cwd", 96 | Globs: []Glob{ 97 | "/*", 98 | }, 99 | Expected: true, 100 | }, 101 | } 102 | 103 | for desc, c := range cases { 104 | t.Run(desc, func(t *testing.T) { 105 | actual, _, err := MatchAny(c.Path, c.Globs, c.Cwd) 106 | if err != nil { 107 | t.Errorf("want nil, got %#v", err) 108 | return 109 | } 110 | 111 | if actual != c.Expected { 112 | t.Errorf("want %t, got %t", c.Expected, actual) 113 | return 114 | } 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /util/logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | type Severity int 10 | 11 | const ( 12 | SeverityDebug Severity = iota 13 | SeverityInfo Severity = iota 14 | SeverityWarn Severity = iota 15 | SeverityError Severity = iota 16 | ) 17 | 18 | func (s Severity) String() string { 19 | switch s { 20 | case SeverityDebug: 21 | return "DEBUG" 22 | case SeverityInfo: 23 | return "INFO" 24 | case SeverityWarn: 25 | return "WARN" 26 | case SeverityError: 27 | return "ERROR" 28 | default: 29 | panic("unreachable") 30 | } 31 | } 32 | 33 | func ParseSeverity(s string) (Severity, error) { 34 | switch s { 35 | case "DEBUG": 36 | return SeverityDebug, nil 37 | case "INFO": 38 | return SeverityInfo, nil 39 | case "WARN": 40 | return SeverityWarn, nil 41 | case "ERROR": 42 | return SeverityError, nil 43 | default: 44 | return 0, fmt.Errorf("unknown severity: %q", s) 45 | } 46 | } 47 | 48 | // MustParseSeverity return the severity if given "DEBUG"/"INFO"/"WARN"/"ERROR". otherwise return "DEBUG" to fallback. 49 | func MustParseSeverity(s string) Severity { 50 | v, err := ParseSeverity(s) 51 | if err != nil { 52 | // NOTE: Fallback 53 | return SeverityDebug 54 | } 55 | return v 56 | } 57 | 58 | type Logger interface { 59 | Debug(string) 60 | Info(string) 61 | Warn(string) 62 | Error(string) 63 | Log(Severity, string) 64 | } 65 | 66 | func NewLogger(severity Severity, writer io.Writer) Logger { 67 | return &severityLogger{ 68 | severity: severity, 69 | writer: writer, 70 | } 71 | } 72 | 73 | type severityLogger struct { 74 | severity Severity 75 | writer io.Writer 76 | } 77 | 78 | func (logger *severityLogger) Debug(message string) { 79 | logger.Log(SeverityDebug, message) 80 | } 81 | 82 | func (logger *severityLogger) Info(message string) { 83 | logger.Log(SeverityInfo, message) 84 | } 85 | 86 | func (logger *severityLogger) Warn(message string) { 87 | logger.Log(SeverityWarn, message) 88 | } 89 | 90 | func (logger *severityLogger) Error(message string) { 91 | logger.Log(SeverityError, message) 92 | } 93 | 94 | func (logger *severityLogger) Log(severity Severity, message string) { 95 | if logger.severity <= severity { 96 | _, _ = fmt.Fprintf(logger.writer, "%s: %s\n", strings.ToLower(severity.String()), message) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /util/logging/logger_stub.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | func SpyLogger() *LoggerSpy { 9 | return &LoggerSpy{ 10 | Logs: &bytes.Buffer{}, 11 | } 12 | } 13 | 14 | type LoggerSpy struct { 15 | Logs *bytes.Buffer 16 | } 17 | 18 | var _ Logger = &LoggerSpy{} 19 | 20 | func (s *LoggerSpy) Debug(message string) { 21 | s.Log(SeverityDebug, message) 22 | } 23 | 24 | func (s *LoggerSpy) Info(message string) { 25 | s.Log(SeverityInfo, message) 26 | } 27 | 28 | func (s *LoggerSpy) Warn(message string) { 29 | s.Log(SeverityWarn, message) 30 | } 31 | 32 | func (s *LoggerSpy) Error(message string) { 33 | s.Log(SeverityError, message) 34 | } 35 | 36 | func (s *LoggerSpy) Log(severity Severity, message string) { 37 | s.Logs.WriteString(fmt.Sprintf("%s: %s\n", severity.String(), message)) 38 | } 39 | -------------------------------------------------------------------------------- /util/ostestable/getwd.go: -------------------------------------------------------------------------------- 1 | package ostestable 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | ) 6 | 7 | type Getwd func() (typedpath.RawPath, error) 8 | 9 | func NewGetwd() Getwd { 10 | return typedpath.Getwd 11 | } 12 | -------------------------------------------------------------------------------- /util/ostestable/getwd_stub.go: -------------------------------------------------------------------------------- 1 | package ostestable 2 | 3 | import "github.com/DeNA/unity-meta-check/util/typedpath" 4 | 5 | //goland:noinspection GoUnusedExportedFunction 6 | func StubGetwd(cwd typedpath.RawPath, err error) Getwd { 7 | return func() (typedpath.RawPath, error) { 8 | return cwd, err 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /util/ostestable/isdir.go: -------------------------------------------------------------------------------- 1 | package ostestable 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "os" 6 | ) 7 | 8 | type IsDir func(path typedpath.RawPath) (bool, error) 9 | 10 | func NewIsDir() IsDir { 11 | return func(path typedpath.RawPath) (bool, error) { 12 | stat, err := os.Stat(string(path)) 13 | if err != nil { 14 | return false, err 15 | } 16 | return stat.IsDir(), nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /util/ostestable/isdir_stub.go: -------------------------------------------------------------------------------- 1 | package ostestable 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | ) 6 | 7 | func StubIsDir(isDir bool, err error) IsDir { 8 | return func(typedpath.RawPath) (bool, error) { 9 | return isDir, err 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /util/pathutil/ancestors.go: -------------------------------------------------------------------------------- 1 | package pathutil 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | func AllAncestorsAndSelf(targetPath typedpath.SlashPath) []typedpath.SlashPath { 10 | result := make([]typedpath.SlashPath, 0) 11 | 12 | current := path.Clean(strings.TrimRight(string(targetPath), "/")) 13 | for current != "" && current != "." { 14 | result = append(result, typedpath.SlashPath(current)) 15 | if current == "/" { 16 | break 17 | } 18 | current = path.Dir(current) 19 | } 20 | 21 | return result 22 | } 23 | -------------------------------------------------------------------------------- /util/pathutil/ancestors_test.go: -------------------------------------------------------------------------------- 1 | package pathutil 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "github.com/google/go-cmp/cmp" 6 | "reflect" 7 | "sort" 8 | "testing" 9 | ) 10 | 11 | func TestAllAncestorsAndSelf(t *testing.T) { 12 | cases := map[string]struct{ 13 | Path typedpath.SlashPath 14 | Expected []typedpath.SlashPath 15 | } { 16 | "empty (boundary case)": { 17 | Path: "", 18 | Expected: []typedpath.SlashPath{}, 19 | }, 20 | "only dot (boundary case)": { 21 | Path: ".", 22 | Expected: []typedpath.SlashPath{ 23 | }, 24 | }, 25 | "single path (easiest case)": { 26 | Path: "path", 27 | Expected: []typedpath.SlashPath{ 28 | "path", 29 | }, 30 | }, 31 | "trailing slash (edge case)": { 32 | Path: "path/", 33 | Expected: []typedpath.SlashPath{ 34 | "path", 35 | }, 36 | }, 37 | "relative path (normal case)": { 38 | Path: "path/to/file", 39 | Expected: []typedpath.SlashPath{ 40 | "path", 41 | "path/to", 42 | "path/to/file", 43 | }, 44 | }, 45 | "dot-prefix path (edge case)": { 46 | Path: "./path/to/file", 47 | Expected: []typedpath.SlashPath{ 48 | "path", 49 | "path/to", 50 | "path/to/file", 51 | }, 52 | }, 53 | "dot-dot-prefix path (edge case)": { 54 | Path: "../path/to/file", 55 | Expected: []typedpath.SlashPath{ 56 | "..", 57 | "../path", 58 | "../path/to", 59 | "../path/to/file", 60 | }, 61 | }, 62 | "absolute path (edge case)": { 63 | Path: "/path/to/file", 64 | Expected: []typedpath.SlashPath{ 65 | "/", 66 | "/path", 67 | "/path/to", 68 | "/path/to/file", 69 | }, 70 | }, 71 | } 72 | 73 | for desc, c := range cases { 74 | t.Run(desc, func(t *testing.T) { 75 | actual := AllAncestorsAndSelf(c.Path) 76 | 77 | sort.Slice(actual, func(i, j int) bool { 78 | return actual[i] < actual[j] 79 | }) 80 | 81 | if !reflect.DeepEqual(actual, c.Expected) { 82 | t.Error(cmp.Diff(c.Expected, actual)) 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /util/pathutil/set.go: -------------------------------------------------------------------------------- 1 | package pathutil 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/cstrset" 5 | "github.com/DeNA/unity-meta-check/util/typedpath" 6 | ) 7 | 8 | type PathSet struct { 9 | set cstrset.Set 10 | } 11 | 12 | func NewPathSet(ignoreCase bool, ss ...typedpath.SlashPath) *PathSet { 13 | items := make([]string, len(ss)) 14 | for i, s := range ss { 15 | items[i] = string(s) 16 | } 17 | if ignoreCase { 18 | return &PathSet{cstrset.NewCaseInsensitive(items...)} 19 | } 20 | return &PathSet{cstrset.NewCaseSensitive(items...)} 21 | } 22 | 23 | func NewPathSetWithSize(ignoreCase bool, size int) *PathSet { 24 | if ignoreCase { 25 | return &PathSet{cstrset.NewCaseInsensitiveWithSize(size)} 26 | } 27 | return &PathSet{cstrset.NewCaseSensitiveWithSize(size)} 28 | } 29 | 30 | func (s *PathSet) Add(path typedpath.SlashPath) { 31 | s.set.Add(string(path)) 32 | } 33 | 34 | func (s *PathSet) Has(path typedpath.SlashPath) bool { 35 | return s.set.Has(string(path)) 36 | } 37 | 38 | func (s *PathSet) Difference(other *PathSet) []typedpath.SlashPath { 39 | diff := s.set.Difference(other.set) 40 | result := make([]typedpath.SlashPath, diff.Size()) 41 | i := 0 42 | diff.Each(func(item string) bool { 43 | result[i] = typedpath.SlashPath(item) 44 | i++ 45 | return true 46 | }) 47 | return result 48 | } 49 | 50 | func (s *PathSet) Len() int { 51 | return s.set.Len() 52 | } 53 | -------------------------------------------------------------------------------- /util/pathutil/split.go: -------------------------------------------------------------------------------- 1 | package pathutil 2 | 3 | import ( 4 | "github.com/DeNA/unity-meta-check/util/typedpath" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | func SplitPathElements(targetPath typedpath.SlashPath) []typedpath.BaseName { 10 | result := make([]typedpath.BaseName, 0) 11 | dir := targetPath 12 | for dir != "" { 13 | newDir, file := path.Split(strings.TrimRight(string(dir), "/")) 14 | result = append(result, typedpath.BaseName(file)) 15 | dir = typedpath.SlashPath(newDir) 16 | } 17 | reverse(result) 18 | return result 19 | } 20 | 21 | func reverse(baseNames []typedpath.BaseName) { 22 | for i := 0; i < len(baseNames)/2; i++ { 23 | j := len(baseNames) - i - 1 24 | baseNames[i], baseNames[j] = baseNames[j], baseNames[i] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /util/pathutil/split_test.go: -------------------------------------------------------------------------------- 1 | package pathutil 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DeNA/unity-meta-check/util/typedpath" 6 | "github.com/google/go-cmp/cmp" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestSplitPathComponents(t *testing.T) { 12 | cases := []struct{ 13 | path typedpath.SlashPath 14 | expected []typedpath.BaseName 15 | }{ 16 | { "", []typedpath.BaseName{} }, 17 | { "Foo", []typedpath.BaseName{"Foo"} }, 18 | { "Foo/", []typedpath.BaseName{"Foo"} }, 19 | { "Foo/Bar", []typedpath.BaseName{"Foo", "Bar"} }, 20 | { "Foo/Bar/Baz", []typedpath.BaseName{"Foo", "Bar", "Baz"} }, 21 | } 22 | 23 | for _, c := range cases { 24 | t.Run(fmt.Sprintf("%q", c.path), func(t *testing.T) { 25 | actual := SplitPathElements(c.path) 26 | 27 | if !reflect.DeepEqual(actual, c.expected) { 28 | t.Errorf(cmp.Diff(c.expected, actual)) 29 | return 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /util/prefix/writer.go: -------------------------------------------------------------------------------- 1 | package prefix 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | type Writer struct { 9 | prefix string 10 | base io.Writer 11 | buf *bytes.Buffer 12 | firstDone bool 13 | } 14 | 15 | func NewWriter(prefix string, base io.Writer) io.Writer { 16 | return &Writer{prefix, base, &bytes.Buffer{}, false} 17 | } 18 | 19 | func (w *Writer) Write(bs []byte) (int, error) { 20 | w.buf.Reset() 21 | 22 | if !w.firstDone { 23 | w.buf.WriteString(w.prefix) 24 | w.firstDone = true 25 | } 26 | 27 | for _, b := range bs { 28 | if b == '\n' { 29 | _ = w.buf.WriteByte(b) 30 | w.buf.WriteString(w.prefix) 31 | } else { 32 | w.buf.WriteByte(b) 33 | } 34 | } 35 | 36 | n, err := w.buf.WriteTo(w.base) 37 | return int(n), err 38 | } 39 | -------------------------------------------------------------------------------- /util/prefix/writer_test.go: -------------------------------------------------------------------------------- 1 | package prefix 2 | 3 | import ( 4 | "bytes" 5 | "github.com/google/go-cmp/cmp" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | func TestWriterEmpty(t *testing.T) { 11 | buf := &bytes.Buffer{} 12 | 13 | writer := NewWriter("PREFIX:", buf) 14 | 15 | if _, err := io.WriteString(writer, ""); err != nil { 16 | t.Errorf("want nil, got %#v", err) 17 | return 18 | } 19 | 20 | actual := buf.String() 21 | expected := "PREFIX:" 22 | if actual != expected { 23 | t.Error(cmp.Diff(expected, actual)) 24 | return 25 | } 26 | } 27 | 28 | func TestWriterFirstLine(t *testing.T) { 29 | buf := &bytes.Buffer{} 30 | 31 | writer := NewWriter("PREFIX:", buf) 32 | 33 | if _, err := io.WriteString(writer, "LINE1"); err != nil { 34 | t.Errorf("want nil, got %#v", err) 35 | return 36 | } 37 | 38 | actual := buf.String() 39 | expected := "PREFIX:LINE1" 40 | if actual != expected { 41 | t.Error(cmp.Diff(expected, actual)) 42 | return 43 | } 44 | } 45 | 46 | func TestWriterFirstLineEnd(t *testing.T) { 47 | buf := &bytes.Buffer{} 48 | 49 | writer := NewWriter("PREFIX:", buf) 50 | 51 | if _, err := io.WriteString(writer, "LINE1\n"); err != nil { 52 | t.Errorf("want nil, got %#v", err) 53 | return 54 | } 55 | 56 | actual := buf.String() 57 | expected := "PREFIX:LINE1\nPREFIX:" 58 | if actual != expected { 59 | t.Error(cmp.Diff(expected, actual)) 60 | return 61 | } 62 | } 63 | 64 | func TestWriterFirstLine2(t *testing.T) { 65 | buf := &bytes.Buffer{} 66 | 67 | writer := NewWriter("PREFIX:", buf) 68 | 69 | if _, err := io.WriteString(writer, "LINE1\nLINE2"); err != nil { 70 | t.Errorf("want nil, got %#v", err) 71 | return 72 | } 73 | 74 | actual := buf.String() 75 | expected := "PREFIX:LINE1\nPREFIX:LINE2" 76 | if actual != expected { 77 | t.Error(cmp.Diff(expected, actual)) 78 | return 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /util/testutil/env.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | type TestEnv struct { 10 | ApiEndpoint string 11 | Owner string 12 | Repo string 13 | Pull string 14 | Token string 15 | } 16 | 17 | type TestEnvError []string 18 | 19 | func (t TestEnvError) Error() string { 20 | return strings.Join(t, "\n") 21 | } 22 | 23 | func GetTestEnv() (*TestEnv, error) { 24 | err := TestEnvError{} 25 | 26 | apiEndpoint := os.Getenv("UNITY_META_CHECK_GITHUB_API_ENDPOINT") 27 | if apiEndpoint == "" { 28 | err = append(err, "missing UNITY_META_CHECK_GITHUB_API_ENDPOINT") 29 | } 30 | 31 | owner := os.Getenv("UNITY_META_CHECK_GITHUB_OWNER") 32 | if owner == "" { 33 | err = append(err, "missing UNITY_META_CHECK_GITHUB_OWNER") 34 | } 35 | 36 | repo := os.Getenv("UNITY_META_CHECK_GITHUB_REPO") 37 | if repo == "" { 38 | err = append(err, "missing UNITY_META_CHECK_GITHUB_REPO") 39 | } 40 | 41 | pull := os.Getenv("UNITY_META_CHECK_GITHUB_PULL_NUMBER") 42 | if pull == "" { 43 | err = append(err, "missing UNITY_META_CHECK_GITHUB_PULL_NUMBER") 44 | } 45 | 46 | token := os.Getenv("UNITY_META_CHECK_GITHUB_TOKEN") 47 | if token == "" { 48 | return nil, errors.New("missing UNITY_META_CHECK_GITHUB_TOKEN") 49 | } 50 | 51 | return &TestEnv{ 52 | apiEndpoint, 53 | owner, 54 | repo, 55 | pull, 56 | token, 57 | }, nil 58 | } 59 | -------------------------------------------------------------------------------- /util/testutil/readcloser_stub.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type ErrorReadCloserStub struct{} 8 | 9 | func (*ErrorReadCloserStub) Close() error { 10 | return errors.New("close: EXPECTED_FAILURE") 11 | } 12 | 13 | func (*ErrorReadCloserStub) Read(_ []byte) (n int, err error) { 14 | return 0, errors.New("read: EXPECTED_FAILURE") 15 | } 16 | -------------------------------------------------------------------------------- /util/testutil/writecloser_spy.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | func SpyWriteCloser(base io.WriteCloser) *WriteCloserSpy { 9 | var inherited io.WriteCloser 10 | if base != nil { 11 | inherited = base 12 | } else { 13 | inherited = &NullWriteCloser{} 14 | } 15 | 16 | captured := &bytes.Buffer{} 17 | 18 | return &WriteCloserSpy{ 19 | Inherited: inherited, 20 | Captured: captured, 21 | IsClosed: false, 22 | } 23 | } 24 | 25 | type WriteCloserSpy struct { 26 | Inherited io.WriteCloser 27 | Captured *bytes.Buffer 28 | IsClosed bool 29 | } 30 | 31 | func (s *WriteCloserSpy) Write(p []byte) (n int, err error) { 32 | s.Captured.Write(p) 33 | return s.Inherited.Write(p) 34 | } 35 | 36 | func (s *WriteCloserSpy) Close() error { 37 | s.IsClosed = true 38 | return s.Inherited.Close() 39 | } 40 | -------------------------------------------------------------------------------- /util/testutil/writecloser_stub.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type WriteCloserStub struct { 8 | WriteErr error 9 | CloseErr error 10 | } 11 | 12 | func (w WriteCloserStub) Write(p []byte) (n int, err error) { 13 | if w.WriteErr == nil { 14 | return len(p), nil 15 | } 16 | return 0, w.WriteErr 17 | } 18 | 19 | func (w WriteCloserStub) Close() error { 20 | return w.CloseErr 21 | } 22 | 23 | //goland:noinspection GoUnusedExportedFunction 24 | func StubWriteCloser(writeErr, closeErr error) io.WriteCloser { 25 | return &WriteCloserStub{ 26 | WriteErr: writeErr, 27 | CloseErr: closeErr, 28 | } 29 | } 30 | 31 | type NullWriteCloser struct{} 32 | 33 | func (*NullWriteCloser) Write(p []byte) (n int, err error) { 34 | return len(p), nil 35 | } 36 | 37 | func (*NullWriteCloser) Close() error { 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /util/typedpath/basename.go: -------------------------------------------------------------------------------- 1 | package typedpath 2 | 3 | type BaseName string 4 | -------------------------------------------------------------------------------- /util/typedpath/raw.go: -------------------------------------------------------------------------------- 1 | package typedpath 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // RawPath is OS depended path. This style is for directly handling file systems. 10 | type RawPath string 11 | 12 | func NewRawPath(basenames ...BaseName) RawPath { 13 | elements := make([]string, len(basenames)) 14 | for i, basename := range basenames { 15 | elements[i] = string(basename) 16 | } 17 | return RawPath(filepath.Join(elements...)) 18 | } 19 | 20 | func NewRootRawPath(basenames ...BaseName) RawPath { 21 | return NewRawPath(append([]BaseName{BaseName([]byte{os.PathSeparator})}, basenames...)...) 22 | } 23 | 24 | func NewRawPathUnsafe(path string) RawPath { 25 | return RawPath(path) 26 | } 27 | 28 | func Getwd() (RawPath, error) { 29 | result, err := os.Getwd() 30 | if err != nil { 31 | return "", err 32 | } 33 | return RawPath(result), err 34 | } 35 | 36 | func (r RawPath) ToSlash() SlashPath { 37 | return SlashPath(filepath.ToSlash(string(r))) 38 | } 39 | 40 | func (r RawPath) JoinRawPath(other RawPath) RawPath { 41 | return RawPath(filepath.Join(string(r), string(other))) 42 | } 43 | 44 | func (r RawPath) JoinBaseName(others ...BaseName) RawPath { 45 | return r.JoinRawPath(NewRawPath(others...)) 46 | } 47 | 48 | func (r RawPath) IsAbs() bool { 49 | return filepath.IsAbs(string(r)) 50 | } 51 | 52 | func (r RawPath) Rel(path RawPath) (RawPath, error) { 53 | result, err := filepath.Rel(string(r), string(path)) 54 | if err != nil { 55 | return "", err 56 | } 57 | return RawPath(result), nil 58 | } 59 | 60 | func (r RawPath) Dir() RawPath { 61 | return RawPath(filepath.Dir(string(r))) 62 | } 63 | 64 | func (r RawPath) Ext() string { 65 | return filepath.Ext(string(r)) 66 | } 67 | 68 | func (r RawPath) Base() string { 69 | return filepath.Base(string(r)) 70 | } 71 | 72 | func (r RawPath) TrimLastSep() RawPath { 73 | return RawPath(strings.TrimRight(string(r), string(filepath.Separator))) 74 | } 75 | -------------------------------------------------------------------------------- /util/typedpath/raw_test.go: -------------------------------------------------------------------------------- 1 | package typedpath 2 | 3 | import "testing" 4 | 5 | func TestNewRawPath(t *testing.T) { 6 | cases := map[string]struct { 7 | BaseNames []BaseName 8 | Expected RawPath 9 | }{ 10 | "empty": { 11 | BaseNames: nil, 12 | Expected: NewSlashPathUnsafe("/").ToRaw(), 13 | }, 14 | "several paths": { 15 | BaseNames: []BaseName{"path", "to", "file"}, 16 | Expected: NewSlashPathUnsafe("/path/to/file").ToRaw(), 17 | }, 18 | } 19 | 20 | for desc, c := range cases { 21 | t.Run(desc, func(t *testing.T) { 22 | actual := NewRootRawPath(c.BaseNames...) 23 | 24 | if actual != c.Expected { 25 | t.Errorf("want %q, got %q", c.Expected, actual) 26 | } 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /util/typedpath/slash.go: -------------------------------------------------------------------------------- 1 | package typedpath 2 | 3 | import ( 4 | "path" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // SlashPath is slash separated path. Typically, Git uses the Slash Path. 10 | type SlashPath string 11 | 12 | func NewSlashPathUnsafe(path string) SlashPath { 13 | return SlashPath(path) 14 | } 15 | 16 | func SlashPathFromBaseName(baseName BaseName) SlashPath { 17 | return SlashPath(baseName) 18 | } 19 | 20 | func (s SlashPath) ToRaw() RawPath { 21 | return RawPath(filepath.FromSlash(string(s))) 22 | } 23 | 24 | func (s SlashPath) JoinSlashPath(other SlashPath) SlashPath { 25 | return SlashPath(path.Join(string(s), string(other))) 26 | } 27 | 28 | func (s SlashPath) JoinBaseName(other BaseName) SlashPath { 29 | return SlashPath(path.Join(string(s), string(other))) 30 | } 31 | 32 | func (s SlashPath) IsAbs() bool { 33 | return strings.HasPrefix(string(s), "/") 34 | } 35 | 36 | func (s SlashPath) Dir() SlashPath { 37 | return SlashPath(path.Dir(string(s))) 38 | } 39 | 40 | func (s SlashPath) Ext() string { 41 | return path.Ext(string(s)) 42 | } 43 | 44 | func (s SlashPath) Split() (SlashPath, BaseName) { 45 | dirname, basename := path.Split(string(s)) 46 | return SlashPath(dirname), BaseName(basename) 47 | } 48 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // NOTE: Also edit the image tag in action.yml 4 | const Version = "4.0.0-alpha3" 5 | --------------------------------------------------------------------------------