From b56f98ee850e1c754958f4603668209bcef53a2e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 06:00:35 +0000 Subject: [PATCH 01/15] Reorganize Java sources into java/ and scaffold .NET 10 port - Move original Java/Bazel implementation into java/ (reference only) - Add CLAUDE.md and TODO.md describing the port plan and work breakdown - Scaffold .NET 10 solution: Copybara.Common, Starlark, Copybara.Core, Copybara.Cli (packaged as 'copybara' .NET tool), Copybara.Tests - Add Preconditions and ImmutableListMultimap foundation helpers Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- .gitignore | 8 + CLAUDE.md | 136 ++++++++++++++++ Copybara.slnx | 11 ++ Directory.Build.props | 14 ++ TODO.md | 153 ++++++++++++++++++ global.json | 6 + .bazelproject => java/.bazelproject | 0 .bazelrc => java/.bazelrc | 0 {.docker => java/.docker}/copybara | 0 .dockerignore => java/.dockerignore | 0 BUILD => java/BUILD | 0 Dockerfile => java/Dockerfile | 0 MODULE.bazel => java/MODULE.bazel | 0 ci.Dockerfile => java/ci.Dockerfile | 0 {ci => java/ci}/ubuntu/continuous.cfg | 0 {ci => java/ci}/ubuntu/continuous.sh | 0 cloudbuild.sh => java/cloudbuild.sh | 0 cloudbuild.yaml => java/cloudbuild.yaml | 0 .../cloudbuild_setup.sh | 0 {copybara => java/copybara}/integration/BUILD | 0 .../integration/reference_doc_test.sh | 0 .../copybara}/integration/test-help.t | 0 .../copybara}/integration/tool_test.sh | 0 {docs => java/docs}/BUILD | 0 {docs => java/docs}/examples.md | 0 {docs => java/docs}/reference.md | 0 {external => java/external}/third_party/BUILD | 0 .../external}/third_party/jcommander.BUILD | 0 .../google/copybara/ActionMigrationTest.java | 0 .../javatests}/com/google/copybara/BUILD | 0 .../BaselinesWithoutLabelVisitorTest.java | 0 .../google/copybara/ChangeMessageTest.java | 0 .../google/copybara/ChangesResponseTest.java | 0 .../com/google/copybara/CheckoutPathTest.java | 0 .../com/google/copybara/CommandEnvTest.java | 0 .../google/copybara/CommandRunnerTest.java | 0 .../com/google/copybara/CoreReverseTest.java | 0 .../google/copybara/CoreTransformTest.java | 0 .../com/google/copybara/ExamplesTest.java | 0 .../com/google/copybara/InfoTest.java | 0 .../com/google/copybara/LabelFinderTest.java | 0 .../google/copybara/MainArgumentsTest.java | 0 .../com/google/copybara/MainTest.java | 0 .../com/google/copybara/MigrateCmdTest.java | 0 .../ReadConfigFromChangeWorkflowTest.java | 0 .../com/google/copybara/ReferenceTest.java | 0 .../copybara/StarlarkDateTimeModuleTest.java | 0 .../copybara/StarlarkJsonModuleTest.java | 0 .../copybara/StarlarkRandomModuleTest.java | 0 .../com/google/copybara/StructModuleTest.java | 0 .../google/copybara/TransformWorkTest.java | 0 .../com/google/copybara/WorkflowTest.java | 0 .../copybara/archive/ArchiveModuleTest.java | 0 .../com/google/copybara/archive/BUILD | 0 .../archive/util/ArchiveUtilTest.java | 0 .../com/google/copybara/archive/util/BUILD | 0 .../copybara/authoring/AuthorParserTest.java | 0 .../google/copybara/authoring/AuthorTest.java | 0 .../copybara/authoring/AuthoringTest.java | 0 .../com/google/copybara/buildozer/BUILD | 0 .../buildozer/BuildozerBatchTest.java | 0 .../buildozer/BuildozerCreateTest.java | 0 .../buildozer/BuildozerDeleteTest.java | 0 .../buildozer/BuildozerModifyTest.java | 0 .../buildozer/BuildozerPrintExecutorTest.java | 0 .../copybara/checks/ApiCheckerTest.java | 0 .../com/google/copybara/checks/BUILD | 0 .../com/google/copybara/config/BUILD | 0 .../config/CapturingConfigFileTest.java | 0 .../copybara/config/MapConfigFileTest.java | 0 .../config/PathBasedConfigFileTest.java | 0 .../config/ResolveDelegateConfigFileTest.java | 0 .../copybara/config/SkylarkParserTest.java | 0 .../copybara/config/SkylarkUtilTest.java | 0 .../copybara/config/ValidationResultTest.java | 0 .../com/google/copybara/config/base/BUILD | 0 .../copybara/config/base/SkylarkUtilTest.java | 0 .../com/google/copybara/configgen/BUILD | 0 .../configgen/ConfigGenHeuristicsTest.java | 0 .../com/google/copybara/credentials/BUILD | 0 .../credentials/CredentialModuleTest.java | 0 .../copybara/credentials/TtlSecretTest.java | 0 .../com/google/copybara/exception/BUILD | 0 .../exception/ValidationExceptionTest.java | 0 .../folder/FolderDestinationReaderTest.java | 0 .../folder/FolderDestinationTest.java | 0 .../copybara/folder/FolderOriginTest.java | 0 .../com/google/copybara/format/BUILD | 0 .../copybara/format/BuildifierFormatTest.java | 0 .../javatests}/com/google/copybara/git/BUILD | 0 .../git/CredentialFileHandlerTest.java | 0 .../git/FuzzyClosestVersionSelectorTest.java | 0 .../copybara/git/GerritDestinationTest.java | 0 .../copybara/git/GerritEndpointTest.java | 0 .../copybara/git/GerritOptionsTest.java | 0 .../google/copybara/git/GerritOriginTest.java | 0 .../copybara/git/GerritTriggerTest.java | 0 .../copybara/git/GitCredentialTest.java | 0 .../git/GitDestinationIntegrateTest.java | 0 .../git/GitDestinationReaderTest.java | 0 .../copybara/git/GitDestinationTest.java | 0 .../copybara/git/GitEnvironmentTest.java | 0 .../copybara/git/GitHubDestinationTest.java | 0 .../copybara/git/GitHubEndpointTest.java | 0 .../copybara/git/GitHubOptionsTest.java | 0 ...GitHubPostSubmitApprovalsProviderTest.java | 0 .../copybara/git/GitHubPrDestinationTest.java | 0 .../git/GitHubPrIntegrateLabelTest.java | 0 .../copybara/git/GitHubPrOriginTest.java | 0 .../GitHubPreSubmitApprovalsProviderTest.java | 0 .../git/GitHubRepositoryHookTest.java | 0 .../GitHubSecuritySettingsValidatorTest.java | 0 .../copybara/git/GitHubTriggerTest.java | 0 .../git/GitHubUserApprovalsValidatorTest.java | 0 .../copybara/git/GitLabMrDestinationTest.java | 0 .../copybara/git/GitLabMrOriginTest.java | 0 .../copybara/git/GitLabMrWriteHookTest.java | 0 .../copybara/git/GitLabMrWriterTest.java | 0 .../google/copybara/git/GitMirrorTest.java | 0 .../copybara/git/GitOriginSubmodulesTest.java | 0 .../google/copybara/git/GitOriginTest.java | 0 .../google/copybara/git/GitRepoTypeTest.java | 0 .../copybara/git/GitRepositoryTest.java | 0 .../google/copybara/git/GitRevisionTest.java | 0 .../git/LatestVersionSelectorForGitTest.java | 0 .../com/google/copybara/git/RefspecTest.java | 0 .../google/copybara/git/SameGitTreeTest.java | 0 .../git/SubmodulesInDestinationTest.java | 0 .../copybara/git/gerritapi/GerritApiTest.java | 0 .../GerritApiTransportWithCheckerTest.java | 0 .../git/github/api/GitHubApiTest.java | 0 .../api/GitHubApiTransportImplTest.java | 0 .../GitHubApiTransportWithCheckerTest.java | 0 .../git/github/api/GitHubGraphQLTest.java | 0 .../com/google/copybara/git/github/util/BUILD | 0 .../git/github/util/GitHubHostTest.java | 0 .../git/github/util/GitHubIdentifierTest.java | 0 .../git/github/util/GitHubUtilTest.java | 0 .../com/google/copybara/git/gitlab/BUILD | 0 .../git/gitlab/GitLabOptionsTest.java | 0 .../copybara/git/gitlab/GitLabUtilTest.java | 0 .../com/google/copybara/git/gitlab/api/BUILD | 0 .../git/gitlab/api/GitLabApiTest.java | 0 .../api/GitLabApiTransportImplTest.java | 0 .../copybara/git/gitlab/api/entities/BUILD | 0 .../CreateMergeRequestParamsTest.java | 0 .../api/entities/ExternalStatusCheckTest.java | 0 .../api/entities/GitLabApiParamsTest.java | 0 .../ListProjectMergeRequestParamsTest.java | 0 .../api/entities/ListUsersParamsTest.java | 0 .../gitlab/api/entities/MergeRequestTest.java | 0 .../api/entities/PaginatedPageListTest.java | 0 .../git/gitlab/api/entities/ProjectTest.java | 0 .../SetExternalStatusCheckParamsTest.java | 0 .../SetExternalStatusCheckResponseTest.java | 0 .../UpdateMergeRequestParamsTest.java | 0 .../javatests}/com/google/copybara/go/BUILD | 0 .../copybara/go/GoProxyVersioningTest.java | 0 .../go/PseudoVersionSelectorTest.java | 0 .../com/google/copybara/hashing/BUILD | 0 .../copybara/hashing/HashingModuleTest.java | 0 .../javatests}/com/google/copybara/hg/BUILD | 0 .../google/copybara/hg/HgDestinationTest.java | 0 .../com/google/copybara/hg/HgOriginTest.java | 0 .../google/copybara/hg/HgRepositoryTest.java | 0 .../javatests}/com/google/copybara/html/BUILD | 0 .../com/google/copybara/html/HtmlTest.java | 0 .../javatests}/com/google/copybara/http/BUILD | 0 .../google/copybara/http/HttpModuleTest.java | 0 .../google/copybara/http/auth/AuthTest.java | 0 .../com/google/copybara/http/auth/BUILD | 0 .../com/google/copybara/http/endpoint/BUILD | 0 .../http/endpoint/HttpEndpointTest.java | 0 .../com/google/copybara/http/json/BUILD | 0 .../json/HttpEndpointJsonContentTest.java | 0 .../com/google/copybara/http/multipart/BUILD | 0 .../HttpEndpointMultipartContentTest.java | 0 ...HttpEndpointUrlEncodedFormContentTest.java | 0 .../com/google/copybara/jcommander/BUILD | 0 .../jcommander/DurationConverterTest.java | 0 .../jcommander/GlobConverterTest.java | 0 .../SemicolonListConverterTest.java | 0 .../javatests}/com/google/copybara/json/BUILD | 0 .../copybara/json/GsonParserUtilTest.java | 0 .../com/google/copybara/monitor/BUILD | 0 .../monitor/ConsoleEventMonitorTest.java | 0 .../com/google/copybara/onboard/BUILD | 0 .../onboard/CommandLineGuideTest.java | 0 .../copybara/onboard/ConfigBuilderTest.java | 0 .../ConfigHeuristicsInputProviderTest.java | 0 .../copybara/onboard/GeneratorCmdTest.java | 0 .../onboard/GitToGitGeneratorTest.java | 0 .../google/copybara/onboard/InputsTest.java | 0 .../copybara/onboard/OnboardCmdTest.java | 0 .../onboard/StarlarkConverterTest.java | 0 .../onboard/core/AskInputProviderTest.java | 0 .../com/google/copybara/onboard/core/BUILD | 0 .../onboard/core/CachedInputProviderTest.java | 0 .../onboard/core/ConstantProviderTest.java | 0 .../core/InputProviderResolverImplTest.java | 0 .../copybara/onboard/core/InputTest.java | 0 .../core/MapBasedInputProviderTest.java | 0 .../core/PrioritizedInputProviderTest.java | 0 .../template/TemplateConfigGeneratorTest.java | 0 .../com/google/copybara/profiler/BUILD | 0 .../profiler/ConsoleProfilerListenerTest.java | 0 .../copybara/profiler/ProfilerTest.java | 0 .../com/google/copybara/python/BUILD | 0 .../copybara/python/PackageMetadataTest.java | 0 .../copybara/python/testfiles/TESTMETADATA | 0 .../javatests}/com/google/copybara/re2/BUILD | 0 .../com/google/copybara/re2/Re2Test.java | 0 .../com/google/copybara/regenerate/BUILD | 0 .../regenerate/RegenerateCmdTest.java | 0 .../com/google/copybara/remotefile/BUILD | 0 .../remotefile/GithubArchiveTest.java | 0 .../remotefile/RemoteArchiveOriginTest.java | 0 .../remotefile/RemoteFileModuleTest.java | 0 .../copybara/remotefile/extractutil/BUILD | 0 .../extractutil/ExtractUtilTest.java | 0 .../javatests}/com/google/copybara/rust/BUILD | 0 .../ComparisonRustVersionRequirementTest.java | 0 .../DefaultRustVersionRequirementTest.java | 0 .../rust/EpochRustVersionRequirementTest.java | 0 .../MultipleRustVersionRequirementTest.java | 0 .../rust/RustCratesIoVersionListTest.java | 0 .../rust/RustCratesIoVersionResolverTest.java | 0 .../rust/RustCratesIoVersionSelectorTest.java | 0 .../google/copybara/rust/RustModuleTest.java | 0 .../rust/RustVersionRequirementTest.java | 0 .../rust/TildeRustVersionRequirementTest.java | 0 .../WildcardRustVersionRequirementTest.java | 0 .../com/google/copybara/templatetoken/BUILD | 0 .../templatetoken/LabelTemplateTest.java | 0 .../javatests}/com/google/copybara/test.bzl | 0 .../copybara/testing/DummyCheckerTest.java | 0 .../copybara/testing/DummyOriginTest.java | 0 .../testing/SkylarkTestExecutorTest.java | 0 .../javatests}/com/google/copybara/toml/BUILD | 0 .../com/google/copybara/toml/TomlTest.java | 0 .../com/google/copybara/transform/BUILD | 0 .../transform/ConvertEncodingTest.java | 0 .../copybara/transform/CopyOrMoveTest.java | 0 .../google/copybara/transform/CoreTest.java | 0 .../transform/ExplicitReversalTest.java | 0 .../copybara/transform/FilterReplaceTest.java | 0 .../google/copybara/transform/RemoveTest.java | 0 .../google/copybara/transform/RenameTest.java | 0 .../copybara/transform/ReplaceTest.java | 0 .../copybara/transform/SequenceTest.java | 0 .../transform/SkylarkConsoleTest.java | 0 .../transform/SkylarkTransformationTest.java | 0 .../copybara/transform/TodoReplaceTest.java | 0 .../copybara/transform/VerifyMatchTest.java | 0 .../com/google/copybara/transform/debug/BUILD | 0 .../transform/debug/TransformDebugTest.java | 0 .../metadata/MetadataModuleTest.java | 0 .../metadata/RevisionMigratorTest.java | 0 .../com/google/copybara/transform/patch/BUILD | 0 .../patch/PatchTransformationTest.java | 0 .../transform/patch/PatchingOptionsTest.java | 0 .../patch/QuiltTransformationTest.java | 0 .../copybara/treestate/TreeStateTest.java | 0 .../com/google/copybara/tsjs/npm/BUILD | 0 .../copybara/tsjs/npm/NpmVersionListTest.java | 0 .../tsjs/npm/NpmVersionResolverTest.java | 0 .../util/ApplyDestinationPatchTest.java | 0 .../copybara/util/AutoPatchUtilTest.java | 0 .../util/CommandLineDiffUtilTest.java | 0 .../copybara/util/ConsistencyFileTest.java | 0 .../google/copybara/util/DiffUtilTest.java | 0 .../google/copybara/util/DirFactoryTest.java | 0 .../google/copybara/util/FileUtilTest.java | 0 .../com/google/copybara/util/GlobTest.java | 0 .../util/LimitFilterOutputStreamTest.java | 0 .../copybara/util/MergeImportToolTest.java | 0 .../google/copybara/util/OriginUtilTest.java | 0 .../copybara/util/RenameDetectorTest.java | 0 .../copybara/util/RepositoryUtilTest.java | 0 .../com/google/copybara/util/ScpUtilTest.java | 0 .../copybara/util/SequenceGlobTest.java | 0 .../copybara/util/TablePrinterTest.java | 0 .../com/google/copybara/util/console/BUILD | 0 .../copybara/util/console/ConsoleTest.java | 0 .../copybara/util/console/ConsolesTest.java | 0 .../util/console/DelegateConsoleTest.java | 0 .../util/console/FileConsoleTest.java | 0 .../util/console/MultiplexingConsoleTest.java | 0 .../util/console/NoPromptConsoleTest.java | 0 .../copybara/util/console/testing/BUILD | 0 .../console/testing/TestingConsoleTest.java | 0 .../com/google/copybara/version/BUILD | 0 .../version/CustomVersionSelectorTest.java | 0 .../version/LatestVersionSelectorTest.java | 0 .../version/OrderedVersionSelectorTest.java | 0 .../RequestedExactMatchSelectorTest.java | 0 .../version/RequestedVersionSelectorTest.java | 0 .../javatests}/com/google/copybara/xml/BUILD | 0 .../com/google/copybara/xml/XmlTest.java | 0 repositories.bzl => java/repositories.bzl | 0 {scripts => java/scripts}/update_docs | 0 {third_party => java/third_party}/BUILD | 0 {third_party => java/third_party}/bazel/BUILD | 0 .../third_party}/bazel/LICENSE | 0 .../third_party}/bazel/bashunit/BUILD | 0 .../third_party}/bazel/bashunit/LICENSE | 0 .../third_party}/bazel/bashunit/testenv.sh | 0 .../third_party}/bazel/bashunit/unittest.bash | 0 .../third_party}/bazel/conditions/BUILD | 0 .../third_party}/bazel/conditions/BUILD.tools | 0 .../shell/AbnormalTerminationException.java | 0 .../main/java/com/google/copybara/shell/BUILD | 0 .../shell/BadExitStatusException.java | 0 .../com/google/copybara/shell/Command.java | 0 .../copybara/shell/CommandException.java | 0 .../google/copybara/shell/CommandResult.java | 0 .../com/google/copybara/shell/Consumers.java | 0 .../copybara/shell/ExecFailedException.java | 0 .../copybara/shell/FutureCommandResult.java | 0 .../copybara/shell/InputStreamSink.java | 0 .../com/google/copybara/shell/Killable.java | 0 .../copybara/shell/KillableObserver.java | 0 .../com/google/copybara/shell/LogUtil.java | 0 .../copybara/shell/ProcessKillable.java | 0 .../com/google/copybara/shell/ShellUtils.java | 0 .../shell/SimpleKillableObserver.java | 0 .../copybara/shell/TerminationStatus.java | 0 .../shell/TimeoutKillableObserver.java | 0 .../devtools/build/lib/concurrent/BUILD | 0 .../build/lib/concurrent/ThreadSafety.java | 0 .../bazel/main/java/net/starlark/java/BUILD | 0 .../main/java/net/starlark/java/annot/BUILD | 0 .../java/net/starlark/java/annot/Param.java | 0 .../net/starlark/java/annot/ParamType.java | 0 .../java/net/starlark/java/annot/README.md | 0 .../java/annot/StarlarkAnnotations.java | 0 .../starlark/java/annot/StarlarkBuiltin.java | 0 .../starlark/java/annot/StarlarkMethod.java | 0 .../net/starlark/java/annot/processor/BUILD | 0 .../processor/StarlarkMethodProcessor.java | 0 .../main/java/net/starlark/java/eval/BUILD | 0 .../starlark/java/eval/BuiltinFunction.java | 0 .../net/starlark/java/eval/CallUtils.java | 0 .../net/starlark/java/eval/CpuProfiler.java | 0 .../java/eval/CpuProfilerNativeSupport.java | 0 .../eval/CpuProfilerNativeSupportImpl.java | 0 .../java/net/starlark/java/eval/Debug.java | 0 .../java/net/starlark/java/eval/Dict.java | 0 .../java/net/starlark/java/eval/Eval.java | 0 .../net/starlark/java/eval/EvalException.java | 0 .../net/starlark/java/eval/EvalUtils.java | 0 .../starlark/java/eval/FlagGuardedValue.java | 0 .../net/starlark/java/eval/FormatParser.java | 0 .../net/starlark/java/eval/GuardedValue.java | 0 .../net/starlark/java/eval/HasBinary.java | 0 .../eval/ImmutableSingletonStarlarkList.java | 0 .../java/eval/ImmutableStarlarkList.java | 0 .../main/java/net/starlark/java/eval/JNI.java | 0 .../java/eval/LazyImmutableStarlarkList.java | 0 .../starlark/java/eval/MethodDescriptor.java | 0 .../net/starlark/java/eval/MethodLibrary.java | 0 .../java/net/starlark/java/eval/Module.java | 0 .../net/starlark/java/eval/Mutability.java | 0 .../java/eval/MutableStarlarkList.java | 0 .../java/net/starlark/java/eval/NoneType.java | 0 .../starlark/java/eval/ParamDescriptor.java | 0 .../java/net/starlark/java/eval/Printer.java | 0 .../net/starlark/java/eval/RangeList.java | 0 .../eval/RegularImmutableStarlarkList.java | 0 .../net/starlark/java/eval/RegularTuple.java | 0 .../java/net/starlark/java/eval/Sequence.java | 0 .../starlark/java/eval/SingletonTuple.java | 0 .../java/net/starlark/java/eval/Starlark.java | 0 .../starlark/java/eval/StarlarkCallable.java | 0 .../net/starlark/java/eval/StarlarkFloat.java | 0 .../starlark/java/eval/StarlarkFunction.java | 0 .../starlark/java/eval/StarlarkIndexable.java | 0 .../net/starlark/java/eval/StarlarkInt.java | 0 .../starlark/java/eval/StarlarkIterable.java | 0 .../net/starlark/java/eval/StarlarkList.java | 0 .../java/eval/StarlarkMembershipTestable.java | 0 .../starlark/java/eval/StarlarkSemantics.java | 0 .../net/starlark/java/eval/StarlarkSet.java | 0 .../starlark/java/eval/StarlarkThread.java | 0 .../starlark/java/eval/StarlarkTypeValue.java | 0 .../net/starlark/java/eval/StarlarkValue.java | 0 .../net/starlark/java/eval/StringModule.java | 0 .../net/starlark/java/eval/Structure.java | 0 .../starlark/java/eval/SymbolGenerator.java | 0 .../java/net/starlark/java/eval/Tuple.java | 0 .../net/starlark/java/eval/TypeChecker.java | 0 .../starlark/java/eval/cpu_profiler_posix.cc | 0 .../main/java/net/starlark/java/lib/BUILD | 0 .../java/lib/MapWrapperStructure.java | 0 .../starlark/java/lib/StarlarkEncodable.java | 0 .../java/net/starlark/java/lib/json/BUILD | 0 .../java/net/starlark/java/lib/json/Json.java | 0 .../java/net/starlark/java/spelling/BUILD | 0 .../starlark/java/spelling/SpellChecker.java | 0 .../net/starlark/java/syntax/Argument.java | 0 .../java/syntax/AssignmentStatement.java | 0 .../main/java/net/starlark/java/syntax/BUILD | 0 .../java/syntax/BinaryOperatorExpression.java | 0 .../starlark/java/syntax/CallExpression.java | 0 .../starlark/java/syntax/CastExpression.java | 0 .../net/starlark/java/syntax/Comment.java | 0 .../starlark/java/syntax/Comprehension.java | 0 .../java/syntax/ConditionalExpression.java | 0 .../starlark/java/syntax/DefStatement.java | 0 .../starlark/java/syntax/DictExpression.java | 0 .../net/starlark/java/syntax/DocComments.java | 0 .../starlark/java/syntax/DotExpression.java | 0 .../net/starlark/java/syntax/Ellipsis.java | 0 .../net/starlark/java/syntax/Expression.java | 0 .../java/syntax/ExpressionStatement.java | 0 .../starlark/java/syntax/FileLocations.java | 0 .../net/starlark/java/syntax/FileOptions.java | 0 .../starlark/java/syntax/FloatLiteral.java | 0 .../starlark/java/syntax/FlowStatement.java | 0 .../starlark/java/syntax/ForStatement.java | 0 .../net/starlark/java/syntax/Identifier.java | 0 .../net/starlark/java/syntax/IfStatement.java | 0 .../starlark/java/syntax/IndexExpression.java | 0 .../net/starlark/java/syntax/IntLiteral.java | 0 .../java/syntax/IsInstanceExpression.java | 0 .../java/syntax/LambdaExpression.java | 0 .../java/net/starlark/java/syntax/Lexer.java | 0 .../starlark/java/syntax/ListExpression.java | 0 .../starlark/java/syntax/LoadStatement.java | 0 .../net/starlark/java/syntax/Location.java | 0 .../java/net/starlark/java/syntax/Node.java | 0 .../net/starlark/java/syntax/NodePrinter.java | 0 .../net/starlark/java/syntax/NodeVisitor.java | 0 .../net/starlark/java/syntax/Parameter.java | 0 .../java/net/starlark/java/syntax/Parser.java | 0 .../net/starlark/java/syntax/ParserInput.java | 0 .../net/starlark/java/syntax/Program.java | 0 .../net/starlark/java/syntax/Resolver.java | 0 .../starlark/java/syntax/ReturnStatement.java | 0 .../starlark/java/syntax/SliceExpression.java | 0 .../starlark/java/syntax/StarlarkFile.java | 0 .../starlark/java/syntax/StarlarkType.java | 0 .../net/starlark/java/syntax/Statement.java | 0 .../starlark/java/syntax/StringLiteral.java | 0 .../net/starlark/java/syntax/SyntaxError.java | 0 .../net/starlark/java/syntax/SyntaxUtils.java | 0 .../net/starlark/java/syntax/TokenKind.java | 0 .../java/syntax/TypeAliasStatement.java | 0 .../starlark/java/syntax/TypeApplication.java | 0 .../net/starlark/java/syntax/TypeChecker.java | 0 .../starlark/java/syntax/TypeConstructor.java | 0 .../net/starlark/java/syntax/TypeContext.java | 0 .../net/starlark/java/syntax/TypeTagger.java | 0 .../java/net/starlark/java/syntax/Types.java | 0 .../java/syntax/UnaryOperatorExpression.java | 0 .../starlark/java/syntax/VarStatement.java | 0 src/Copybara.Cli/Copybara.Cli.csproj | 18 +++ src/Copybara.Cli/Program.cs | 22 +++ src/Copybara.Common/Copybara.Common.csproj | 6 + src/Copybara.Common/ImmutableListMultimap.cs | 128 +++++++++++++++ src/Copybara.Common/Preconditions.cs | 113 +++++++++++++ src/Copybara.Core/Copybara.Core.csproj | 15 ++ src/Copybara.Core/_Placeholder.cs | 1 + src/Starlark/Starlark.csproj | 9 ++ src/Starlark/_Placeholder.cs | 1 + tests/Copybara.Tests/Copybara.Tests.csproj | 17 ++ tests/Copybara.Tests/_Placeholder.cs | 1 + 467 files changed, 659 insertions(+) create mode 100644 CLAUDE.md create mode 100644 Copybara.slnx create mode 100644 Directory.Build.props create mode 100644 TODO.md create mode 100644 global.json rename .bazelproject => java/.bazelproject (100%) rename .bazelrc => java/.bazelrc (100%) rename {.docker => java/.docker}/copybara (100%) rename .dockerignore => java/.dockerignore (100%) rename BUILD => java/BUILD (100%) rename Dockerfile => java/Dockerfile (100%) rename MODULE.bazel => java/MODULE.bazel (100%) rename ci.Dockerfile => java/ci.Dockerfile (100%) rename {ci => java/ci}/ubuntu/continuous.cfg (100%) rename {ci => java/ci}/ubuntu/continuous.sh (100%) rename cloudbuild.sh => java/cloudbuild.sh (100%) rename cloudbuild.yaml => java/cloudbuild.yaml (100%) rename cloudbuild_setup.sh => java/cloudbuild_setup.sh (100%) rename {copybara => java/copybara}/integration/BUILD (100%) rename {copybara => java/copybara}/integration/reference_doc_test.sh (100%) rename {copybara => java/copybara}/integration/test-help.t (100%) rename {copybara => java/copybara}/integration/tool_test.sh (100%) rename {docs => java/docs}/BUILD (100%) rename {docs => java/docs}/examples.md (100%) rename {docs => java/docs}/reference.md (100%) rename {external => java/external}/third_party/BUILD (100%) rename {external => java/external}/third_party/jcommander.BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/ActionMigrationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/BaselinesWithoutLabelVisitorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/ChangeMessageTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/ChangesResponseTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/CheckoutPathTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/CommandEnvTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/CommandRunnerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/CoreReverseTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/CoreTransformTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/ExamplesTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/InfoTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/LabelFinderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/MainArgumentsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/MainTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/MigrateCmdTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/ReadConfigFromChangeWorkflowTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/ReferenceTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/StarlarkDateTimeModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/StarlarkJsonModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/StarlarkRandomModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/StructModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/TransformWorkTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/WorkflowTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/archive/ArchiveModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/archive/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/archive/util/ArchiveUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/archive/util/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/authoring/AuthorParserTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/authoring/AuthorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/authoring/AuthoringTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/buildozer/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/buildozer/BuildozerBatchTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/buildozer/BuildozerCreateTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/buildozer/BuildozerDeleteTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/buildozer/BuildozerModifyTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/buildozer/BuildozerPrintExecutorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/checks/ApiCheckerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/checks/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/config/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/config/CapturingConfigFileTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/config/MapConfigFileTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/config/PathBasedConfigFileTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/config/ResolveDelegateConfigFileTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/config/SkylarkParserTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/config/SkylarkUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/config/ValidationResultTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/config/base/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/config/base/SkylarkUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/configgen/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/configgen/ConfigGenHeuristicsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/credentials/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/credentials/CredentialModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/credentials/TtlSecretTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/exception/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/exception/ValidationExceptionTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/folder/FolderDestinationReaderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/folder/FolderDestinationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/folder/FolderOriginTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/format/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/format/BuildifierFormatTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/git/CredentialFileHandlerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/FuzzyClosestVersionSelectorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GerritDestinationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GerritEndpointTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GerritOptionsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GerritOriginTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GerritTriggerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitCredentialTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitDestinationIntegrateTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitDestinationReaderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitDestinationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitEnvironmentTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubDestinationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubEndpointTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubOptionsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubPostSubmitApprovalsProviderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubPrDestinationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubPrIntegrateLabelTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubPrOriginTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubPreSubmitApprovalsProviderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubRepositoryHookTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubSecuritySettingsValidatorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubTriggerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitHubUserApprovalsValidatorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitLabMrDestinationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitLabMrOriginTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitLabMrWriteHookTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitLabMrWriterTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitMirrorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitOriginSubmodulesTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitOriginTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitRepoTypeTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitRepositoryTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/GitRevisionTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/LatestVersionSelectorForGitTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/RefspecTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/SameGitTreeTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/SubmodulesInDestinationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gerritapi/GerritApiTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gerritapi/GerritApiTransportWithCheckerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/github/api/GitHubApiTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/github/api/GitHubApiTransportImplTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/github/api/GitHubApiTransportWithCheckerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/github/api/GitHubGraphQLTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/github/util/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/git/github/util/GitHubHostTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/github/util/GitHubIdentifierTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/github/util/GitHubUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/GitLabOptionsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/GitLabUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/GitLabApiTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/GitLabApiTransportImplTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/CreateMergeRequestParamsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/ExternalStatusCheckTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/GitLabApiParamsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/ListProjectMergeRequestParamsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/ListUsersParamsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/MergeRequestTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/PaginatedPageListTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/ProjectTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckParamsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckResponseTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/git/gitlab/api/entities/UpdateMergeRequestParamsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/go/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/go/GoProxyVersioningTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/go/PseudoVersionSelectorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/hashing/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/hashing/HashingModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/hg/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/hg/HgDestinationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/hg/HgOriginTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/hg/HgRepositoryTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/html/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/html/HtmlTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/http/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/http/HttpModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/http/auth/AuthTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/http/auth/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/http/endpoint/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/http/endpoint/HttpEndpointTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/http/json/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/http/json/HttpEndpointJsonContentTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/http/multipart/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/http/multipart/HttpEndpointMultipartContentTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/http/multipart/HttpEndpointUrlEncodedFormContentTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/jcommander/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/jcommander/DurationConverterTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/jcommander/GlobConverterTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/jcommander/SemicolonListConverterTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/json/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/json/GsonParserUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/monitor/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/monitor/ConsoleEventMonitorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/CommandLineGuideTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/ConfigBuilderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/ConfigHeuristicsInputProviderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/GeneratorCmdTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/GitToGitGeneratorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/InputsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/OnboardCmdTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/StarlarkConverterTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/core/AskInputProviderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/core/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/core/CachedInputProviderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/core/ConstantProviderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/core/InputProviderResolverImplTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/core/InputTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/core/MapBasedInputProviderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/core/PrioritizedInputProviderTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/onboard/core/template/TemplateConfigGeneratorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/profiler/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/profiler/ConsoleProfilerListenerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/profiler/ProfilerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/python/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/python/PackageMetadataTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/python/testfiles/TESTMETADATA (100%) rename {javatests => java/javatests}/com/google/copybara/re2/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/re2/Re2Test.java (100%) rename {javatests => java/javatests}/com/google/copybara/regenerate/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/regenerate/RegenerateCmdTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/remotefile/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/remotefile/GithubArchiveTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/remotefile/RemoteArchiveOriginTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/remotefile/RemoteFileModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/remotefile/extractutil/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/remotefile/extractutil/ExtractUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/rust/ComparisonRustVersionRequirementTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/DefaultRustVersionRequirementTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/EpochRustVersionRequirementTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/MultipleRustVersionRequirementTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/RustCratesIoVersionListTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/RustCratesIoVersionResolverTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/RustCratesIoVersionSelectorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/RustModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/RustVersionRequirementTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/TildeRustVersionRequirementTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/rust/WildcardRustVersionRequirementTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/templatetoken/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/templatetoken/LabelTemplateTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/test.bzl (100%) rename {javatests => java/javatests}/com/google/copybara/testing/DummyCheckerTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/testing/DummyOriginTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/testing/SkylarkTestExecutorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/toml/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/toml/TomlTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/transform/ConvertEncodingTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/CopyOrMoveTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/CoreTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/ExplicitReversalTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/FilterReplaceTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/RemoveTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/RenameTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/ReplaceTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/SequenceTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/SkylarkConsoleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/SkylarkTransformationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/TodoReplaceTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/VerifyMatchTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/debug/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/transform/debug/TransformDebugTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/metadata/MetadataModuleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/metadata/RevisionMigratorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/patch/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/transform/patch/PatchTransformationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/patch/PatchingOptionsTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/transform/patch/QuiltTransformationTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/treestate/TreeStateTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/tsjs/npm/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/tsjs/npm/NpmVersionListTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/tsjs/npm/NpmVersionResolverTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/ApplyDestinationPatchTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/AutoPatchUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/CommandLineDiffUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/ConsistencyFileTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/DiffUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/DirFactoryTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/FileUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/GlobTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/LimitFilterOutputStreamTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/MergeImportToolTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/OriginUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/RenameDetectorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/RepositoryUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/ScpUtilTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/SequenceGlobTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/TablePrinterTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/console/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/util/console/ConsoleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/console/ConsolesTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/console/DelegateConsoleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/console/FileConsoleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/console/MultiplexingConsoleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/console/NoPromptConsoleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/util/console/testing/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/util/console/testing/TestingConsoleTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/version/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/version/CustomVersionSelectorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/version/LatestVersionSelectorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/version/OrderedVersionSelectorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/version/RequestedExactMatchSelectorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/version/RequestedVersionSelectorTest.java (100%) rename {javatests => java/javatests}/com/google/copybara/xml/BUILD (100%) rename {javatests => java/javatests}/com/google/copybara/xml/XmlTest.java (100%) rename repositories.bzl => java/repositories.bzl (100%) rename {scripts => java/scripts}/update_docs (100%) rename {third_party => java/third_party}/BUILD (100%) rename {third_party => java/third_party}/bazel/BUILD (100%) rename {third_party => java/third_party}/bazel/LICENSE (100%) rename {third_party => java/third_party}/bazel/bashunit/BUILD (100%) rename {third_party => java/third_party}/bazel/bashunit/LICENSE (100%) rename {third_party => java/third_party}/bazel/bashunit/testenv.sh (100%) rename {third_party => java/third_party}/bazel/bashunit/unittest.bash (100%) rename {third_party => java/third_party}/bazel/conditions/BUILD (100%) rename {third_party => java/third_party}/bazel/conditions/BUILD.tools (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/AbnormalTerminationException.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/BadExitStatusException.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/Command.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/CommandException.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/CommandResult.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/Consumers.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/ExecFailedException.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/FutureCommandResult.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/InputStreamSink.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/Killable.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/KillableObserver.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/LogUtil.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/ProcessKillable.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/ShellUtils.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/SimpleKillableObserver.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/TerminationStatus.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/copybara/shell/TimeoutKillableObserver.java (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/devtools/build/lib/concurrent/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/com/google/devtools/build/lib/concurrent/ThreadSafety.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/annot/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/annot/Param.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/annot/ParamType.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/annot/README.md (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/annot/StarlarkAnnotations.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/annot/StarlarkBuiltin.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/annot/StarlarkMethod.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/annot/processor/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/annot/processor/StarlarkMethodProcessor.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/BuiltinFunction.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/CallUtils.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/CpuProfiler.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupport.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupportImpl.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Debug.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Dict.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Eval.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/EvalException.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/EvalUtils.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/FlagGuardedValue.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/FormatParser.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/GuardedValue.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/HasBinary.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/ImmutableSingletonStarlarkList.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/ImmutableStarlarkList.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/JNI.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/LazyImmutableStarlarkList.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/MethodDescriptor.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/MethodLibrary.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Module.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Mutability.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/MutableStarlarkList.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/NoneType.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/ParamDescriptor.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Printer.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/RangeList.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/RegularImmutableStarlarkList.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/RegularTuple.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Sequence.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/SingletonTuple.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Starlark.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkCallable.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkFloat.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkFunction.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkIndexable.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkInt.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkIterable.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkList.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkMembershipTestable.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkSemantics.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkSet.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkThread.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkTypeValue.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StarlarkValue.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/StringModule.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Structure.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/SymbolGenerator.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/Tuple.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/TypeChecker.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/eval/cpu_profiler_posix.cc (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/lib/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/lib/MapWrapperStructure.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/lib/StarlarkEncodable.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/lib/json/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/lib/json/Json.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/spelling/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/spelling/SpellChecker.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Argument.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/AssignmentStatement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/BUILD (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/BinaryOperatorExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/CallExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/CastExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Comment.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Comprehension.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/ConditionalExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/DefStatement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/DictExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/DocComments.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/DotExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Ellipsis.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Expression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/ExpressionStatement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/FileLocations.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/FileOptions.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/FloatLiteral.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/FlowStatement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/ForStatement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Identifier.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/IfStatement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/IndexExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/IntLiteral.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/IsInstanceExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/LambdaExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Lexer.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/ListExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/LoadStatement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Location.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Node.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/NodePrinter.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/NodeVisitor.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Parameter.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Parser.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/ParserInput.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Program.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Resolver.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/ReturnStatement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/SliceExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/StarlarkFile.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/StarlarkType.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Statement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/StringLiteral.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/SyntaxError.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/SyntaxUtils.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/TokenKind.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/TypeAliasStatement.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/TypeApplication.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/TypeChecker.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/TypeConstructor.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/TypeContext.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/TypeTagger.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/Types.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/UnaryOperatorExpression.java (100%) rename {third_party => java/third_party}/bazel/main/java/net/starlark/java/syntax/VarStatement.java (100%) create mode 100644 src/Copybara.Cli/Copybara.Cli.csproj create mode 100644 src/Copybara.Cli/Program.cs create mode 100644 src/Copybara.Common/Copybara.Common.csproj create mode 100644 src/Copybara.Common/ImmutableListMultimap.cs create mode 100644 src/Copybara.Common/Preconditions.cs create mode 100644 src/Copybara.Core/Copybara.Core.csproj create mode 100644 src/Copybara.Core/_Placeholder.cs create mode 100644 src/Starlark/Starlark.csproj create mode 100644 src/Starlark/_Placeholder.cs create mode 100644 tests/Copybara.Tests/Copybara.Tests.csproj create mode 100644 tests/Copybara.Tests/_Placeholder.cs diff --git a/.gitignore b/.gitignore index 6fd85bcf9..a6db66213 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,11 @@ bazel-* .aswb /.bazelrc.user MODULE.bazel.lock +# .NET +bin/ +obj/ +*.user +[Nn]upkg/ +.vs/ +*.suo +TestResults/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..20a3fb7e7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,136 @@ +# CLAUDE.md — Copybara .NET Port + +This file orients Claude (and humans) working in this repository. It describes +what the project is, the state of the C# port, the conventions to follow, and +where things live. + +## What is Copybara? + +Copybara is a tool, originally built and open-sourced by Google, for +**transforming and moving code between repositories**. A typical use case is +keeping an internal (confidential) repository in sync with a public one: you +declare a *workflow* that reads changes from an *origin*, applies a chain of +*transformations* (move files, replace strings, scrub metadata, etc.), and +writes the result to a *destination*, recording state as a label in the +destination commit message so the process is stateless and reproducible. + +Configuration is written in **Starlark** (the Python-like dialect used by +Bazel), in a file conventionally named `copy.bara.sky`. The CLI exposes +subcommands like `migrate`, `info`, `validate`, and `version`. + +Key domain concepts: + +- **Origin** — where code is read from (git, GitHub, Gerrit, Mercurial, a + local folder, remote files, …). +- **Destination** — where code is written to (git, GitHub PR, a local folder, …). +- **Workflow** — ties an origin, destination, authoring, and a list of + transformations together. Runs in a mode (`SQUASH`, `ITERATIVE`, `CHANGE_REQUEST`). +- **Transformation** — a reversible (where possible) operation on the checkout + (`core.replace`, `core.move`, `core.copy`, metadata scrubbers, patch, …). +- **Revision / Change** — a point-in-time state of an origin (e.g. a git SHA) + and its metadata (author, message, labels, timestamp). +- **Glob** — include/exclude path matcher used pervasively to select files. +- **Authoring** — how author identity is mapped from origin to destination + (pass-thru, allowlist, overwrite). +- **Console** — abstraction over terminal I/O (ANSI, log, file, prompts). + +## Repository layout + +``` +/ Root of the .NET port +├── CLAUDE.md This file +├── TODO.md Detailed, living port plan / work breakdown +├── README.md Upstream Copybara readme (kept as-is) +├── LICENSE Apache 2.0 +├── Copybara.sln .NET solution +├── src/ +│ ├── Copybara.Common/ Guava-like collections, Preconditions, helpers +│ ├── Starlark/ Port of net.starlark.java (Starlark interpreter) +│ ├── Copybara.Core/ Port of com.google.copybara.* (the engine) +│ └── Copybara.Cli/ The .NET tool entry point (command `copybara`) +├── tests/ +│ └── Copybara.Tests/ xUnit tests (ports of javatests where practical) +└── java/ The ORIGINAL Java/Bazel implementation (reference only) + ├── com/google/copybara/... ~90k LOC — the engine + ├── third_party/bazel/.../net/starlark ~31k LOC — Starlark interpreter + ├── javatests/ Original test suite + ├── BUILD, MODULE.bazel, ... Original Bazel build + └── docs/, scripts/, ci/, ... +``` + +**The `java/` directory is the source of truth for behavior.** When porting a +class, open the corresponding Java file under `java/com/google/copybara/…` (or +`java/third_party/bazel/main/java/net/starlark/…` for the interpreter) and +mirror its behavior. Do not delete or modify files under `java/`. + +## Port goals + +1. **Target .NET 10**, C# latest language version, nullable reference types on. +2. **Ship as a .NET tool** (`dotnet tool install --global Copybara`) that + exposes a `copybara` command with the same CLI surface as upstream. +3. **Preserve behavior and config compatibility**: existing `copy.bara.sky` + configs should work. This requires a faithful Starlark interpreter. +4. **Use LibGit2Sharp** (NuGet package id: `LibGit2Sharp`) for git plumbing + instead of shelling out to `git` where practical; fall back to invoking the + `git` binary only where LibGit2Sharp lacks a feature (e.g. some fetch/push + refspec behaviors, credential helpers). +5. **Idiomatic C#**: PascalCase members, properties over getters where natural, + `async`/`Task` for I/O-bound repo operations, records for value types. +6. **Faithful but not slavish**: keep class/relationship structure recognizable + against the Java source, but drop Java-isms (checked exceptions, builder + boilerplate where records/init suffice, Guava types where BCL equivalents fit). + +## Java → C# mapping conventions + +Follow these consistently so the port stays coherent across contributors/agents. + +| Java / library | C# / .NET | +|----------------------------------------|-----------------------------------------------------------------| +| `com.google.copybara.foo` | namespace `Copybara.Foo` | +| `net.starlark.java.eval` | namespace `Starlark.Eval` (project `Starlark`) | +| `ImmutableList` | `ImmutableArray` (return as `IReadOnlyList` in APIs) | +| `ImmutableMap` | `ImmutableDictionary` / `IReadOnlyDictionary` | +| `ImmutableSet` | `ImmutableHashSet` / `IReadOnlySet` | +| `ImmutableListMultimap` | `Copybara.Common.ImmutableListMultimap` (helper type) | +| `Optional` | nullable (`T?`) for refs; `T?`/`Nullable` for structs | +| `Preconditions.checkNotNull/State` | `Copybara.Common.Preconditions` helpers | +| Guava `Splitter`/`Joiner`/`CharMatcher`| BCL `string.Split`/`string.Join` + helpers in `Copybara.Common` | +| checked exceptions (`throws`) | plain exceptions; document in XML ``; no `throws` | +| `@StarlarkBuiltin` / `@StarlarkMethod` | `[StarlarkBuiltin]` / `[StarlarkMethod]` attributes | +| `StarlarkValue` | `IStarlarkValue` marker interface | +| JGit | `LibGit2Sharp` (+ `git` binary fallback via a `GitEnv` runner) | +| JCommander (`@Parameter`) | custom lightweight arg parser in `Copybara.Cli` (see TODO) | +| Flogger `FluentLogger` | `Microsoft.Extensions.Logging.ILogger` | +| gson | `System.Text.Json` | +| re2j | .NET `System.Text.RegularExpressions` (note: not RE2 semantics) | +| `java.nio.file.Path` | `string` paths + `System.IO`; a `CheckoutPath` domain type | +| JUnit + Truth | xUnit + FluentAssertions | + +Notes / gotchas: + +- **re2j is not a drop-in for .NET Regex.** Copybara exposes `core.replace` + regex groups with RE2 semantics. Start with `System.Text.RegularExpressions` + and record any semantic divergence in TODO; consider a real RE2 port later. +- **Starlark is the critical path.** Almost every domain object is a + `StarlarkValue` with `@StarlarkMethod` fields, and the config loader executes + Starlark. The `Starlark` project must land before most `*Module` classes can + be meaningfully ported. See TODO for the phased approach. +- **File header:** keep the Apache 2.0 license header on ported files. + +## Build / test + +``` +dotnet build Copybara.sln +dotnet test Copybara.sln +# Run the tool locally: +dotnet run --project src/Copybara.Cli -- migrate copy.bara.sky +# Pack as a global tool: +dotnet pack src/Copybara.Cli -c Release +``` + +## Status + +The port is in progress. **TODO.md is the living work breakdown** — consult it +for what is done, in progress, and pending, and update it as work lands. + + diff --git a/Copybara.slnx b/Copybara.slnx new file mode 100644 index 000000000..8552873eb --- /dev/null +++ b/Copybara.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..f537548f7 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,14 @@ + + + net10.0 + latest + enable + enable + false + false + Copybara .NET Port + Copybara + https://github.com/theolivenbaum/copybara + Apache-2.0 + + diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..416c6ffc9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,153 @@ +# TODO — Copybara .NET 10 Port + +Living work breakdown. Status legend: ✅ done · 🚧 in progress · ⬜ pending · +🔬 needs investigation. + +The port is large (~90k LOC engine + ~31k LOC Starlark interpreter). It is +organized in dependency order: foundation first, then the Starlark interpreter, +then the domain modules that depend on both. + +--- + +## Phase 0 — Project setup + +- ✅ Move original Java/Bazel tree into `java/` (reference only). +- ✅ Author `CLAUDE.md` and `TODO.md`. +- 🚧 Create solution + project scaffold: + - `src/Copybara.Common` — helpers (Preconditions, ImmutableListMultimap, Glob-support types). + - `src/Starlark` — Starlark interpreter port. + - `src/Copybara.Core` — engine. + - `src/Copybara.Cli` — the `copybara` .NET tool (`PackAsTool=true`). + - `tests/Copybara.Tests` — xUnit. +- 🚧 Wire NuGet deps: `LibGit2Sharp`, `Microsoft.Extensions.Logging`, + `System.Text.Json`, `System.Collections.Immutable`, xUnit, FluentAssertions. +- ⬜ `.gitignore` for .NET (`bin/`, `obj/`, `*.user`). +- ⬜ CI: `dotnet build` + `dotnet test` GitHub Action. + +## Phase 1 — Foundation (`Copybara.Common` + `Copybara.Core` primitives) + +No Starlark dependency. Port these first; they unblock everything else. + +- 🚧 `Copybara.Common` + - `Preconditions` (CheckNotNull, CheckArgument, CheckState). + - `ImmutableListMultimap` + builder. + - String helpers (Splitter/Joiner/CharMatcher equivalents as needed). +- 🚧 `exception/` → `Copybara.Exceptions` + - `ValidationException` (+ `CheckCondition`), `RepoException`, + `CannotResolveRevisionException`, `EmptyChangeException`, + `ChangeRejectedException`, `NonReversibleValidationException`, + `CommandLineException`, `RedundantChangeException`, `VoidOperationException`, + `NotADestinationFileException`, `AccessValidationException`, + `CannotResolveLabel`. +- ⬜ `util/` core (subset, no git): + - `ExitCode`, `Glob` + `GlobAtom` + `SequenceGlob` + `ReadablePathMatcher`, + `FileUtil`, `DirFactory`, `CommandOutput(WithStatus)`, `CommandRunner` + (process runner), `Identity`, `TablePrinter`, `RenameDetector`, `DiffUtil` + (backed by `git diff`/LibGit2Sharp), `OriginUtil`. +- ⬜ `util/console/` → `Copybara.Util.Console` + - `Console` interface, `AnsiConsole`, `LogConsole`, `FileConsole`, + `NoPromptConsole`, `CapturingConsole`, `Consoles`, `ProgressPrefixConsole`. +- ⬜ `revision/` → `Copybara.Revision`: `Revision`, `Change`, `Changes`, `OriginRef`. +- ⬜ `authoring/` → `Copybara.Authoring`: `Author`, `AuthorParser`, `Authoring`, + `InvalidAuthorException` (note: `Author`/`Authoring` are Starlark values — see Phase 3). +- ⬜ `templatetoken/` → `Copybara.TemplateToken`. +- ⬜ `profiler/` → `Copybara.Profiler`. + +## Phase 2 — Starlark interpreter (`src/Starlark`) + +Port of `java/third_party/bazel/main/java/net/starlark/java`. This is a +self-contained interpreter and the critical dependency for config loading. +Sub-packages: `annot`, `syntax` (lexer/parser/AST), `eval` (values, evaluator, +builtins), `lib` (json, proto, etc.), `spelling`. + +- ⬜ `annot/` — attributes: `[StarlarkBuiltin]`, `[StarlarkMethod]`, + `[Param]`, `[ParamType]`, `StarlarkDocumentationCategory`, etc. +- ⬜ `syntax/` — `Lexer`, `Parser`, AST node types, `Location`, `FileOptions`, + `SyntaxError`. +- ⬜ `eval/` — `StarlarkValue`, `Starlark` (entry/helpers), `StarlarkThread`, + `Module`, `Mutability`, `StarlarkInt/Float/List/Dict/Tuple/Function`, + `EvalException`, `MethodLibrary`, `StarlarkCallable`, `Structure`, + `CallUtils`/method-descriptor reflection (map `@StarlarkMethod` → dispatch). +- ⬜ `lib/json` — `Json` module (interop with `System.Text.Json`). +- ⬜ `spelling/` — `SpellChecker`. +- ⬜ Decide reflection strategy: Java uses annotation processing + reflection. + In C#, use reflection over `[StarlarkMethod]` attributes at startup, cached + per type. (Source generators are a later optimization.) +- 🔬 Validate with a handful of upstream Starlark eval tests ported to xUnit. + +## Phase 3 — Config model & core module (needs Phases 1–2) + +- ⬜ `config/` → `Copybara.Config`: `Config`, `ConfigFile`, + `PathBasedConfigFile`, `MapConfigFile`, `Migration`, `ConfigValidator`, + `SkylarkParser`, `Config(Loader/Provider)`, `LabelsAwareModule`. +- ⬜ Top-level engine types in `com.google.copybara/`: + `Option`/`Options`, `GeneralOptions`, `ModuleSet`/`ModuleSupplier`, + `Core`(`CoreModule`, `CoreGlobal`), `Workflow`, `WorkflowOptions`, + `WorkflowMode`, `WorkflowRunHelper`, `Migration`, `Origin`, `Destination`, + `Transformation`, `TransformWork`, `TransformResult`, `CheckoutPath`, + `CheckoutFileSystem`, `Metadata`, `Info`, `MigrationInfo`. + +## Phase 4 — Transformations (`transform/`) + +- ⬜ `Replace`, `Move`/`Copy`/`Remove`, `Sequence`, `TransformationRegistry`, + `SkylarkTransformation`, `ExplicitReversal`, `TodoReplace`, `Scrubber`, + `metadata/*` (message/label manipulation), `debug/*`, `patch/*`. + +## Phase 5 — Git support (`git/`) — uses LibGit2Sharp + +Largest single module (175 files). Port in slices: + +- ⬜ Core plumbing: `GitRepository`, `GitRevision`, `GitReference`, + `GitEnvironment`, `GitCredential`, `RefspecConverter`, `GitOptions`. + Prefer LibGit2Sharp; keep a `git` CLI runner for gaps. +- ⬜ Origin/Destination: `GitOrigin`, `GitDestination`, `GitModule`, + `GitMirror`, `ChangeReader`, writer hooks. +- ⬜ GitHub: `github/api` client (System.Text.Json + HttpClient), + `GitHubOrigin`, `GitHubPrDestination`, `GitHubPrOrigin`, `github/util`. +- ⬜ Gerrit: `gerritapi` client, `GerritOrigin`, `GerritDestination`. +- ⬜ GitLab: `gitlab/api`, origin/destination. +- ⬜ `version/` resolvers. + +## Phase 6 — Other origins/destinations & modules + +- ⬜ `folder/` (FolderOrigin/FolderDestination) — good early integration target. +- ⬜ `remotefile/`, `archive/`, `hashing/`, `http/`, `format/` (buildifier), + `hg/` (Mercurial), `go/`, `rust/`, `python/`, `tsjs/`, `toml/`, `json/`, + `xml/`, `html/`, `re2/`, `buildozer/`, `checks/`, `approval/`, `feedback/`, + `action/`, `credentials/`, `treestate/`, `monitor/`, `regenerate/`, + `onboard/`, `configgen/`, `doc/` (reference doc generator). + +## Phase 7 — CLI (`src/Copybara.Cli`) + +- ⬜ Arg parsing (replace JCommander). Options contributed per-module, à la + `Options.getAll()`. Lightweight custom parser matching upstream flag names. +- ⬜ `Main` orchestration (mirror `Main.java`): console setup, logging config, + module set creation, command dispatch, exit codes, error handling. +- ⬜ Commands: `MigrateCmd`, `InfoCmd`, `ValidateCmd`, `HelpCmd`, `VersionCmd`, + `RegenerateCmd`, `OnboardCmd`/`GeneratorCmd`. +- ⬜ `PackAsTool` metadata, `build-data` version embedding. + +## Phase 8 — Tests, docs, polish + +- ⬜ Port high-value tests from `javatests/` (glob, author, replace, workflow, + git origin/destination round-trips). +- ⬜ End-to-end smoke test: folder→folder migrate with a `core.replace`. +- ⬜ Reference doc generation parity (optional). +- ⬜ Performance pass; RE2 semantics decision. + +--- + +## Cross-cutting decisions / open questions + +- 🔬 **RE2 vs .NET Regex** for `core.replace` — start with .NET Regex, flag + divergences. +- 🔬 **Async vs sync** — repo I/O is naturally async in .NET; upstream is + blocking. Decide per-boundary; keep the engine mostly synchronous initially to + stay close to the source, use async only at process/network edges. +- 🔬 **Path handling** — Java uses `java.nio.file.Path` + Jimfs in tests. Use + `string` + `System.IO`, and an in-memory filesystem abstraction for tests if + needed. +- 🔬 **git CLI dependency** — LibGit2Sharp covers most, but shallow fetch, + some refspec and credential-helper behaviors may still require the `git` + binary. Keep `GitEnv`/`CommandRunner` available as a fallback. + diff --git a/global.json b/global.json new file mode 100644 index 000000000..512142d2b --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/.bazelproject b/java/.bazelproject similarity index 100% rename from .bazelproject rename to java/.bazelproject diff --git a/.bazelrc b/java/.bazelrc similarity index 100% rename from .bazelrc rename to java/.bazelrc diff --git a/.docker/copybara b/java/.docker/copybara similarity index 100% rename from .docker/copybara rename to java/.docker/copybara diff --git a/.dockerignore b/java/.dockerignore similarity index 100% rename from .dockerignore rename to java/.dockerignore diff --git a/BUILD b/java/BUILD similarity index 100% rename from BUILD rename to java/BUILD diff --git a/Dockerfile b/java/Dockerfile similarity index 100% rename from Dockerfile rename to java/Dockerfile diff --git a/MODULE.bazel b/java/MODULE.bazel similarity index 100% rename from MODULE.bazel rename to java/MODULE.bazel diff --git a/ci.Dockerfile b/java/ci.Dockerfile similarity index 100% rename from ci.Dockerfile rename to java/ci.Dockerfile diff --git a/ci/ubuntu/continuous.cfg b/java/ci/ubuntu/continuous.cfg similarity index 100% rename from ci/ubuntu/continuous.cfg rename to java/ci/ubuntu/continuous.cfg diff --git a/ci/ubuntu/continuous.sh b/java/ci/ubuntu/continuous.sh similarity index 100% rename from ci/ubuntu/continuous.sh rename to java/ci/ubuntu/continuous.sh diff --git a/cloudbuild.sh b/java/cloudbuild.sh similarity index 100% rename from cloudbuild.sh rename to java/cloudbuild.sh diff --git a/cloudbuild.yaml b/java/cloudbuild.yaml similarity index 100% rename from cloudbuild.yaml rename to java/cloudbuild.yaml diff --git a/cloudbuild_setup.sh b/java/cloudbuild_setup.sh similarity index 100% rename from cloudbuild_setup.sh rename to java/cloudbuild_setup.sh diff --git a/copybara/integration/BUILD b/java/copybara/integration/BUILD similarity index 100% rename from copybara/integration/BUILD rename to java/copybara/integration/BUILD diff --git a/copybara/integration/reference_doc_test.sh b/java/copybara/integration/reference_doc_test.sh similarity index 100% rename from copybara/integration/reference_doc_test.sh rename to java/copybara/integration/reference_doc_test.sh diff --git a/copybara/integration/test-help.t b/java/copybara/integration/test-help.t similarity index 100% rename from copybara/integration/test-help.t rename to java/copybara/integration/test-help.t diff --git a/copybara/integration/tool_test.sh b/java/copybara/integration/tool_test.sh similarity index 100% rename from copybara/integration/tool_test.sh rename to java/copybara/integration/tool_test.sh diff --git a/docs/BUILD b/java/docs/BUILD similarity index 100% rename from docs/BUILD rename to java/docs/BUILD diff --git a/docs/examples.md b/java/docs/examples.md similarity index 100% rename from docs/examples.md rename to java/docs/examples.md diff --git a/docs/reference.md b/java/docs/reference.md similarity index 100% rename from docs/reference.md rename to java/docs/reference.md diff --git a/external/third_party/BUILD b/java/external/third_party/BUILD similarity index 100% rename from external/third_party/BUILD rename to java/external/third_party/BUILD diff --git a/external/third_party/jcommander.BUILD b/java/external/third_party/jcommander.BUILD similarity index 100% rename from external/third_party/jcommander.BUILD rename to java/external/third_party/jcommander.BUILD diff --git a/javatests/com/google/copybara/ActionMigrationTest.java b/java/javatests/com/google/copybara/ActionMigrationTest.java similarity index 100% rename from javatests/com/google/copybara/ActionMigrationTest.java rename to java/javatests/com/google/copybara/ActionMigrationTest.java diff --git a/javatests/com/google/copybara/BUILD b/java/javatests/com/google/copybara/BUILD similarity index 100% rename from javatests/com/google/copybara/BUILD rename to java/javatests/com/google/copybara/BUILD diff --git a/javatests/com/google/copybara/BaselinesWithoutLabelVisitorTest.java b/java/javatests/com/google/copybara/BaselinesWithoutLabelVisitorTest.java similarity index 100% rename from javatests/com/google/copybara/BaselinesWithoutLabelVisitorTest.java rename to java/javatests/com/google/copybara/BaselinesWithoutLabelVisitorTest.java diff --git a/javatests/com/google/copybara/ChangeMessageTest.java b/java/javatests/com/google/copybara/ChangeMessageTest.java similarity index 100% rename from javatests/com/google/copybara/ChangeMessageTest.java rename to java/javatests/com/google/copybara/ChangeMessageTest.java diff --git a/javatests/com/google/copybara/ChangesResponseTest.java b/java/javatests/com/google/copybara/ChangesResponseTest.java similarity index 100% rename from javatests/com/google/copybara/ChangesResponseTest.java rename to java/javatests/com/google/copybara/ChangesResponseTest.java diff --git a/javatests/com/google/copybara/CheckoutPathTest.java b/java/javatests/com/google/copybara/CheckoutPathTest.java similarity index 100% rename from javatests/com/google/copybara/CheckoutPathTest.java rename to java/javatests/com/google/copybara/CheckoutPathTest.java diff --git a/javatests/com/google/copybara/CommandEnvTest.java b/java/javatests/com/google/copybara/CommandEnvTest.java similarity index 100% rename from javatests/com/google/copybara/CommandEnvTest.java rename to java/javatests/com/google/copybara/CommandEnvTest.java diff --git a/javatests/com/google/copybara/CommandRunnerTest.java b/java/javatests/com/google/copybara/CommandRunnerTest.java similarity index 100% rename from javatests/com/google/copybara/CommandRunnerTest.java rename to java/javatests/com/google/copybara/CommandRunnerTest.java diff --git a/javatests/com/google/copybara/CoreReverseTest.java b/java/javatests/com/google/copybara/CoreReverseTest.java similarity index 100% rename from javatests/com/google/copybara/CoreReverseTest.java rename to java/javatests/com/google/copybara/CoreReverseTest.java diff --git a/javatests/com/google/copybara/CoreTransformTest.java b/java/javatests/com/google/copybara/CoreTransformTest.java similarity index 100% rename from javatests/com/google/copybara/CoreTransformTest.java rename to java/javatests/com/google/copybara/CoreTransformTest.java diff --git a/javatests/com/google/copybara/ExamplesTest.java b/java/javatests/com/google/copybara/ExamplesTest.java similarity index 100% rename from javatests/com/google/copybara/ExamplesTest.java rename to java/javatests/com/google/copybara/ExamplesTest.java diff --git a/javatests/com/google/copybara/InfoTest.java b/java/javatests/com/google/copybara/InfoTest.java similarity index 100% rename from javatests/com/google/copybara/InfoTest.java rename to java/javatests/com/google/copybara/InfoTest.java diff --git a/javatests/com/google/copybara/LabelFinderTest.java b/java/javatests/com/google/copybara/LabelFinderTest.java similarity index 100% rename from javatests/com/google/copybara/LabelFinderTest.java rename to java/javatests/com/google/copybara/LabelFinderTest.java diff --git a/javatests/com/google/copybara/MainArgumentsTest.java b/java/javatests/com/google/copybara/MainArgumentsTest.java similarity index 100% rename from javatests/com/google/copybara/MainArgumentsTest.java rename to java/javatests/com/google/copybara/MainArgumentsTest.java diff --git a/javatests/com/google/copybara/MainTest.java b/java/javatests/com/google/copybara/MainTest.java similarity index 100% rename from javatests/com/google/copybara/MainTest.java rename to java/javatests/com/google/copybara/MainTest.java diff --git a/javatests/com/google/copybara/MigrateCmdTest.java b/java/javatests/com/google/copybara/MigrateCmdTest.java similarity index 100% rename from javatests/com/google/copybara/MigrateCmdTest.java rename to java/javatests/com/google/copybara/MigrateCmdTest.java diff --git a/javatests/com/google/copybara/ReadConfigFromChangeWorkflowTest.java b/java/javatests/com/google/copybara/ReadConfigFromChangeWorkflowTest.java similarity index 100% rename from javatests/com/google/copybara/ReadConfigFromChangeWorkflowTest.java rename to java/javatests/com/google/copybara/ReadConfigFromChangeWorkflowTest.java diff --git a/javatests/com/google/copybara/ReferenceTest.java b/java/javatests/com/google/copybara/ReferenceTest.java similarity index 100% rename from javatests/com/google/copybara/ReferenceTest.java rename to java/javatests/com/google/copybara/ReferenceTest.java diff --git a/javatests/com/google/copybara/StarlarkDateTimeModuleTest.java b/java/javatests/com/google/copybara/StarlarkDateTimeModuleTest.java similarity index 100% rename from javatests/com/google/copybara/StarlarkDateTimeModuleTest.java rename to java/javatests/com/google/copybara/StarlarkDateTimeModuleTest.java diff --git a/javatests/com/google/copybara/StarlarkJsonModuleTest.java b/java/javatests/com/google/copybara/StarlarkJsonModuleTest.java similarity index 100% rename from javatests/com/google/copybara/StarlarkJsonModuleTest.java rename to java/javatests/com/google/copybara/StarlarkJsonModuleTest.java diff --git a/javatests/com/google/copybara/StarlarkRandomModuleTest.java b/java/javatests/com/google/copybara/StarlarkRandomModuleTest.java similarity index 100% rename from javatests/com/google/copybara/StarlarkRandomModuleTest.java rename to java/javatests/com/google/copybara/StarlarkRandomModuleTest.java diff --git a/javatests/com/google/copybara/StructModuleTest.java b/java/javatests/com/google/copybara/StructModuleTest.java similarity index 100% rename from javatests/com/google/copybara/StructModuleTest.java rename to java/javatests/com/google/copybara/StructModuleTest.java diff --git a/javatests/com/google/copybara/TransformWorkTest.java b/java/javatests/com/google/copybara/TransformWorkTest.java similarity index 100% rename from javatests/com/google/copybara/TransformWorkTest.java rename to java/javatests/com/google/copybara/TransformWorkTest.java diff --git a/javatests/com/google/copybara/WorkflowTest.java b/java/javatests/com/google/copybara/WorkflowTest.java similarity index 100% rename from javatests/com/google/copybara/WorkflowTest.java rename to java/javatests/com/google/copybara/WorkflowTest.java diff --git a/javatests/com/google/copybara/archive/ArchiveModuleTest.java b/java/javatests/com/google/copybara/archive/ArchiveModuleTest.java similarity index 100% rename from javatests/com/google/copybara/archive/ArchiveModuleTest.java rename to java/javatests/com/google/copybara/archive/ArchiveModuleTest.java diff --git a/javatests/com/google/copybara/archive/BUILD b/java/javatests/com/google/copybara/archive/BUILD similarity index 100% rename from javatests/com/google/copybara/archive/BUILD rename to java/javatests/com/google/copybara/archive/BUILD diff --git a/javatests/com/google/copybara/archive/util/ArchiveUtilTest.java b/java/javatests/com/google/copybara/archive/util/ArchiveUtilTest.java similarity index 100% rename from javatests/com/google/copybara/archive/util/ArchiveUtilTest.java rename to java/javatests/com/google/copybara/archive/util/ArchiveUtilTest.java diff --git a/javatests/com/google/copybara/archive/util/BUILD b/java/javatests/com/google/copybara/archive/util/BUILD similarity index 100% rename from javatests/com/google/copybara/archive/util/BUILD rename to java/javatests/com/google/copybara/archive/util/BUILD diff --git a/javatests/com/google/copybara/authoring/AuthorParserTest.java b/java/javatests/com/google/copybara/authoring/AuthorParserTest.java similarity index 100% rename from javatests/com/google/copybara/authoring/AuthorParserTest.java rename to java/javatests/com/google/copybara/authoring/AuthorParserTest.java diff --git a/javatests/com/google/copybara/authoring/AuthorTest.java b/java/javatests/com/google/copybara/authoring/AuthorTest.java similarity index 100% rename from javatests/com/google/copybara/authoring/AuthorTest.java rename to java/javatests/com/google/copybara/authoring/AuthorTest.java diff --git a/javatests/com/google/copybara/authoring/AuthoringTest.java b/java/javatests/com/google/copybara/authoring/AuthoringTest.java similarity index 100% rename from javatests/com/google/copybara/authoring/AuthoringTest.java rename to java/javatests/com/google/copybara/authoring/AuthoringTest.java diff --git a/javatests/com/google/copybara/buildozer/BUILD b/java/javatests/com/google/copybara/buildozer/BUILD similarity index 100% rename from javatests/com/google/copybara/buildozer/BUILD rename to java/javatests/com/google/copybara/buildozer/BUILD diff --git a/javatests/com/google/copybara/buildozer/BuildozerBatchTest.java b/java/javatests/com/google/copybara/buildozer/BuildozerBatchTest.java similarity index 100% rename from javatests/com/google/copybara/buildozer/BuildozerBatchTest.java rename to java/javatests/com/google/copybara/buildozer/BuildozerBatchTest.java diff --git a/javatests/com/google/copybara/buildozer/BuildozerCreateTest.java b/java/javatests/com/google/copybara/buildozer/BuildozerCreateTest.java similarity index 100% rename from javatests/com/google/copybara/buildozer/BuildozerCreateTest.java rename to java/javatests/com/google/copybara/buildozer/BuildozerCreateTest.java diff --git a/javatests/com/google/copybara/buildozer/BuildozerDeleteTest.java b/java/javatests/com/google/copybara/buildozer/BuildozerDeleteTest.java similarity index 100% rename from javatests/com/google/copybara/buildozer/BuildozerDeleteTest.java rename to java/javatests/com/google/copybara/buildozer/BuildozerDeleteTest.java diff --git a/javatests/com/google/copybara/buildozer/BuildozerModifyTest.java b/java/javatests/com/google/copybara/buildozer/BuildozerModifyTest.java similarity index 100% rename from javatests/com/google/copybara/buildozer/BuildozerModifyTest.java rename to java/javatests/com/google/copybara/buildozer/BuildozerModifyTest.java diff --git a/javatests/com/google/copybara/buildozer/BuildozerPrintExecutorTest.java b/java/javatests/com/google/copybara/buildozer/BuildozerPrintExecutorTest.java similarity index 100% rename from javatests/com/google/copybara/buildozer/BuildozerPrintExecutorTest.java rename to java/javatests/com/google/copybara/buildozer/BuildozerPrintExecutorTest.java diff --git a/javatests/com/google/copybara/checks/ApiCheckerTest.java b/java/javatests/com/google/copybara/checks/ApiCheckerTest.java similarity index 100% rename from javatests/com/google/copybara/checks/ApiCheckerTest.java rename to java/javatests/com/google/copybara/checks/ApiCheckerTest.java diff --git a/javatests/com/google/copybara/checks/BUILD b/java/javatests/com/google/copybara/checks/BUILD similarity index 100% rename from javatests/com/google/copybara/checks/BUILD rename to java/javatests/com/google/copybara/checks/BUILD diff --git a/javatests/com/google/copybara/config/BUILD b/java/javatests/com/google/copybara/config/BUILD similarity index 100% rename from javatests/com/google/copybara/config/BUILD rename to java/javatests/com/google/copybara/config/BUILD diff --git a/javatests/com/google/copybara/config/CapturingConfigFileTest.java b/java/javatests/com/google/copybara/config/CapturingConfigFileTest.java similarity index 100% rename from javatests/com/google/copybara/config/CapturingConfigFileTest.java rename to java/javatests/com/google/copybara/config/CapturingConfigFileTest.java diff --git a/javatests/com/google/copybara/config/MapConfigFileTest.java b/java/javatests/com/google/copybara/config/MapConfigFileTest.java similarity index 100% rename from javatests/com/google/copybara/config/MapConfigFileTest.java rename to java/javatests/com/google/copybara/config/MapConfigFileTest.java diff --git a/javatests/com/google/copybara/config/PathBasedConfigFileTest.java b/java/javatests/com/google/copybara/config/PathBasedConfigFileTest.java similarity index 100% rename from javatests/com/google/copybara/config/PathBasedConfigFileTest.java rename to java/javatests/com/google/copybara/config/PathBasedConfigFileTest.java diff --git a/javatests/com/google/copybara/config/ResolveDelegateConfigFileTest.java b/java/javatests/com/google/copybara/config/ResolveDelegateConfigFileTest.java similarity index 100% rename from javatests/com/google/copybara/config/ResolveDelegateConfigFileTest.java rename to java/javatests/com/google/copybara/config/ResolveDelegateConfigFileTest.java diff --git a/javatests/com/google/copybara/config/SkylarkParserTest.java b/java/javatests/com/google/copybara/config/SkylarkParserTest.java similarity index 100% rename from javatests/com/google/copybara/config/SkylarkParserTest.java rename to java/javatests/com/google/copybara/config/SkylarkParserTest.java diff --git a/javatests/com/google/copybara/config/SkylarkUtilTest.java b/java/javatests/com/google/copybara/config/SkylarkUtilTest.java similarity index 100% rename from javatests/com/google/copybara/config/SkylarkUtilTest.java rename to java/javatests/com/google/copybara/config/SkylarkUtilTest.java diff --git a/javatests/com/google/copybara/config/ValidationResultTest.java b/java/javatests/com/google/copybara/config/ValidationResultTest.java similarity index 100% rename from javatests/com/google/copybara/config/ValidationResultTest.java rename to java/javatests/com/google/copybara/config/ValidationResultTest.java diff --git a/javatests/com/google/copybara/config/base/BUILD b/java/javatests/com/google/copybara/config/base/BUILD similarity index 100% rename from javatests/com/google/copybara/config/base/BUILD rename to java/javatests/com/google/copybara/config/base/BUILD diff --git a/javatests/com/google/copybara/config/base/SkylarkUtilTest.java b/java/javatests/com/google/copybara/config/base/SkylarkUtilTest.java similarity index 100% rename from javatests/com/google/copybara/config/base/SkylarkUtilTest.java rename to java/javatests/com/google/copybara/config/base/SkylarkUtilTest.java diff --git a/javatests/com/google/copybara/configgen/BUILD b/java/javatests/com/google/copybara/configgen/BUILD similarity index 100% rename from javatests/com/google/copybara/configgen/BUILD rename to java/javatests/com/google/copybara/configgen/BUILD diff --git a/javatests/com/google/copybara/configgen/ConfigGenHeuristicsTest.java b/java/javatests/com/google/copybara/configgen/ConfigGenHeuristicsTest.java similarity index 100% rename from javatests/com/google/copybara/configgen/ConfigGenHeuristicsTest.java rename to java/javatests/com/google/copybara/configgen/ConfigGenHeuristicsTest.java diff --git a/javatests/com/google/copybara/credentials/BUILD b/java/javatests/com/google/copybara/credentials/BUILD similarity index 100% rename from javatests/com/google/copybara/credentials/BUILD rename to java/javatests/com/google/copybara/credentials/BUILD diff --git a/javatests/com/google/copybara/credentials/CredentialModuleTest.java b/java/javatests/com/google/copybara/credentials/CredentialModuleTest.java similarity index 100% rename from javatests/com/google/copybara/credentials/CredentialModuleTest.java rename to java/javatests/com/google/copybara/credentials/CredentialModuleTest.java diff --git a/javatests/com/google/copybara/credentials/TtlSecretTest.java b/java/javatests/com/google/copybara/credentials/TtlSecretTest.java similarity index 100% rename from javatests/com/google/copybara/credentials/TtlSecretTest.java rename to java/javatests/com/google/copybara/credentials/TtlSecretTest.java diff --git a/javatests/com/google/copybara/exception/BUILD b/java/javatests/com/google/copybara/exception/BUILD similarity index 100% rename from javatests/com/google/copybara/exception/BUILD rename to java/javatests/com/google/copybara/exception/BUILD diff --git a/javatests/com/google/copybara/exception/ValidationExceptionTest.java b/java/javatests/com/google/copybara/exception/ValidationExceptionTest.java similarity index 100% rename from javatests/com/google/copybara/exception/ValidationExceptionTest.java rename to java/javatests/com/google/copybara/exception/ValidationExceptionTest.java diff --git a/javatests/com/google/copybara/folder/FolderDestinationReaderTest.java b/java/javatests/com/google/copybara/folder/FolderDestinationReaderTest.java similarity index 100% rename from javatests/com/google/copybara/folder/FolderDestinationReaderTest.java rename to java/javatests/com/google/copybara/folder/FolderDestinationReaderTest.java diff --git a/javatests/com/google/copybara/folder/FolderDestinationTest.java b/java/javatests/com/google/copybara/folder/FolderDestinationTest.java similarity index 100% rename from javatests/com/google/copybara/folder/FolderDestinationTest.java rename to java/javatests/com/google/copybara/folder/FolderDestinationTest.java diff --git a/javatests/com/google/copybara/folder/FolderOriginTest.java b/java/javatests/com/google/copybara/folder/FolderOriginTest.java similarity index 100% rename from javatests/com/google/copybara/folder/FolderOriginTest.java rename to java/javatests/com/google/copybara/folder/FolderOriginTest.java diff --git a/javatests/com/google/copybara/format/BUILD b/java/javatests/com/google/copybara/format/BUILD similarity index 100% rename from javatests/com/google/copybara/format/BUILD rename to java/javatests/com/google/copybara/format/BUILD diff --git a/javatests/com/google/copybara/format/BuildifierFormatTest.java b/java/javatests/com/google/copybara/format/BuildifierFormatTest.java similarity index 100% rename from javatests/com/google/copybara/format/BuildifierFormatTest.java rename to java/javatests/com/google/copybara/format/BuildifierFormatTest.java diff --git a/javatests/com/google/copybara/git/BUILD b/java/javatests/com/google/copybara/git/BUILD similarity index 100% rename from javatests/com/google/copybara/git/BUILD rename to java/javatests/com/google/copybara/git/BUILD diff --git a/javatests/com/google/copybara/git/CredentialFileHandlerTest.java b/java/javatests/com/google/copybara/git/CredentialFileHandlerTest.java similarity index 100% rename from javatests/com/google/copybara/git/CredentialFileHandlerTest.java rename to java/javatests/com/google/copybara/git/CredentialFileHandlerTest.java diff --git a/javatests/com/google/copybara/git/FuzzyClosestVersionSelectorTest.java b/java/javatests/com/google/copybara/git/FuzzyClosestVersionSelectorTest.java similarity index 100% rename from javatests/com/google/copybara/git/FuzzyClosestVersionSelectorTest.java rename to java/javatests/com/google/copybara/git/FuzzyClosestVersionSelectorTest.java diff --git a/javatests/com/google/copybara/git/GerritDestinationTest.java b/java/javatests/com/google/copybara/git/GerritDestinationTest.java similarity index 100% rename from javatests/com/google/copybara/git/GerritDestinationTest.java rename to java/javatests/com/google/copybara/git/GerritDestinationTest.java diff --git a/javatests/com/google/copybara/git/GerritEndpointTest.java b/java/javatests/com/google/copybara/git/GerritEndpointTest.java similarity index 100% rename from javatests/com/google/copybara/git/GerritEndpointTest.java rename to java/javatests/com/google/copybara/git/GerritEndpointTest.java diff --git a/javatests/com/google/copybara/git/GerritOptionsTest.java b/java/javatests/com/google/copybara/git/GerritOptionsTest.java similarity index 100% rename from javatests/com/google/copybara/git/GerritOptionsTest.java rename to java/javatests/com/google/copybara/git/GerritOptionsTest.java diff --git a/javatests/com/google/copybara/git/GerritOriginTest.java b/java/javatests/com/google/copybara/git/GerritOriginTest.java similarity index 100% rename from javatests/com/google/copybara/git/GerritOriginTest.java rename to java/javatests/com/google/copybara/git/GerritOriginTest.java diff --git a/javatests/com/google/copybara/git/GerritTriggerTest.java b/java/javatests/com/google/copybara/git/GerritTriggerTest.java similarity index 100% rename from javatests/com/google/copybara/git/GerritTriggerTest.java rename to java/javatests/com/google/copybara/git/GerritTriggerTest.java diff --git a/javatests/com/google/copybara/git/GitCredentialTest.java b/java/javatests/com/google/copybara/git/GitCredentialTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitCredentialTest.java rename to java/javatests/com/google/copybara/git/GitCredentialTest.java diff --git a/javatests/com/google/copybara/git/GitDestinationIntegrateTest.java b/java/javatests/com/google/copybara/git/GitDestinationIntegrateTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitDestinationIntegrateTest.java rename to java/javatests/com/google/copybara/git/GitDestinationIntegrateTest.java diff --git a/javatests/com/google/copybara/git/GitDestinationReaderTest.java b/java/javatests/com/google/copybara/git/GitDestinationReaderTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitDestinationReaderTest.java rename to java/javatests/com/google/copybara/git/GitDestinationReaderTest.java diff --git a/javatests/com/google/copybara/git/GitDestinationTest.java b/java/javatests/com/google/copybara/git/GitDestinationTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitDestinationTest.java rename to java/javatests/com/google/copybara/git/GitDestinationTest.java diff --git a/javatests/com/google/copybara/git/GitEnvironmentTest.java b/java/javatests/com/google/copybara/git/GitEnvironmentTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitEnvironmentTest.java rename to java/javatests/com/google/copybara/git/GitEnvironmentTest.java diff --git a/javatests/com/google/copybara/git/GitHubDestinationTest.java b/java/javatests/com/google/copybara/git/GitHubDestinationTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubDestinationTest.java rename to java/javatests/com/google/copybara/git/GitHubDestinationTest.java diff --git a/javatests/com/google/copybara/git/GitHubEndpointTest.java b/java/javatests/com/google/copybara/git/GitHubEndpointTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubEndpointTest.java rename to java/javatests/com/google/copybara/git/GitHubEndpointTest.java diff --git a/javatests/com/google/copybara/git/GitHubOptionsTest.java b/java/javatests/com/google/copybara/git/GitHubOptionsTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubOptionsTest.java rename to java/javatests/com/google/copybara/git/GitHubOptionsTest.java diff --git a/javatests/com/google/copybara/git/GitHubPostSubmitApprovalsProviderTest.java b/java/javatests/com/google/copybara/git/GitHubPostSubmitApprovalsProviderTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubPostSubmitApprovalsProviderTest.java rename to java/javatests/com/google/copybara/git/GitHubPostSubmitApprovalsProviderTest.java diff --git a/javatests/com/google/copybara/git/GitHubPrDestinationTest.java b/java/javatests/com/google/copybara/git/GitHubPrDestinationTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubPrDestinationTest.java rename to java/javatests/com/google/copybara/git/GitHubPrDestinationTest.java diff --git a/javatests/com/google/copybara/git/GitHubPrIntegrateLabelTest.java b/java/javatests/com/google/copybara/git/GitHubPrIntegrateLabelTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubPrIntegrateLabelTest.java rename to java/javatests/com/google/copybara/git/GitHubPrIntegrateLabelTest.java diff --git a/javatests/com/google/copybara/git/GitHubPrOriginTest.java b/java/javatests/com/google/copybara/git/GitHubPrOriginTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubPrOriginTest.java rename to java/javatests/com/google/copybara/git/GitHubPrOriginTest.java diff --git a/javatests/com/google/copybara/git/GitHubPreSubmitApprovalsProviderTest.java b/java/javatests/com/google/copybara/git/GitHubPreSubmitApprovalsProviderTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubPreSubmitApprovalsProviderTest.java rename to java/javatests/com/google/copybara/git/GitHubPreSubmitApprovalsProviderTest.java diff --git a/javatests/com/google/copybara/git/GitHubRepositoryHookTest.java b/java/javatests/com/google/copybara/git/GitHubRepositoryHookTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubRepositoryHookTest.java rename to java/javatests/com/google/copybara/git/GitHubRepositoryHookTest.java diff --git a/javatests/com/google/copybara/git/GitHubSecuritySettingsValidatorTest.java b/java/javatests/com/google/copybara/git/GitHubSecuritySettingsValidatorTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubSecuritySettingsValidatorTest.java rename to java/javatests/com/google/copybara/git/GitHubSecuritySettingsValidatorTest.java diff --git a/javatests/com/google/copybara/git/GitHubTriggerTest.java b/java/javatests/com/google/copybara/git/GitHubTriggerTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubTriggerTest.java rename to java/javatests/com/google/copybara/git/GitHubTriggerTest.java diff --git a/javatests/com/google/copybara/git/GitHubUserApprovalsValidatorTest.java b/java/javatests/com/google/copybara/git/GitHubUserApprovalsValidatorTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitHubUserApprovalsValidatorTest.java rename to java/javatests/com/google/copybara/git/GitHubUserApprovalsValidatorTest.java diff --git a/javatests/com/google/copybara/git/GitLabMrDestinationTest.java b/java/javatests/com/google/copybara/git/GitLabMrDestinationTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitLabMrDestinationTest.java rename to java/javatests/com/google/copybara/git/GitLabMrDestinationTest.java diff --git a/javatests/com/google/copybara/git/GitLabMrOriginTest.java b/java/javatests/com/google/copybara/git/GitLabMrOriginTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitLabMrOriginTest.java rename to java/javatests/com/google/copybara/git/GitLabMrOriginTest.java diff --git a/javatests/com/google/copybara/git/GitLabMrWriteHookTest.java b/java/javatests/com/google/copybara/git/GitLabMrWriteHookTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitLabMrWriteHookTest.java rename to java/javatests/com/google/copybara/git/GitLabMrWriteHookTest.java diff --git a/javatests/com/google/copybara/git/GitLabMrWriterTest.java b/java/javatests/com/google/copybara/git/GitLabMrWriterTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitLabMrWriterTest.java rename to java/javatests/com/google/copybara/git/GitLabMrWriterTest.java diff --git a/javatests/com/google/copybara/git/GitMirrorTest.java b/java/javatests/com/google/copybara/git/GitMirrorTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitMirrorTest.java rename to java/javatests/com/google/copybara/git/GitMirrorTest.java diff --git a/javatests/com/google/copybara/git/GitOriginSubmodulesTest.java b/java/javatests/com/google/copybara/git/GitOriginSubmodulesTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitOriginSubmodulesTest.java rename to java/javatests/com/google/copybara/git/GitOriginSubmodulesTest.java diff --git a/javatests/com/google/copybara/git/GitOriginTest.java b/java/javatests/com/google/copybara/git/GitOriginTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitOriginTest.java rename to java/javatests/com/google/copybara/git/GitOriginTest.java diff --git a/javatests/com/google/copybara/git/GitRepoTypeTest.java b/java/javatests/com/google/copybara/git/GitRepoTypeTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitRepoTypeTest.java rename to java/javatests/com/google/copybara/git/GitRepoTypeTest.java diff --git a/javatests/com/google/copybara/git/GitRepositoryTest.java b/java/javatests/com/google/copybara/git/GitRepositoryTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitRepositoryTest.java rename to java/javatests/com/google/copybara/git/GitRepositoryTest.java diff --git a/javatests/com/google/copybara/git/GitRevisionTest.java b/java/javatests/com/google/copybara/git/GitRevisionTest.java similarity index 100% rename from javatests/com/google/copybara/git/GitRevisionTest.java rename to java/javatests/com/google/copybara/git/GitRevisionTest.java diff --git a/javatests/com/google/copybara/git/LatestVersionSelectorForGitTest.java b/java/javatests/com/google/copybara/git/LatestVersionSelectorForGitTest.java similarity index 100% rename from javatests/com/google/copybara/git/LatestVersionSelectorForGitTest.java rename to java/javatests/com/google/copybara/git/LatestVersionSelectorForGitTest.java diff --git a/javatests/com/google/copybara/git/RefspecTest.java b/java/javatests/com/google/copybara/git/RefspecTest.java similarity index 100% rename from javatests/com/google/copybara/git/RefspecTest.java rename to java/javatests/com/google/copybara/git/RefspecTest.java diff --git a/javatests/com/google/copybara/git/SameGitTreeTest.java b/java/javatests/com/google/copybara/git/SameGitTreeTest.java similarity index 100% rename from javatests/com/google/copybara/git/SameGitTreeTest.java rename to java/javatests/com/google/copybara/git/SameGitTreeTest.java diff --git a/javatests/com/google/copybara/git/SubmodulesInDestinationTest.java b/java/javatests/com/google/copybara/git/SubmodulesInDestinationTest.java similarity index 100% rename from javatests/com/google/copybara/git/SubmodulesInDestinationTest.java rename to java/javatests/com/google/copybara/git/SubmodulesInDestinationTest.java diff --git a/javatests/com/google/copybara/git/gerritapi/GerritApiTest.java b/java/javatests/com/google/copybara/git/gerritapi/GerritApiTest.java similarity index 100% rename from javatests/com/google/copybara/git/gerritapi/GerritApiTest.java rename to java/javatests/com/google/copybara/git/gerritapi/GerritApiTest.java diff --git a/javatests/com/google/copybara/git/gerritapi/GerritApiTransportWithCheckerTest.java b/java/javatests/com/google/copybara/git/gerritapi/GerritApiTransportWithCheckerTest.java similarity index 100% rename from javatests/com/google/copybara/git/gerritapi/GerritApiTransportWithCheckerTest.java rename to java/javatests/com/google/copybara/git/gerritapi/GerritApiTransportWithCheckerTest.java diff --git a/javatests/com/google/copybara/git/github/api/GitHubApiTest.java b/java/javatests/com/google/copybara/git/github/api/GitHubApiTest.java similarity index 100% rename from javatests/com/google/copybara/git/github/api/GitHubApiTest.java rename to java/javatests/com/google/copybara/git/github/api/GitHubApiTest.java diff --git a/javatests/com/google/copybara/git/github/api/GitHubApiTransportImplTest.java b/java/javatests/com/google/copybara/git/github/api/GitHubApiTransportImplTest.java similarity index 100% rename from javatests/com/google/copybara/git/github/api/GitHubApiTransportImplTest.java rename to java/javatests/com/google/copybara/git/github/api/GitHubApiTransportImplTest.java diff --git a/javatests/com/google/copybara/git/github/api/GitHubApiTransportWithCheckerTest.java b/java/javatests/com/google/copybara/git/github/api/GitHubApiTransportWithCheckerTest.java similarity index 100% rename from javatests/com/google/copybara/git/github/api/GitHubApiTransportWithCheckerTest.java rename to java/javatests/com/google/copybara/git/github/api/GitHubApiTransportWithCheckerTest.java diff --git a/javatests/com/google/copybara/git/github/api/GitHubGraphQLTest.java b/java/javatests/com/google/copybara/git/github/api/GitHubGraphQLTest.java similarity index 100% rename from javatests/com/google/copybara/git/github/api/GitHubGraphQLTest.java rename to java/javatests/com/google/copybara/git/github/api/GitHubGraphQLTest.java diff --git a/javatests/com/google/copybara/git/github/util/BUILD b/java/javatests/com/google/copybara/git/github/util/BUILD similarity index 100% rename from javatests/com/google/copybara/git/github/util/BUILD rename to java/javatests/com/google/copybara/git/github/util/BUILD diff --git a/javatests/com/google/copybara/git/github/util/GitHubHostTest.java b/java/javatests/com/google/copybara/git/github/util/GitHubHostTest.java similarity index 100% rename from javatests/com/google/copybara/git/github/util/GitHubHostTest.java rename to java/javatests/com/google/copybara/git/github/util/GitHubHostTest.java diff --git a/javatests/com/google/copybara/git/github/util/GitHubIdentifierTest.java b/java/javatests/com/google/copybara/git/github/util/GitHubIdentifierTest.java similarity index 100% rename from javatests/com/google/copybara/git/github/util/GitHubIdentifierTest.java rename to java/javatests/com/google/copybara/git/github/util/GitHubIdentifierTest.java diff --git a/javatests/com/google/copybara/git/github/util/GitHubUtilTest.java b/java/javatests/com/google/copybara/git/github/util/GitHubUtilTest.java similarity index 100% rename from javatests/com/google/copybara/git/github/util/GitHubUtilTest.java rename to java/javatests/com/google/copybara/git/github/util/GitHubUtilTest.java diff --git a/javatests/com/google/copybara/git/gitlab/BUILD b/java/javatests/com/google/copybara/git/gitlab/BUILD similarity index 100% rename from javatests/com/google/copybara/git/gitlab/BUILD rename to java/javatests/com/google/copybara/git/gitlab/BUILD diff --git a/javatests/com/google/copybara/git/gitlab/GitLabOptionsTest.java b/java/javatests/com/google/copybara/git/gitlab/GitLabOptionsTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/GitLabOptionsTest.java rename to java/javatests/com/google/copybara/git/gitlab/GitLabOptionsTest.java diff --git a/javatests/com/google/copybara/git/gitlab/GitLabUtilTest.java b/java/javatests/com/google/copybara/git/gitlab/GitLabUtilTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/GitLabUtilTest.java rename to java/javatests/com/google/copybara/git/gitlab/GitLabUtilTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/BUILD b/java/javatests/com/google/copybara/git/gitlab/api/BUILD similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/BUILD rename to java/javatests/com/google/copybara/git/gitlab/api/BUILD diff --git a/javatests/com/google/copybara/git/gitlab/api/GitLabApiTest.java b/java/javatests/com/google/copybara/git/gitlab/api/GitLabApiTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/GitLabApiTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/GitLabApiTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/GitLabApiTransportImplTest.java b/java/javatests/com/google/copybara/git/gitlab/api/GitLabApiTransportImplTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/GitLabApiTransportImplTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/GitLabApiTransportImplTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/BUILD b/java/javatests/com/google/copybara/git/gitlab/api/entities/BUILD similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/BUILD rename to java/javatests/com/google/copybara/git/gitlab/api/entities/BUILD diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/CreateMergeRequestParamsTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/CreateMergeRequestParamsTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/CreateMergeRequestParamsTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/CreateMergeRequestParamsTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/ExternalStatusCheckTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/ExternalStatusCheckTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/ExternalStatusCheckTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/ExternalStatusCheckTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/GitLabApiParamsTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/GitLabApiParamsTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/GitLabApiParamsTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/GitLabApiParamsTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/ListProjectMergeRequestParamsTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/ListProjectMergeRequestParamsTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/ListProjectMergeRequestParamsTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/ListProjectMergeRequestParamsTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/ListUsersParamsTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/ListUsersParamsTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/ListUsersParamsTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/ListUsersParamsTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/MergeRequestTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/MergeRequestTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/MergeRequestTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/MergeRequestTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/PaginatedPageListTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/PaginatedPageListTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/PaginatedPageListTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/PaginatedPageListTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/ProjectTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/ProjectTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/ProjectTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/ProjectTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckParamsTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckParamsTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckParamsTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckParamsTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckResponseTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckResponseTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckResponseTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/SetExternalStatusCheckResponseTest.java diff --git a/javatests/com/google/copybara/git/gitlab/api/entities/UpdateMergeRequestParamsTest.java b/java/javatests/com/google/copybara/git/gitlab/api/entities/UpdateMergeRequestParamsTest.java similarity index 100% rename from javatests/com/google/copybara/git/gitlab/api/entities/UpdateMergeRequestParamsTest.java rename to java/javatests/com/google/copybara/git/gitlab/api/entities/UpdateMergeRequestParamsTest.java diff --git a/javatests/com/google/copybara/go/BUILD b/java/javatests/com/google/copybara/go/BUILD similarity index 100% rename from javatests/com/google/copybara/go/BUILD rename to java/javatests/com/google/copybara/go/BUILD diff --git a/javatests/com/google/copybara/go/GoProxyVersioningTest.java b/java/javatests/com/google/copybara/go/GoProxyVersioningTest.java similarity index 100% rename from javatests/com/google/copybara/go/GoProxyVersioningTest.java rename to java/javatests/com/google/copybara/go/GoProxyVersioningTest.java diff --git a/javatests/com/google/copybara/go/PseudoVersionSelectorTest.java b/java/javatests/com/google/copybara/go/PseudoVersionSelectorTest.java similarity index 100% rename from javatests/com/google/copybara/go/PseudoVersionSelectorTest.java rename to java/javatests/com/google/copybara/go/PseudoVersionSelectorTest.java diff --git a/javatests/com/google/copybara/hashing/BUILD b/java/javatests/com/google/copybara/hashing/BUILD similarity index 100% rename from javatests/com/google/copybara/hashing/BUILD rename to java/javatests/com/google/copybara/hashing/BUILD diff --git a/javatests/com/google/copybara/hashing/HashingModuleTest.java b/java/javatests/com/google/copybara/hashing/HashingModuleTest.java similarity index 100% rename from javatests/com/google/copybara/hashing/HashingModuleTest.java rename to java/javatests/com/google/copybara/hashing/HashingModuleTest.java diff --git a/javatests/com/google/copybara/hg/BUILD b/java/javatests/com/google/copybara/hg/BUILD similarity index 100% rename from javatests/com/google/copybara/hg/BUILD rename to java/javatests/com/google/copybara/hg/BUILD diff --git a/javatests/com/google/copybara/hg/HgDestinationTest.java b/java/javatests/com/google/copybara/hg/HgDestinationTest.java similarity index 100% rename from javatests/com/google/copybara/hg/HgDestinationTest.java rename to java/javatests/com/google/copybara/hg/HgDestinationTest.java diff --git a/javatests/com/google/copybara/hg/HgOriginTest.java b/java/javatests/com/google/copybara/hg/HgOriginTest.java similarity index 100% rename from javatests/com/google/copybara/hg/HgOriginTest.java rename to java/javatests/com/google/copybara/hg/HgOriginTest.java diff --git a/javatests/com/google/copybara/hg/HgRepositoryTest.java b/java/javatests/com/google/copybara/hg/HgRepositoryTest.java similarity index 100% rename from javatests/com/google/copybara/hg/HgRepositoryTest.java rename to java/javatests/com/google/copybara/hg/HgRepositoryTest.java diff --git a/javatests/com/google/copybara/html/BUILD b/java/javatests/com/google/copybara/html/BUILD similarity index 100% rename from javatests/com/google/copybara/html/BUILD rename to java/javatests/com/google/copybara/html/BUILD diff --git a/javatests/com/google/copybara/html/HtmlTest.java b/java/javatests/com/google/copybara/html/HtmlTest.java similarity index 100% rename from javatests/com/google/copybara/html/HtmlTest.java rename to java/javatests/com/google/copybara/html/HtmlTest.java diff --git a/javatests/com/google/copybara/http/BUILD b/java/javatests/com/google/copybara/http/BUILD similarity index 100% rename from javatests/com/google/copybara/http/BUILD rename to java/javatests/com/google/copybara/http/BUILD diff --git a/javatests/com/google/copybara/http/HttpModuleTest.java b/java/javatests/com/google/copybara/http/HttpModuleTest.java similarity index 100% rename from javatests/com/google/copybara/http/HttpModuleTest.java rename to java/javatests/com/google/copybara/http/HttpModuleTest.java diff --git a/javatests/com/google/copybara/http/auth/AuthTest.java b/java/javatests/com/google/copybara/http/auth/AuthTest.java similarity index 100% rename from javatests/com/google/copybara/http/auth/AuthTest.java rename to java/javatests/com/google/copybara/http/auth/AuthTest.java diff --git a/javatests/com/google/copybara/http/auth/BUILD b/java/javatests/com/google/copybara/http/auth/BUILD similarity index 100% rename from javatests/com/google/copybara/http/auth/BUILD rename to java/javatests/com/google/copybara/http/auth/BUILD diff --git a/javatests/com/google/copybara/http/endpoint/BUILD b/java/javatests/com/google/copybara/http/endpoint/BUILD similarity index 100% rename from javatests/com/google/copybara/http/endpoint/BUILD rename to java/javatests/com/google/copybara/http/endpoint/BUILD diff --git a/javatests/com/google/copybara/http/endpoint/HttpEndpointTest.java b/java/javatests/com/google/copybara/http/endpoint/HttpEndpointTest.java similarity index 100% rename from javatests/com/google/copybara/http/endpoint/HttpEndpointTest.java rename to java/javatests/com/google/copybara/http/endpoint/HttpEndpointTest.java diff --git a/javatests/com/google/copybara/http/json/BUILD b/java/javatests/com/google/copybara/http/json/BUILD similarity index 100% rename from javatests/com/google/copybara/http/json/BUILD rename to java/javatests/com/google/copybara/http/json/BUILD diff --git a/javatests/com/google/copybara/http/json/HttpEndpointJsonContentTest.java b/java/javatests/com/google/copybara/http/json/HttpEndpointJsonContentTest.java similarity index 100% rename from javatests/com/google/copybara/http/json/HttpEndpointJsonContentTest.java rename to java/javatests/com/google/copybara/http/json/HttpEndpointJsonContentTest.java diff --git a/javatests/com/google/copybara/http/multipart/BUILD b/java/javatests/com/google/copybara/http/multipart/BUILD similarity index 100% rename from javatests/com/google/copybara/http/multipart/BUILD rename to java/javatests/com/google/copybara/http/multipart/BUILD diff --git a/javatests/com/google/copybara/http/multipart/HttpEndpointMultipartContentTest.java b/java/javatests/com/google/copybara/http/multipart/HttpEndpointMultipartContentTest.java similarity index 100% rename from javatests/com/google/copybara/http/multipart/HttpEndpointMultipartContentTest.java rename to java/javatests/com/google/copybara/http/multipart/HttpEndpointMultipartContentTest.java diff --git a/javatests/com/google/copybara/http/multipart/HttpEndpointUrlEncodedFormContentTest.java b/java/javatests/com/google/copybara/http/multipart/HttpEndpointUrlEncodedFormContentTest.java similarity index 100% rename from javatests/com/google/copybara/http/multipart/HttpEndpointUrlEncodedFormContentTest.java rename to java/javatests/com/google/copybara/http/multipart/HttpEndpointUrlEncodedFormContentTest.java diff --git a/javatests/com/google/copybara/jcommander/BUILD b/java/javatests/com/google/copybara/jcommander/BUILD similarity index 100% rename from javatests/com/google/copybara/jcommander/BUILD rename to java/javatests/com/google/copybara/jcommander/BUILD diff --git a/javatests/com/google/copybara/jcommander/DurationConverterTest.java b/java/javatests/com/google/copybara/jcommander/DurationConverterTest.java similarity index 100% rename from javatests/com/google/copybara/jcommander/DurationConverterTest.java rename to java/javatests/com/google/copybara/jcommander/DurationConverterTest.java diff --git a/javatests/com/google/copybara/jcommander/GlobConverterTest.java b/java/javatests/com/google/copybara/jcommander/GlobConverterTest.java similarity index 100% rename from javatests/com/google/copybara/jcommander/GlobConverterTest.java rename to java/javatests/com/google/copybara/jcommander/GlobConverterTest.java diff --git a/javatests/com/google/copybara/jcommander/SemicolonListConverterTest.java b/java/javatests/com/google/copybara/jcommander/SemicolonListConverterTest.java similarity index 100% rename from javatests/com/google/copybara/jcommander/SemicolonListConverterTest.java rename to java/javatests/com/google/copybara/jcommander/SemicolonListConverterTest.java diff --git a/javatests/com/google/copybara/json/BUILD b/java/javatests/com/google/copybara/json/BUILD similarity index 100% rename from javatests/com/google/copybara/json/BUILD rename to java/javatests/com/google/copybara/json/BUILD diff --git a/javatests/com/google/copybara/json/GsonParserUtilTest.java b/java/javatests/com/google/copybara/json/GsonParserUtilTest.java similarity index 100% rename from javatests/com/google/copybara/json/GsonParserUtilTest.java rename to java/javatests/com/google/copybara/json/GsonParserUtilTest.java diff --git a/javatests/com/google/copybara/monitor/BUILD b/java/javatests/com/google/copybara/monitor/BUILD similarity index 100% rename from javatests/com/google/copybara/monitor/BUILD rename to java/javatests/com/google/copybara/monitor/BUILD diff --git a/javatests/com/google/copybara/monitor/ConsoleEventMonitorTest.java b/java/javatests/com/google/copybara/monitor/ConsoleEventMonitorTest.java similarity index 100% rename from javatests/com/google/copybara/monitor/ConsoleEventMonitorTest.java rename to java/javatests/com/google/copybara/monitor/ConsoleEventMonitorTest.java diff --git a/javatests/com/google/copybara/onboard/BUILD b/java/javatests/com/google/copybara/onboard/BUILD similarity index 100% rename from javatests/com/google/copybara/onboard/BUILD rename to java/javatests/com/google/copybara/onboard/BUILD diff --git a/javatests/com/google/copybara/onboard/CommandLineGuideTest.java b/java/javatests/com/google/copybara/onboard/CommandLineGuideTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/CommandLineGuideTest.java rename to java/javatests/com/google/copybara/onboard/CommandLineGuideTest.java diff --git a/javatests/com/google/copybara/onboard/ConfigBuilderTest.java b/java/javatests/com/google/copybara/onboard/ConfigBuilderTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/ConfigBuilderTest.java rename to java/javatests/com/google/copybara/onboard/ConfigBuilderTest.java diff --git a/javatests/com/google/copybara/onboard/ConfigHeuristicsInputProviderTest.java b/java/javatests/com/google/copybara/onboard/ConfigHeuristicsInputProviderTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/ConfigHeuristicsInputProviderTest.java rename to java/javatests/com/google/copybara/onboard/ConfigHeuristicsInputProviderTest.java diff --git a/javatests/com/google/copybara/onboard/GeneratorCmdTest.java b/java/javatests/com/google/copybara/onboard/GeneratorCmdTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/GeneratorCmdTest.java rename to java/javatests/com/google/copybara/onboard/GeneratorCmdTest.java diff --git a/javatests/com/google/copybara/onboard/GitToGitGeneratorTest.java b/java/javatests/com/google/copybara/onboard/GitToGitGeneratorTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/GitToGitGeneratorTest.java rename to java/javatests/com/google/copybara/onboard/GitToGitGeneratorTest.java diff --git a/javatests/com/google/copybara/onboard/InputsTest.java b/java/javatests/com/google/copybara/onboard/InputsTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/InputsTest.java rename to java/javatests/com/google/copybara/onboard/InputsTest.java diff --git a/javatests/com/google/copybara/onboard/OnboardCmdTest.java b/java/javatests/com/google/copybara/onboard/OnboardCmdTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/OnboardCmdTest.java rename to java/javatests/com/google/copybara/onboard/OnboardCmdTest.java diff --git a/javatests/com/google/copybara/onboard/StarlarkConverterTest.java b/java/javatests/com/google/copybara/onboard/StarlarkConverterTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/StarlarkConverterTest.java rename to java/javatests/com/google/copybara/onboard/StarlarkConverterTest.java diff --git a/javatests/com/google/copybara/onboard/core/AskInputProviderTest.java b/java/javatests/com/google/copybara/onboard/core/AskInputProviderTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/core/AskInputProviderTest.java rename to java/javatests/com/google/copybara/onboard/core/AskInputProviderTest.java diff --git a/javatests/com/google/copybara/onboard/core/BUILD b/java/javatests/com/google/copybara/onboard/core/BUILD similarity index 100% rename from javatests/com/google/copybara/onboard/core/BUILD rename to java/javatests/com/google/copybara/onboard/core/BUILD diff --git a/javatests/com/google/copybara/onboard/core/CachedInputProviderTest.java b/java/javatests/com/google/copybara/onboard/core/CachedInputProviderTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/core/CachedInputProviderTest.java rename to java/javatests/com/google/copybara/onboard/core/CachedInputProviderTest.java diff --git a/javatests/com/google/copybara/onboard/core/ConstantProviderTest.java b/java/javatests/com/google/copybara/onboard/core/ConstantProviderTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/core/ConstantProviderTest.java rename to java/javatests/com/google/copybara/onboard/core/ConstantProviderTest.java diff --git a/javatests/com/google/copybara/onboard/core/InputProviderResolverImplTest.java b/java/javatests/com/google/copybara/onboard/core/InputProviderResolverImplTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/core/InputProviderResolverImplTest.java rename to java/javatests/com/google/copybara/onboard/core/InputProviderResolverImplTest.java diff --git a/javatests/com/google/copybara/onboard/core/InputTest.java b/java/javatests/com/google/copybara/onboard/core/InputTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/core/InputTest.java rename to java/javatests/com/google/copybara/onboard/core/InputTest.java diff --git a/javatests/com/google/copybara/onboard/core/MapBasedInputProviderTest.java b/java/javatests/com/google/copybara/onboard/core/MapBasedInputProviderTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/core/MapBasedInputProviderTest.java rename to java/javatests/com/google/copybara/onboard/core/MapBasedInputProviderTest.java diff --git a/javatests/com/google/copybara/onboard/core/PrioritizedInputProviderTest.java b/java/javatests/com/google/copybara/onboard/core/PrioritizedInputProviderTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/core/PrioritizedInputProviderTest.java rename to java/javatests/com/google/copybara/onboard/core/PrioritizedInputProviderTest.java diff --git a/javatests/com/google/copybara/onboard/core/template/TemplateConfigGeneratorTest.java b/java/javatests/com/google/copybara/onboard/core/template/TemplateConfigGeneratorTest.java similarity index 100% rename from javatests/com/google/copybara/onboard/core/template/TemplateConfigGeneratorTest.java rename to java/javatests/com/google/copybara/onboard/core/template/TemplateConfigGeneratorTest.java diff --git a/javatests/com/google/copybara/profiler/BUILD b/java/javatests/com/google/copybara/profiler/BUILD similarity index 100% rename from javatests/com/google/copybara/profiler/BUILD rename to java/javatests/com/google/copybara/profiler/BUILD diff --git a/javatests/com/google/copybara/profiler/ConsoleProfilerListenerTest.java b/java/javatests/com/google/copybara/profiler/ConsoleProfilerListenerTest.java similarity index 100% rename from javatests/com/google/copybara/profiler/ConsoleProfilerListenerTest.java rename to java/javatests/com/google/copybara/profiler/ConsoleProfilerListenerTest.java diff --git a/javatests/com/google/copybara/profiler/ProfilerTest.java b/java/javatests/com/google/copybara/profiler/ProfilerTest.java similarity index 100% rename from javatests/com/google/copybara/profiler/ProfilerTest.java rename to java/javatests/com/google/copybara/profiler/ProfilerTest.java diff --git a/javatests/com/google/copybara/python/BUILD b/java/javatests/com/google/copybara/python/BUILD similarity index 100% rename from javatests/com/google/copybara/python/BUILD rename to java/javatests/com/google/copybara/python/BUILD diff --git a/javatests/com/google/copybara/python/PackageMetadataTest.java b/java/javatests/com/google/copybara/python/PackageMetadataTest.java similarity index 100% rename from javatests/com/google/copybara/python/PackageMetadataTest.java rename to java/javatests/com/google/copybara/python/PackageMetadataTest.java diff --git a/javatests/com/google/copybara/python/testfiles/TESTMETADATA b/java/javatests/com/google/copybara/python/testfiles/TESTMETADATA similarity index 100% rename from javatests/com/google/copybara/python/testfiles/TESTMETADATA rename to java/javatests/com/google/copybara/python/testfiles/TESTMETADATA diff --git a/javatests/com/google/copybara/re2/BUILD b/java/javatests/com/google/copybara/re2/BUILD similarity index 100% rename from javatests/com/google/copybara/re2/BUILD rename to java/javatests/com/google/copybara/re2/BUILD diff --git a/javatests/com/google/copybara/re2/Re2Test.java b/java/javatests/com/google/copybara/re2/Re2Test.java similarity index 100% rename from javatests/com/google/copybara/re2/Re2Test.java rename to java/javatests/com/google/copybara/re2/Re2Test.java diff --git a/javatests/com/google/copybara/regenerate/BUILD b/java/javatests/com/google/copybara/regenerate/BUILD similarity index 100% rename from javatests/com/google/copybara/regenerate/BUILD rename to java/javatests/com/google/copybara/regenerate/BUILD diff --git a/javatests/com/google/copybara/regenerate/RegenerateCmdTest.java b/java/javatests/com/google/copybara/regenerate/RegenerateCmdTest.java similarity index 100% rename from javatests/com/google/copybara/regenerate/RegenerateCmdTest.java rename to java/javatests/com/google/copybara/regenerate/RegenerateCmdTest.java diff --git a/javatests/com/google/copybara/remotefile/BUILD b/java/javatests/com/google/copybara/remotefile/BUILD similarity index 100% rename from javatests/com/google/copybara/remotefile/BUILD rename to java/javatests/com/google/copybara/remotefile/BUILD diff --git a/javatests/com/google/copybara/remotefile/GithubArchiveTest.java b/java/javatests/com/google/copybara/remotefile/GithubArchiveTest.java similarity index 100% rename from javatests/com/google/copybara/remotefile/GithubArchiveTest.java rename to java/javatests/com/google/copybara/remotefile/GithubArchiveTest.java diff --git a/javatests/com/google/copybara/remotefile/RemoteArchiveOriginTest.java b/java/javatests/com/google/copybara/remotefile/RemoteArchiveOriginTest.java similarity index 100% rename from javatests/com/google/copybara/remotefile/RemoteArchiveOriginTest.java rename to java/javatests/com/google/copybara/remotefile/RemoteArchiveOriginTest.java diff --git a/javatests/com/google/copybara/remotefile/RemoteFileModuleTest.java b/java/javatests/com/google/copybara/remotefile/RemoteFileModuleTest.java similarity index 100% rename from javatests/com/google/copybara/remotefile/RemoteFileModuleTest.java rename to java/javatests/com/google/copybara/remotefile/RemoteFileModuleTest.java diff --git a/javatests/com/google/copybara/remotefile/extractutil/BUILD b/java/javatests/com/google/copybara/remotefile/extractutil/BUILD similarity index 100% rename from javatests/com/google/copybara/remotefile/extractutil/BUILD rename to java/javatests/com/google/copybara/remotefile/extractutil/BUILD diff --git a/javatests/com/google/copybara/remotefile/extractutil/ExtractUtilTest.java b/java/javatests/com/google/copybara/remotefile/extractutil/ExtractUtilTest.java similarity index 100% rename from javatests/com/google/copybara/remotefile/extractutil/ExtractUtilTest.java rename to java/javatests/com/google/copybara/remotefile/extractutil/ExtractUtilTest.java diff --git a/javatests/com/google/copybara/rust/BUILD b/java/javatests/com/google/copybara/rust/BUILD similarity index 100% rename from javatests/com/google/copybara/rust/BUILD rename to java/javatests/com/google/copybara/rust/BUILD diff --git a/javatests/com/google/copybara/rust/ComparisonRustVersionRequirementTest.java b/java/javatests/com/google/copybara/rust/ComparisonRustVersionRequirementTest.java similarity index 100% rename from javatests/com/google/copybara/rust/ComparisonRustVersionRequirementTest.java rename to java/javatests/com/google/copybara/rust/ComparisonRustVersionRequirementTest.java diff --git a/javatests/com/google/copybara/rust/DefaultRustVersionRequirementTest.java b/java/javatests/com/google/copybara/rust/DefaultRustVersionRequirementTest.java similarity index 100% rename from javatests/com/google/copybara/rust/DefaultRustVersionRequirementTest.java rename to java/javatests/com/google/copybara/rust/DefaultRustVersionRequirementTest.java diff --git a/javatests/com/google/copybara/rust/EpochRustVersionRequirementTest.java b/java/javatests/com/google/copybara/rust/EpochRustVersionRequirementTest.java similarity index 100% rename from javatests/com/google/copybara/rust/EpochRustVersionRequirementTest.java rename to java/javatests/com/google/copybara/rust/EpochRustVersionRequirementTest.java diff --git a/javatests/com/google/copybara/rust/MultipleRustVersionRequirementTest.java b/java/javatests/com/google/copybara/rust/MultipleRustVersionRequirementTest.java similarity index 100% rename from javatests/com/google/copybara/rust/MultipleRustVersionRequirementTest.java rename to java/javatests/com/google/copybara/rust/MultipleRustVersionRequirementTest.java diff --git a/javatests/com/google/copybara/rust/RustCratesIoVersionListTest.java b/java/javatests/com/google/copybara/rust/RustCratesIoVersionListTest.java similarity index 100% rename from javatests/com/google/copybara/rust/RustCratesIoVersionListTest.java rename to java/javatests/com/google/copybara/rust/RustCratesIoVersionListTest.java diff --git a/javatests/com/google/copybara/rust/RustCratesIoVersionResolverTest.java b/java/javatests/com/google/copybara/rust/RustCratesIoVersionResolverTest.java similarity index 100% rename from javatests/com/google/copybara/rust/RustCratesIoVersionResolverTest.java rename to java/javatests/com/google/copybara/rust/RustCratesIoVersionResolverTest.java diff --git a/javatests/com/google/copybara/rust/RustCratesIoVersionSelectorTest.java b/java/javatests/com/google/copybara/rust/RustCratesIoVersionSelectorTest.java similarity index 100% rename from javatests/com/google/copybara/rust/RustCratesIoVersionSelectorTest.java rename to java/javatests/com/google/copybara/rust/RustCratesIoVersionSelectorTest.java diff --git a/javatests/com/google/copybara/rust/RustModuleTest.java b/java/javatests/com/google/copybara/rust/RustModuleTest.java similarity index 100% rename from javatests/com/google/copybara/rust/RustModuleTest.java rename to java/javatests/com/google/copybara/rust/RustModuleTest.java diff --git a/javatests/com/google/copybara/rust/RustVersionRequirementTest.java b/java/javatests/com/google/copybara/rust/RustVersionRequirementTest.java similarity index 100% rename from javatests/com/google/copybara/rust/RustVersionRequirementTest.java rename to java/javatests/com/google/copybara/rust/RustVersionRequirementTest.java diff --git a/javatests/com/google/copybara/rust/TildeRustVersionRequirementTest.java b/java/javatests/com/google/copybara/rust/TildeRustVersionRequirementTest.java similarity index 100% rename from javatests/com/google/copybara/rust/TildeRustVersionRequirementTest.java rename to java/javatests/com/google/copybara/rust/TildeRustVersionRequirementTest.java diff --git a/javatests/com/google/copybara/rust/WildcardRustVersionRequirementTest.java b/java/javatests/com/google/copybara/rust/WildcardRustVersionRequirementTest.java similarity index 100% rename from javatests/com/google/copybara/rust/WildcardRustVersionRequirementTest.java rename to java/javatests/com/google/copybara/rust/WildcardRustVersionRequirementTest.java diff --git a/javatests/com/google/copybara/templatetoken/BUILD b/java/javatests/com/google/copybara/templatetoken/BUILD similarity index 100% rename from javatests/com/google/copybara/templatetoken/BUILD rename to java/javatests/com/google/copybara/templatetoken/BUILD diff --git a/javatests/com/google/copybara/templatetoken/LabelTemplateTest.java b/java/javatests/com/google/copybara/templatetoken/LabelTemplateTest.java similarity index 100% rename from javatests/com/google/copybara/templatetoken/LabelTemplateTest.java rename to java/javatests/com/google/copybara/templatetoken/LabelTemplateTest.java diff --git a/javatests/com/google/copybara/test.bzl b/java/javatests/com/google/copybara/test.bzl similarity index 100% rename from javatests/com/google/copybara/test.bzl rename to java/javatests/com/google/copybara/test.bzl diff --git a/javatests/com/google/copybara/testing/DummyCheckerTest.java b/java/javatests/com/google/copybara/testing/DummyCheckerTest.java similarity index 100% rename from javatests/com/google/copybara/testing/DummyCheckerTest.java rename to java/javatests/com/google/copybara/testing/DummyCheckerTest.java diff --git a/javatests/com/google/copybara/testing/DummyOriginTest.java b/java/javatests/com/google/copybara/testing/DummyOriginTest.java similarity index 100% rename from javatests/com/google/copybara/testing/DummyOriginTest.java rename to java/javatests/com/google/copybara/testing/DummyOriginTest.java diff --git a/javatests/com/google/copybara/testing/SkylarkTestExecutorTest.java b/java/javatests/com/google/copybara/testing/SkylarkTestExecutorTest.java similarity index 100% rename from javatests/com/google/copybara/testing/SkylarkTestExecutorTest.java rename to java/javatests/com/google/copybara/testing/SkylarkTestExecutorTest.java diff --git a/javatests/com/google/copybara/toml/BUILD b/java/javatests/com/google/copybara/toml/BUILD similarity index 100% rename from javatests/com/google/copybara/toml/BUILD rename to java/javatests/com/google/copybara/toml/BUILD diff --git a/javatests/com/google/copybara/toml/TomlTest.java b/java/javatests/com/google/copybara/toml/TomlTest.java similarity index 100% rename from javatests/com/google/copybara/toml/TomlTest.java rename to java/javatests/com/google/copybara/toml/TomlTest.java diff --git a/javatests/com/google/copybara/transform/BUILD b/java/javatests/com/google/copybara/transform/BUILD similarity index 100% rename from javatests/com/google/copybara/transform/BUILD rename to java/javatests/com/google/copybara/transform/BUILD diff --git a/javatests/com/google/copybara/transform/ConvertEncodingTest.java b/java/javatests/com/google/copybara/transform/ConvertEncodingTest.java similarity index 100% rename from javatests/com/google/copybara/transform/ConvertEncodingTest.java rename to java/javatests/com/google/copybara/transform/ConvertEncodingTest.java diff --git a/javatests/com/google/copybara/transform/CopyOrMoveTest.java b/java/javatests/com/google/copybara/transform/CopyOrMoveTest.java similarity index 100% rename from javatests/com/google/copybara/transform/CopyOrMoveTest.java rename to java/javatests/com/google/copybara/transform/CopyOrMoveTest.java diff --git a/javatests/com/google/copybara/transform/CoreTest.java b/java/javatests/com/google/copybara/transform/CoreTest.java similarity index 100% rename from javatests/com/google/copybara/transform/CoreTest.java rename to java/javatests/com/google/copybara/transform/CoreTest.java diff --git a/javatests/com/google/copybara/transform/ExplicitReversalTest.java b/java/javatests/com/google/copybara/transform/ExplicitReversalTest.java similarity index 100% rename from javatests/com/google/copybara/transform/ExplicitReversalTest.java rename to java/javatests/com/google/copybara/transform/ExplicitReversalTest.java diff --git a/javatests/com/google/copybara/transform/FilterReplaceTest.java b/java/javatests/com/google/copybara/transform/FilterReplaceTest.java similarity index 100% rename from javatests/com/google/copybara/transform/FilterReplaceTest.java rename to java/javatests/com/google/copybara/transform/FilterReplaceTest.java diff --git a/javatests/com/google/copybara/transform/RemoveTest.java b/java/javatests/com/google/copybara/transform/RemoveTest.java similarity index 100% rename from javatests/com/google/copybara/transform/RemoveTest.java rename to java/javatests/com/google/copybara/transform/RemoveTest.java diff --git a/javatests/com/google/copybara/transform/RenameTest.java b/java/javatests/com/google/copybara/transform/RenameTest.java similarity index 100% rename from javatests/com/google/copybara/transform/RenameTest.java rename to java/javatests/com/google/copybara/transform/RenameTest.java diff --git a/javatests/com/google/copybara/transform/ReplaceTest.java b/java/javatests/com/google/copybara/transform/ReplaceTest.java similarity index 100% rename from javatests/com/google/copybara/transform/ReplaceTest.java rename to java/javatests/com/google/copybara/transform/ReplaceTest.java diff --git a/javatests/com/google/copybara/transform/SequenceTest.java b/java/javatests/com/google/copybara/transform/SequenceTest.java similarity index 100% rename from javatests/com/google/copybara/transform/SequenceTest.java rename to java/javatests/com/google/copybara/transform/SequenceTest.java diff --git a/javatests/com/google/copybara/transform/SkylarkConsoleTest.java b/java/javatests/com/google/copybara/transform/SkylarkConsoleTest.java similarity index 100% rename from javatests/com/google/copybara/transform/SkylarkConsoleTest.java rename to java/javatests/com/google/copybara/transform/SkylarkConsoleTest.java diff --git a/javatests/com/google/copybara/transform/SkylarkTransformationTest.java b/java/javatests/com/google/copybara/transform/SkylarkTransformationTest.java similarity index 100% rename from javatests/com/google/copybara/transform/SkylarkTransformationTest.java rename to java/javatests/com/google/copybara/transform/SkylarkTransformationTest.java diff --git a/javatests/com/google/copybara/transform/TodoReplaceTest.java b/java/javatests/com/google/copybara/transform/TodoReplaceTest.java similarity index 100% rename from javatests/com/google/copybara/transform/TodoReplaceTest.java rename to java/javatests/com/google/copybara/transform/TodoReplaceTest.java diff --git a/javatests/com/google/copybara/transform/VerifyMatchTest.java b/java/javatests/com/google/copybara/transform/VerifyMatchTest.java similarity index 100% rename from javatests/com/google/copybara/transform/VerifyMatchTest.java rename to java/javatests/com/google/copybara/transform/VerifyMatchTest.java diff --git a/javatests/com/google/copybara/transform/debug/BUILD b/java/javatests/com/google/copybara/transform/debug/BUILD similarity index 100% rename from javatests/com/google/copybara/transform/debug/BUILD rename to java/javatests/com/google/copybara/transform/debug/BUILD diff --git a/javatests/com/google/copybara/transform/debug/TransformDebugTest.java b/java/javatests/com/google/copybara/transform/debug/TransformDebugTest.java similarity index 100% rename from javatests/com/google/copybara/transform/debug/TransformDebugTest.java rename to java/javatests/com/google/copybara/transform/debug/TransformDebugTest.java diff --git a/javatests/com/google/copybara/transform/metadata/MetadataModuleTest.java b/java/javatests/com/google/copybara/transform/metadata/MetadataModuleTest.java similarity index 100% rename from javatests/com/google/copybara/transform/metadata/MetadataModuleTest.java rename to java/javatests/com/google/copybara/transform/metadata/MetadataModuleTest.java diff --git a/javatests/com/google/copybara/transform/metadata/RevisionMigratorTest.java b/java/javatests/com/google/copybara/transform/metadata/RevisionMigratorTest.java similarity index 100% rename from javatests/com/google/copybara/transform/metadata/RevisionMigratorTest.java rename to java/javatests/com/google/copybara/transform/metadata/RevisionMigratorTest.java diff --git a/javatests/com/google/copybara/transform/patch/BUILD b/java/javatests/com/google/copybara/transform/patch/BUILD similarity index 100% rename from javatests/com/google/copybara/transform/patch/BUILD rename to java/javatests/com/google/copybara/transform/patch/BUILD diff --git a/javatests/com/google/copybara/transform/patch/PatchTransformationTest.java b/java/javatests/com/google/copybara/transform/patch/PatchTransformationTest.java similarity index 100% rename from javatests/com/google/copybara/transform/patch/PatchTransformationTest.java rename to java/javatests/com/google/copybara/transform/patch/PatchTransformationTest.java diff --git a/javatests/com/google/copybara/transform/patch/PatchingOptionsTest.java b/java/javatests/com/google/copybara/transform/patch/PatchingOptionsTest.java similarity index 100% rename from javatests/com/google/copybara/transform/patch/PatchingOptionsTest.java rename to java/javatests/com/google/copybara/transform/patch/PatchingOptionsTest.java diff --git a/javatests/com/google/copybara/transform/patch/QuiltTransformationTest.java b/java/javatests/com/google/copybara/transform/patch/QuiltTransformationTest.java similarity index 100% rename from javatests/com/google/copybara/transform/patch/QuiltTransformationTest.java rename to java/javatests/com/google/copybara/transform/patch/QuiltTransformationTest.java diff --git a/javatests/com/google/copybara/treestate/TreeStateTest.java b/java/javatests/com/google/copybara/treestate/TreeStateTest.java similarity index 100% rename from javatests/com/google/copybara/treestate/TreeStateTest.java rename to java/javatests/com/google/copybara/treestate/TreeStateTest.java diff --git a/javatests/com/google/copybara/tsjs/npm/BUILD b/java/javatests/com/google/copybara/tsjs/npm/BUILD similarity index 100% rename from javatests/com/google/copybara/tsjs/npm/BUILD rename to java/javatests/com/google/copybara/tsjs/npm/BUILD diff --git a/javatests/com/google/copybara/tsjs/npm/NpmVersionListTest.java b/java/javatests/com/google/copybara/tsjs/npm/NpmVersionListTest.java similarity index 100% rename from javatests/com/google/copybara/tsjs/npm/NpmVersionListTest.java rename to java/javatests/com/google/copybara/tsjs/npm/NpmVersionListTest.java diff --git a/javatests/com/google/copybara/tsjs/npm/NpmVersionResolverTest.java b/java/javatests/com/google/copybara/tsjs/npm/NpmVersionResolverTest.java similarity index 100% rename from javatests/com/google/copybara/tsjs/npm/NpmVersionResolverTest.java rename to java/javatests/com/google/copybara/tsjs/npm/NpmVersionResolverTest.java diff --git a/javatests/com/google/copybara/util/ApplyDestinationPatchTest.java b/java/javatests/com/google/copybara/util/ApplyDestinationPatchTest.java similarity index 100% rename from javatests/com/google/copybara/util/ApplyDestinationPatchTest.java rename to java/javatests/com/google/copybara/util/ApplyDestinationPatchTest.java diff --git a/javatests/com/google/copybara/util/AutoPatchUtilTest.java b/java/javatests/com/google/copybara/util/AutoPatchUtilTest.java similarity index 100% rename from javatests/com/google/copybara/util/AutoPatchUtilTest.java rename to java/javatests/com/google/copybara/util/AutoPatchUtilTest.java diff --git a/javatests/com/google/copybara/util/CommandLineDiffUtilTest.java b/java/javatests/com/google/copybara/util/CommandLineDiffUtilTest.java similarity index 100% rename from javatests/com/google/copybara/util/CommandLineDiffUtilTest.java rename to java/javatests/com/google/copybara/util/CommandLineDiffUtilTest.java diff --git a/javatests/com/google/copybara/util/ConsistencyFileTest.java b/java/javatests/com/google/copybara/util/ConsistencyFileTest.java similarity index 100% rename from javatests/com/google/copybara/util/ConsistencyFileTest.java rename to java/javatests/com/google/copybara/util/ConsistencyFileTest.java diff --git a/javatests/com/google/copybara/util/DiffUtilTest.java b/java/javatests/com/google/copybara/util/DiffUtilTest.java similarity index 100% rename from javatests/com/google/copybara/util/DiffUtilTest.java rename to java/javatests/com/google/copybara/util/DiffUtilTest.java diff --git a/javatests/com/google/copybara/util/DirFactoryTest.java b/java/javatests/com/google/copybara/util/DirFactoryTest.java similarity index 100% rename from javatests/com/google/copybara/util/DirFactoryTest.java rename to java/javatests/com/google/copybara/util/DirFactoryTest.java diff --git a/javatests/com/google/copybara/util/FileUtilTest.java b/java/javatests/com/google/copybara/util/FileUtilTest.java similarity index 100% rename from javatests/com/google/copybara/util/FileUtilTest.java rename to java/javatests/com/google/copybara/util/FileUtilTest.java diff --git a/javatests/com/google/copybara/util/GlobTest.java b/java/javatests/com/google/copybara/util/GlobTest.java similarity index 100% rename from javatests/com/google/copybara/util/GlobTest.java rename to java/javatests/com/google/copybara/util/GlobTest.java diff --git a/javatests/com/google/copybara/util/LimitFilterOutputStreamTest.java b/java/javatests/com/google/copybara/util/LimitFilterOutputStreamTest.java similarity index 100% rename from javatests/com/google/copybara/util/LimitFilterOutputStreamTest.java rename to java/javatests/com/google/copybara/util/LimitFilterOutputStreamTest.java diff --git a/javatests/com/google/copybara/util/MergeImportToolTest.java b/java/javatests/com/google/copybara/util/MergeImportToolTest.java similarity index 100% rename from javatests/com/google/copybara/util/MergeImportToolTest.java rename to java/javatests/com/google/copybara/util/MergeImportToolTest.java diff --git a/javatests/com/google/copybara/util/OriginUtilTest.java b/java/javatests/com/google/copybara/util/OriginUtilTest.java similarity index 100% rename from javatests/com/google/copybara/util/OriginUtilTest.java rename to java/javatests/com/google/copybara/util/OriginUtilTest.java diff --git a/javatests/com/google/copybara/util/RenameDetectorTest.java b/java/javatests/com/google/copybara/util/RenameDetectorTest.java similarity index 100% rename from javatests/com/google/copybara/util/RenameDetectorTest.java rename to java/javatests/com/google/copybara/util/RenameDetectorTest.java diff --git a/javatests/com/google/copybara/util/RepositoryUtilTest.java b/java/javatests/com/google/copybara/util/RepositoryUtilTest.java similarity index 100% rename from javatests/com/google/copybara/util/RepositoryUtilTest.java rename to java/javatests/com/google/copybara/util/RepositoryUtilTest.java diff --git a/javatests/com/google/copybara/util/ScpUtilTest.java b/java/javatests/com/google/copybara/util/ScpUtilTest.java similarity index 100% rename from javatests/com/google/copybara/util/ScpUtilTest.java rename to java/javatests/com/google/copybara/util/ScpUtilTest.java diff --git a/javatests/com/google/copybara/util/SequenceGlobTest.java b/java/javatests/com/google/copybara/util/SequenceGlobTest.java similarity index 100% rename from javatests/com/google/copybara/util/SequenceGlobTest.java rename to java/javatests/com/google/copybara/util/SequenceGlobTest.java diff --git a/javatests/com/google/copybara/util/TablePrinterTest.java b/java/javatests/com/google/copybara/util/TablePrinterTest.java similarity index 100% rename from javatests/com/google/copybara/util/TablePrinterTest.java rename to java/javatests/com/google/copybara/util/TablePrinterTest.java diff --git a/javatests/com/google/copybara/util/console/BUILD b/java/javatests/com/google/copybara/util/console/BUILD similarity index 100% rename from javatests/com/google/copybara/util/console/BUILD rename to java/javatests/com/google/copybara/util/console/BUILD diff --git a/javatests/com/google/copybara/util/console/ConsoleTest.java b/java/javatests/com/google/copybara/util/console/ConsoleTest.java similarity index 100% rename from javatests/com/google/copybara/util/console/ConsoleTest.java rename to java/javatests/com/google/copybara/util/console/ConsoleTest.java diff --git a/javatests/com/google/copybara/util/console/ConsolesTest.java b/java/javatests/com/google/copybara/util/console/ConsolesTest.java similarity index 100% rename from javatests/com/google/copybara/util/console/ConsolesTest.java rename to java/javatests/com/google/copybara/util/console/ConsolesTest.java diff --git a/javatests/com/google/copybara/util/console/DelegateConsoleTest.java b/java/javatests/com/google/copybara/util/console/DelegateConsoleTest.java similarity index 100% rename from javatests/com/google/copybara/util/console/DelegateConsoleTest.java rename to java/javatests/com/google/copybara/util/console/DelegateConsoleTest.java diff --git a/javatests/com/google/copybara/util/console/FileConsoleTest.java b/java/javatests/com/google/copybara/util/console/FileConsoleTest.java similarity index 100% rename from javatests/com/google/copybara/util/console/FileConsoleTest.java rename to java/javatests/com/google/copybara/util/console/FileConsoleTest.java diff --git a/javatests/com/google/copybara/util/console/MultiplexingConsoleTest.java b/java/javatests/com/google/copybara/util/console/MultiplexingConsoleTest.java similarity index 100% rename from javatests/com/google/copybara/util/console/MultiplexingConsoleTest.java rename to java/javatests/com/google/copybara/util/console/MultiplexingConsoleTest.java diff --git a/javatests/com/google/copybara/util/console/NoPromptConsoleTest.java b/java/javatests/com/google/copybara/util/console/NoPromptConsoleTest.java similarity index 100% rename from javatests/com/google/copybara/util/console/NoPromptConsoleTest.java rename to java/javatests/com/google/copybara/util/console/NoPromptConsoleTest.java diff --git a/javatests/com/google/copybara/util/console/testing/BUILD b/java/javatests/com/google/copybara/util/console/testing/BUILD similarity index 100% rename from javatests/com/google/copybara/util/console/testing/BUILD rename to java/javatests/com/google/copybara/util/console/testing/BUILD diff --git a/javatests/com/google/copybara/util/console/testing/TestingConsoleTest.java b/java/javatests/com/google/copybara/util/console/testing/TestingConsoleTest.java similarity index 100% rename from javatests/com/google/copybara/util/console/testing/TestingConsoleTest.java rename to java/javatests/com/google/copybara/util/console/testing/TestingConsoleTest.java diff --git a/javatests/com/google/copybara/version/BUILD b/java/javatests/com/google/copybara/version/BUILD similarity index 100% rename from javatests/com/google/copybara/version/BUILD rename to java/javatests/com/google/copybara/version/BUILD diff --git a/javatests/com/google/copybara/version/CustomVersionSelectorTest.java b/java/javatests/com/google/copybara/version/CustomVersionSelectorTest.java similarity index 100% rename from javatests/com/google/copybara/version/CustomVersionSelectorTest.java rename to java/javatests/com/google/copybara/version/CustomVersionSelectorTest.java diff --git a/javatests/com/google/copybara/version/LatestVersionSelectorTest.java b/java/javatests/com/google/copybara/version/LatestVersionSelectorTest.java similarity index 100% rename from javatests/com/google/copybara/version/LatestVersionSelectorTest.java rename to java/javatests/com/google/copybara/version/LatestVersionSelectorTest.java diff --git a/javatests/com/google/copybara/version/OrderedVersionSelectorTest.java b/java/javatests/com/google/copybara/version/OrderedVersionSelectorTest.java similarity index 100% rename from javatests/com/google/copybara/version/OrderedVersionSelectorTest.java rename to java/javatests/com/google/copybara/version/OrderedVersionSelectorTest.java diff --git a/javatests/com/google/copybara/version/RequestedExactMatchSelectorTest.java b/java/javatests/com/google/copybara/version/RequestedExactMatchSelectorTest.java similarity index 100% rename from javatests/com/google/copybara/version/RequestedExactMatchSelectorTest.java rename to java/javatests/com/google/copybara/version/RequestedExactMatchSelectorTest.java diff --git a/javatests/com/google/copybara/version/RequestedVersionSelectorTest.java b/java/javatests/com/google/copybara/version/RequestedVersionSelectorTest.java similarity index 100% rename from javatests/com/google/copybara/version/RequestedVersionSelectorTest.java rename to java/javatests/com/google/copybara/version/RequestedVersionSelectorTest.java diff --git a/javatests/com/google/copybara/xml/BUILD b/java/javatests/com/google/copybara/xml/BUILD similarity index 100% rename from javatests/com/google/copybara/xml/BUILD rename to java/javatests/com/google/copybara/xml/BUILD diff --git a/javatests/com/google/copybara/xml/XmlTest.java b/java/javatests/com/google/copybara/xml/XmlTest.java similarity index 100% rename from javatests/com/google/copybara/xml/XmlTest.java rename to java/javatests/com/google/copybara/xml/XmlTest.java diff --git a/repositories.bzl b/java/repositories.bzl similarity index 100% rename from repositories.bzl rename to java/repositories.bzl diff --git a/scripts/update_docs b/java/scripts/update_docs similarity index 100% rename from scripts/update_docs rename to java/scripts/update_docs diff --git a/third_party/BUILD b/java/third_party/BUILD similarity index 100% rename from third_party/BUILD rename to java/third_party/BUILD diff --git a/third_party/bazel/BUILD b/java/third_party/bazel/BUILD similarity index 100% rename from third_party/bazel/BUILD rename to java/third_party/bazel/BUILD diff --git a/third_party/bazel/LICENSE b/java/third_party/bazel/LICENSE similarity index 100% rename from third_party/bazel/LICENSE rename to java/third_party/bazel/LICENSE diff --git a/third_party/bazel/bashunit/BUILD b/java/third_party/bazel/bashunit/BUILD similarity index 100% rename from third_party/bazel/bashunit/BUILD rename to java/third_party/bazel/bashunit/BUILD diff --git a/third_party/bazel/bashunit/LICENSE b/java/third_party/bazel/bashunit/LICENSE similarity index 100% rename from third_party/bazel/bashunit/LICENSE rename to java/third_party/bazel/bashunit/LICENSE diff --git a/third_party/bazel/bashunit/testenv.sh b/java/third_party/bazel/bashunit/testenv.sh similarity index 100% rename from third_party/bazel/bashunit/testenv.sh rename to java/third_party/bazel/bashunit/testenv.sh diff --git a/third_party/bazel/bashunit/unittest.bash b/java/third_party/bazel/bashunit/unittest.bash similarity index 100% rename from third_party/bazel/bashunit/unittest.bash rename to java/third_party/bazel/bashunit/unittest.bash diff --git a/third_party/bazel/conditions/BUILD b/java/third_party/bazel/conditions/BUILD similarity index 100% rename from third_party/bazel/conditions/BUILD rename to java/third_party/bazel/conditions/BUILD diff --git a/third_party/bazel/conditions/BUILD.tools b/java/third_party/bazel/conditions/BUILD.tools similarity index 100% rename from third_party/bazel/conditions/BUILD.tools rename to java/third_party/bazel/conditions/BUILD.tools diff --git a/third_party/bazel/main/java/com/google/copybara/shell/AbnormalTerminationException.java b/java/third_party/bazel/main/java/com/google/copybara/shell/AbnormalTerminationException.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/AbnormalTerminationException.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/AbnormalTerminationException.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/BUILD b/java/third_party/bazel/main/java/com/google/copybara/shell/BUILD similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/BUILD rename to java/third_party/bazel/main/java/com/google/copybara/shell/BUILD diff --git a/third_party/bazel/main/java/com/google/copybara/shell/BadExitStatusException.java b/java/third_party/bazel/main/java/com/google/copybara/shell/BadExitStatusException.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/BadExitStatusException.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/BadExitStatusException.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/Command.java b/java/third_party/bazel/main/java/com/google/copybara/shell/Command.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/Command.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/Command.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/CommandException.java b/java/third_party/bazel/main/java/com/google/copybara/shell/CommandException.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/CommandException.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/CommandException.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/CommandResult.java b/java/third_party/bazel/main/java/com/google/copybara/shell/CommandResult.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/CommandResult.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/CommandResult.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/Consumers.java b/java/third_party/bazel/main/java/com/google/copybara/shell/Consumers.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/Consumers.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/Consumers.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/ExecFailedException.java b/java/third_party/bazel/main/java/com/google/copybara/shell/ExecFailedException.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/ExecFailedException.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/ExecFailedException.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/FutureCommandResult.java b/java/third_party/bazel/main/java/com/google/copybara/shell/FutureCommandResult.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/FutureCommandResult.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/FutureCommandResult.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/InputStreamSink.java b/java/third_party/bazel/main/java/com/google/copybara/shell/InputStreamSink.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/InputStreamSink.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/InputStreamSink.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/Killable.java b/java/third_party/bazel/main/java/com/google/copybara/shell/Killable.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/Killable.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/Killable.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/KillableObserver.java b/java/third_party/bazel/main/java/com/google/copybara/shell/KillableObserver.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/KillableObserver.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/KillableObserver.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/LogUtil.java b/java/third_party/bazel/main/java/com/google/copybara/shell/LogUtil.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/LogUtil.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/LogUtil.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/ProcessKillable.java b/java/third_party/bazel/main/java/com/google/copybara/shell/ProcessKillable.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/ProcessKillable.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/ProcessKillable.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/ShellUtils.java b/java/third_party/bazel/main/java/com/google/copybara/shell/ShellUtils.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/ShellUtils.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/ShellUtils.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/SimpleKillableObserver.java b/java/third_party/bazel/main/java/com/google/copybara/shell/SimpleKillableObserver.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/SimpleKillableObserver.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/SimpleKillableObserver.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/TerminationStatus.java b/java/third_party/bazel/main/java/com/google/copybara/shell/TerminationStatus.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/TerminationStatus.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/TerminationStatus.java diff --git a/third_party/bazel/main/java/com/google/copybara/shell/TimeoutKillableObserver.java b/java/third_party/bazel/main/java/com/google/copybara/shell/TimeoutKillableObserver.java similarity index 100% rename from third_party/bazel/main/java/com/google/copybara/shell/TimeoutKillableObserver.java rename to java/third_party/bazel/main/java/com/google/copybara/shell/TimeoutKillableObserver.java diff --git a/third_party/bazel/main/java/com/google/devtools/build/lib/concurrent/BUILD b/java/third_party/bazel/main/java/com/google/devtools/build/lib/concurrent/BUILD similarity index 100% rename from third_party/bazel/main/java/com/google/devtools/build/lib/concurrent/BUILD rename to java/third_party/bazel/main/java/com/google/devtools/build/lib/concurrent/BUILD diff --git a/third_party/bazel/main/java/com/google/devtools/build/lib/concurrent/ThreadSafety.java b/java/third_party/bazel/main/java/com/google/devtools/build/lib/concurrent/ThreadSafety.java similarity index 100% rename from third_party/bazel/main/java/com/google/devtools/build/lib/concurrent/ThreadSafety.java rename to java/third_party/bazel/main/java/com/google/devtools/build/lib/concurrent/ThreadSafety.java diff --git a/third_party/bazel/main/java/net/starlark/java/BUILD b/java/third_party/bazel/main/java/net/starlark/java/BUILD similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/BUILD rename to java/third_party/bazel/main/java/net/starlark/java/BUILD diff --git a/third_party/bazel/main/java/net/starlark/java/annot/BUILD b/java/third_party/bazel/main/java/net/starlark/java/annot/BUILD similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/annot/BUILD rename to java/third_party/bazel/main/java/net/starlark/java/annot/BUILD diff --git a/third_party/bazel/main/java/net/starlark/java/annot/Param.java b/java/third_party/bazel/main/java/net/starlark/java/annot/Param.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/annot/Param.java rename to java/third_party/bazel/main/java/net/starlark/java/annot/Param.java diff --git a/third_party/bazel/main/java/net/starlark/java/annot/ParamType.java b/java/third_party/bazel/main/java/net/starlark/java/annot/ParamType.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/annot/ParamType.java rename to java/third_party/bazel/main/java/net/starlark/java/annot/ParamType.java diff --git a/third_party/bazel/main/java/net/starlark/java/annot/README.md b/java/third_party/bazel/main/java/net/starlark/java/annot/README.md similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/annot/README.md rename to java/third_party/bazel/main/java/net/starlark/java/annot/README.md diff --git a/third_party/bazel/main/java/net/starlark/java/annot/StarlarkAnnotations.java b/java/third_party/bazel/main/java/net/starlark/java/annot/StarlarkAnnotations.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/annot/StarlarkAnnotations.java rename to java/third_party/bazel/main/java/net/starlark/java/annot/StarlarkAnnotations.java diff --git a/third_party/bazel/main/java/net/starlark/java/annot/StarlarkBuiltin.java b/java/third_party/bazel/main/java/net/starlark/java/annot/StarlarkBuiltin.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/annot/StarlarkBuiltin.java rename to java/third_party/bazel/main/java/net/starlark/java/annot/StarlarkBuiltin.java diff --git a/third_party/bazel/main/java/net/starlark/java/annot/StarlarkMethod.java b/java/third_party/bazel/main/java/net/starlark/java/annot/StarlarkMethod.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/annot/StarlarkMethod.java rename to java/third_party/bazel/main/java/net/starlark/java/annot/StarlarkMethod.java diff --git a/third_party/bazel/main/java/net/starlark/java/annot/processor/BUILD b/java/third_party/bazel/main/java/net/starlark/java/annot/processor/BUILD similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/annot/processor/BUILD rename to java/third_party/bazel/main/java/net/starlark/java/annot/processor/BUILD diff --git a/third_party/bazel/main/java/net/starlark/java/annot/processor/StarlarkMethodProcessor.java b/java/third_party/bazel/main/java/net/starlark/java/annot/processor/StarlarkMethodProcessor.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/annot/processor/StarlarkMethodProcessor.java rename to java/third_party/bazel/main/java/net/starlark/java/annot/processor/StarlarkMethodProcessor.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/BUILD b/java/third_party/bazel/main/java/net/starlark/java/eval/BUILD similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/BUILD rename to java/third_party/bazel/main/java/net/starlark/java/eval/BUILD diff --git a/third_party/bazel/main/java/net/starlark/java/eval/BuiltinFunction.java b/java/third_party/bazel/main/java/net/starlark/java/eval/BuiltinFunction.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/BuiltinFunction.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/BuiltinFunction.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/CallUtils.java b/java/third_party/bazel/main/java/net/starlark/java/eval/CallUtils.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/CallUtils.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/CallUtils.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/CpuProfiler.java b/java/third_party/bazel/main/java/net/starlark/java/eval/CpuProfiler.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/CpuProfiler.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/CpuProfiler.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupport.java b/java/third_party/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupport.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupport.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupport.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupportImpl.java b/java/third_party/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupportImpl.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupportImpl.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/CpuProfilerNativeSupportImpl.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Debug.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Debug.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Debug.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Debug.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Dict.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Dict.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Dict.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Dict.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Eval.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Eval.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Eval.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Eval.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/EvalException.java b/java/third_party/bazel/main/java/net/starlark/java/eval/EvalException.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/EvalException.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/EvalException.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/EvalUtils.java b/java/third_party/bazel/main/java/net/starlark/java/eval/EvalUtils.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/EvalUtils.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/EvalUtils.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/FlagGuardedValue.java b/java/third_party/bazel/main/java/net/starlark/java/eval/FlagGuardedValue.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/FlagGuardedValue.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/FlagGuardedValue.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/FormatParser.java b/java/third_party/bazel/main/java/net/starlark/java/eval/FormatParser.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/FormatParser.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/FormatParser.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/GuardedValue.java b/java/third_party/bazel/main/java/net/starlark/java/eval/GuardedValue.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/GuardedValue.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/GuardedValue.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/HasBinary.java b/java/third_party/bazel/main/java/net/starlark/java/eval/HasBinary.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/HasBinary.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/HasBinary.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/ImmutableSingletonStarlarkList.java b/java/third_party/bazel/main/java/net/starlark/java/eval/ImmutableSingletonStarlarkList.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/ImmutableSingletonStarlarkList.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/ImmutableSingletonStarlarkList.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/ImmutableStarlarkList.java b/java/third_party/bazel/main/java/net/starlark/java/eval/ImmutableStarlarkList.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/ImmutableStarlarkList.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/ImmutableStarlarkList.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/JNI.java b/java/third_party/bazel/main/java/net/starlark/java/eval/JNI.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/JNI.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/JNI.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/LazyImmutableStarlarkList.java b/java/third_party/bazel/main/java/net/starlark/java/eval/LazyImmutableStarlarkList.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/LazyImmutableStarlarkList.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/LazyImmutableStarlarkList.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/MethodDescriptor.java b/java/third_party/bazel/main/java/net/starlark/java/eval/MethodDescriptor.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/MethodDescriptor.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/MethodDescriptor.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/MethodLibrary.java b/java/third_party/bazel/main/java/net/starlark/java/eval/MethodLibrary.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/MethodLibrary.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/MethodLibrary.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Module.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Module.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Module.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Module.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Mutability.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Mutability.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Mutability.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Mutability.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/MutableStarlarkList.java b/java/third_party/bazel/main/java/net/starlark/java/eval/MutableStarlarkList.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/MutableStarlarkList.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/MutableStarlarkList.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/NoneType.java b/java/third_party/bazel/main/java/net/starlark/java/eval/NoneType.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/NoneType.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/NoneType.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/ParamDescriptor.java b/java/third_party/bazel/main/java/net/starlark/java/eval/ParamDescriptor.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/ParamDescriptor.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/ParamDescriptor.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Printer.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Printer.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Printer.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Printer.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/RangeList.java b/java/third_party/bazel/main/java/net/starlark/java/eval/RangeList.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/RangeList.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/RangeList.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/RegularImmutableStarlarkList.java b/java/third_party/bazel/main/java/net/starlark/java/eval/RegularImmutableStarlarkList.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/RegularImmutableStarlarkList.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/RegularImmutableStarlarkList.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/RegularTuple.java b/java/third_party/bazel/main/java/net/starlark/java/eval/RegularTuple.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/RegularTuple.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/RegularTuple.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Sequence.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Sequence.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Sequence.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Sequence.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/SingletonTuple.java b/java/third_party/bazel/main/java/net/starlark/java/eval/SingletonTuple.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/SingletonTuple.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/SingletonTuple.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Starlark.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Starlark.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Starlark.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Starlark.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkCallable.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkCallable.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkCallable.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkCallable.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkFloat.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkFloat.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkFloat.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkFloat.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkFunction.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkFunction.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkFunction.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkFunction.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkIndexable.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkIndexable.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkIndexable.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkIndexable.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkInt.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkInt.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkInt.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkInt.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkIterable.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkIterable.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkIterable.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkIterable.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkList.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkList.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkList.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkList.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkMembershipTestable.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkMembershipTestable.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkMembershipTestable.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkMembershipTestable.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkSemantics.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkSemantics.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkSemantics.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkSemantics.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkSet.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkSet.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkSet.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkSet.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkThread.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkThread.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkThread.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkThread.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkTypeValue.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkTypeValue.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkTypeValue.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkTypeValue.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StarlarkValue.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkValue.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StarlarkValue.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StarlarkValue.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/StringModule.java b/java/third_party/bazel/main/java/net/starlark/java/eval/StringModule.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/StringModule.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/StringModule.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Structure.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Structure.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Structure.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Structure.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/SymbolGenerator.java b/java/third_party/bazel/main/java/net/starlark/java/eval/SymbolGenerator.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/SymbolGenerator.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/SymbolGenerator.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/Tuple.java b/java/third_party/bazel/main/java/net/starlark/java/eval/Tuple.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/Tuple.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/Tuple.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/TypeChecker.java b/java/third_party/bazel/main/java/net/starlark/java/eval/TypeChecker.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/TypeChecker.java rename to java/third_party/bazel/main/java/net/starlark/java/eval/TypeChecker.java diff --git a/third_party/bazel/main/java/net/starlark/java/eval/cpu_profiler_posix.cc b/java/third_party/bazel/main/java/net/starlark/java/eval/cpu_profiler_posix.cc similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/eval/cpu_profiler_posix.cc rename to java/third_party/bazel/main/java/net/starlark/java/eval/cpu_profiler_posix.cc diff --git a/third_party/bazel/main/java/net/starlark/java/lib/BUILD b/java/third_party/bazel/main/java/net/starlark/java/lib/BUILD similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/lib/BUILD rename to java/third_party/bazel/main/java/net/starlark/java/lib/BUILD diff --git a/third_party/bazel/main/java/net/starlark/java/lib/MapWrapperStructure.java b/java/third_party/bazel/main/java/net/starlark/java/lib/MapWrapperStructure.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/lib/MapWrapperStructure.java rename to java/third_party/bazel/main/java/net/starlark/java/lib/MapWrapperStructure.java diff --git a/third_party/bazel/main/java/net/starlark/java/lib/StarlarkEncodable.java b/java/third_party/bazel/main/java/net/starlark/java/lib/StarlarkEncodable.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/lib/StarlarkEncodable.java rename to java/third_party/bazel/main/java/net/starlark/java/lib/StarlarkEncodable.java diff --git a/third_party/bazel/main/java/net/starlark/java/lib/json/BUILD b/java/third_party/bazel/main/java/net/starlark/java/lib/json/BUILD similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/lib/json/BUILD rename to java/third_party/bazel/main/java/net/starlark/java/lib/json/BUILD diff --git a/third_party/bazel/main/java/net/starlark/java/lib/json/Json.java b/java/third_party/bazel/main/java/net/starlark/java/lib/json/Json.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/lib/json/Json.java rename to java/third_party/bazel/main/java/net/starlark/java/lib/json/Json.java diff --git a/third_party/bazel/main/java/net/starlark/java/spelling/BUILD b/java/third_party/bazel/main/java/net/starlark/java/spelling/BUILD similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/spelling/BUILD rename to java/third_party/bazel/main/java/net/starlark/java/spelling/BUILD diff --git a/third_party/bazel/main/java/net/starlark/java/spelling/SpellChecker.java b/java/third_party/bazel/main/java/net/starlark/java/spelling/SpellChecker.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/spelling/SpellChecker.java rename to java/third_party/bazel/main/java/net/starlark/java/spelling/SpellChecker.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Argument.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Argument.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Argument.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Argument.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/AssignmentStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/AssignmentStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/AssignmentStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/AssignmentStatement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/BUILD b/java/third_party/bazel/main/java/net/starlark/java/syntax/BUILD similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/BUILD rename to java/third_party/bazel/main/java/net/starlark/java/syntax/BUILD diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/BinaryOperatorExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/BinaryOperatorExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/BinaryOperatorExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/BinaryOperatorExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/CallExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/CallExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/CallExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/CallExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/CastExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/CastExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/CastExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/CastExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Comment.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Comment.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Comment.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Comment.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Comprehension.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Comprehension.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Comprehension.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Comprehension.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/ConditionalExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/ConditionalExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/ConditionalExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/ConditionalExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/DefStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/DefStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/DefStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/DefStatement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/DictExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/DictExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/DictExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/DictExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/DocComments.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/DocComments.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/DocComments.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/DocComments.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/DotExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/DotExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/DotExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/DotExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Ellipsis.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Ellipsis.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Ellipsis.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Ellipsis.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Expression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Expression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Expression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Expression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/ExpressionStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/ExpressionStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/ExpressionStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/ExpressionStatement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/FileLocations.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/FileLocations.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/FileLocations.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/FileLocations.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/FileOptions.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/FileOptions.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/FileOptions.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/FileOptions.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/FloatLiteral.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/FloatLiteral.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/FloatLiteral.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/FloatLiteral.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/FlowStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/FlowStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/FlowStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/FlowStatement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/ForStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/ForStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/ForStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/ForStatement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Identifier.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Identifier.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Identifier.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Identifier.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/IfStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/IfStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/IfStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/IfStatement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/IndexExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/IndexExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/IndexExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/IndexExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/IntLiteral.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/IntLiteral.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/IntLiteral.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/IntLiteral.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/IsInstanceExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/IsInstanceExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/IsInstanceExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/IsInstanceExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/LambdaExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/LambdaExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/LambdaExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/LambdaExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Lexer.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Lexer.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Lexer.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Lexer.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/ListExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/ListExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/ListExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/ListExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/LoadStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/LoadStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/LoadStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/LoadStatement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Location.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Location.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Location.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Location.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Node.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Node.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Node.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Node.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/NodePrinter.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/NodePrinter.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/NodePrinter.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/NodePrinter.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/NodeVisitor.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/NodeVisitor.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/NodeVisitor.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/NodeVisitor.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Parameter.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Parameter.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Parameter.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Parameter.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Parser.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Parser.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Parser.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Parser.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/ParserInput.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/ParserInput.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/ParserInput.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/ParserInput.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Program.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Program.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Program.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Program.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Resolver.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Resolver.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Resolver.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Resolver.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/ReturnStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/ReturnStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/ReturnStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/ReturnStatement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/SliceExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/SliceExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/SliceExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/SliceExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/StarlarkFile.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/StarlarkFile.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/StarlarkFile.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/StarlarkFile.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/StarlarkType.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/StarlarkType.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/StarlarkType.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/StarlarkType.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Statement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Statement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Statement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Statement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/StringLiteral.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/StringLiteral.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/StringLiteral.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/StringLiteral.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/SyntaxError.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/SyntaxError.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/SyntaxError.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/SyntaxError.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/SyntaxUtils.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/SyntaxUtils.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/SyntaxUtils.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/SyntaxUtils.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/TokenKind.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/TokenKind.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/TokenKind.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/TokenKind.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/TypeAliasStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/TypeAliasStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/TypeAliasStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/TypeAliasStatement.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/TypeApplication.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/TypeApplication.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/TypeApplication.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/TypeApplication.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/TypeChecker.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/TypeChecker.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/TypeChecker.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/TypeChecker.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/TypeConstructor.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/TypeConstructor.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/TypeConstructor.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/TypeConstructor.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/TypeContext.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/TypeContext.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/TypeContext.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/TypeContext.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/TypeTagger.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/TypeTagger.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/TypeTagger.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/TypeTagger.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/Types.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/Types.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/Types.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/Types.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/UnaryOperatorExpression.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/UnaryOperatorExpression.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/UnaryOperatorExpression.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/UnaryOperatorExpression.java diff --git a/third_party/bazel/main/java/net/starlark/java/syntax/VarStatement.java b/java/third_party/bazel/main/java/net/starlark/java/syntax/VarStatement.java similarity index 100% rename from third_party/bazel/main/java/net/starlark/java/syntax/VarStatement.java rename to java/third_party/bazel/main/java/net/starlark/java/syntax/VarStatement.java diff --git a/src/Copybara.Cli/Copybara.Cli.csproj b/src/Copybara.Cli/Copybara.Cli.csproj new file mode 100644 index 000000000..1b2690274 --- /dev/null +++ b/src/Copybara.Cli/Copybara.Cli.csproj @@ -0,0 +1,18 @@ + + + Exe + Copybara.Cli + copybara + true + copybara + Copybara + ./nupkg + 0.1.0 + A tool for transforming and moving code between repositories (.NET port of Google Copybara). + + + + + + + diff --git a/src/Copybara.Cli/Program.cs b/src/Copybara.Cli/Program.cs new file mode 100644 index 000000000..827627b78 --- /dev/null +++ b/src/Copybara.Cli/Program.cs @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Entry point for the `copybara` .NET tool. +// The full CLI (arg parsing, command dispatch, Main orchestration) is ported in Phase 7. +// This is a placeholder that keeps the tool runnable while the engine is built out. + +Console.WriteLine("Copybara (.NET port) — under construction. See TODO.md for status."); +return 0; diff --git a/src/Copybara.Common/Copybara.Common.csproj b/src/Copybara.Common/Copybara.Common.csproj new file mode 100644 index 000000000..f753f338c --- /dev/null +++ b/src/Copybara.Common/Copybara.Common.csproj @@ -0,0 +1,6 @@ + + + Copybara.Common + Copybara.Common + + diff --git a/src/Copybara.Common/ImmutableListMultimap.cs b/src/Copybara.Common/ImmutableListMultimap.cs new file mode 100644 index 000000000..1abf62e2a --- /dev/null +++ b/src/Copybara.Common/ImmutableListMultimap.cs @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections; +using System.Collections.Immutable; + +namespace Copybara.Common; + +/// +/// An immutable multimap preserving insertion order of keys and values, mirroring the +/// subset of Guava's ImmutableListMultimap that Copybara relies on. A key may map to +/// multiple values; iteration order matches insertion order. +/// +public sealed class ImmutableListMultimap : IEnumerable> + where TKey : notnull +{ + private readonly ImmutableArray> _entries; + private readonly ImmutableDictionary> _index; + + private ImmutableListMultimap( + ImmutableArray> entries, + ImmutableDictionary> index) + { + _entries = entries; + _index = index; + } + + private static readonly ImmutableListMultimap EmptyInstance = + new(ImmutableArray>.Empty, + ImmutableDictionary>.Empty); + + public static ImmutableListMultimap Empty => EmptyInstance; + + /// All values associated with in insertion order (empty if none). + public ImmutableArray this[TKey key] => + _index.TryGetValue(key, out var values) ? values : ImmutableArray.Empty; + + /// All values associated with in insertion order (empty if none). + public ImmutableArray Get(TKey key) => this[key]; + + public bool ContainsKey(TKey key) => _index.ContainsKey(key); + + public bool ContainsEntry(TKey key, TValue value) => + _index.TryGetValue(key, out var values) && values.Contains(value); + + public IEnumerable Keys => _index.Keys; + + public int Count => _entries.Length; + + public bool IsEmpty => _entries.IsEmpty; + + /// Returns the entries as a map from key to its list of values. + public ImmutableDictionary> AsMap() => _index; + + public IEnumerator> GetEnumerator() => + ((IEnumerable>)_entries).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public static Builder CreateBuilder() => new(); + + /// Builder that accumulates entries preserving insertion order. + public sealed class Builder + { + private readonly ImmutableArray>.Builder _entries = + ImmutableArray.CreateBuilder>(); + + public Builder Put(TKey key, TValue value) + { + _entries.Add(new KeyValuePair(key, value)); + return this; + } + + public Builder PutAll(TKey key, IEnumerable values) + { + foreach (var value in values) + { + _entries.Add(new KeyValuePair(key, value)); + } + return this; + } + + public Builder PutAll(ImmutableListMultimap multimap) + { + foreach (var entry in multimap) + { + _entries.Add(entry); + } + return this; + } + + public ImmutableListMultimap Build() + { + var entries = _entries.ToImmutable(); + var perKey = new Dictionary.Builder>(); + foreach (var entry in entries) + { + if (!perKey.TryGetValue(entry.Key, out var listBuilder)) + { + listBuilder = ImmutableArray.CreateBuilder(); + perKey[entry.Key] = listBuilder; + } + listBuilder.Add(entry.Value); + } + + var indexBuilder = ImmutableDictionary.CreateBuilder>(); + foreach (var kvp in perKey) + { + indexBuilder[kvp.Key] = kvp.Value.ToImmutable(); + } + + return new ImmutableListMultimap(entries, indexBuilder.ToImmutable()); + } + } +} diff --git a/src/Copybara.Common/Preconditions.cs b/src/Copybara.Common/Preconditions.cs new file mode 100644 index 000000000..33c9d1bae --- /dev/null +++ b/src/Copybara.Common/Preconditions.cs @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Diagnostics.CodeAnalysis; + +namespace Copybara.Common; + +/// +/// Guava-style precondition helpers. Ported from com.google.common.base.Preconditions +/// (only the members Copybara actually uses). +/// +public static class Preconditions +{ + /// Ensures is not null, returning it. + public static T CheckNotNull([NotNull] T? reference) where T : class + { + if (reference is null) + { + throw new ArgumentNullException(); + } + return reference; + } + + /// Ensures is not null, returning it. + public static T CheckNotNull([NotNull] T? reference, object? errorMessage) where T : class + { + if (reference is null) + { + throw new ArgumentNullException(null, errorMessage?.ToString()); + } + return reference; + } + + /// Ensures is not null, returning it. + public static T CheckNotNull([NotNull] T? reference, string format, params object?[] args) + where T : class + { + if (reference is null) + { + throw new ArgumentNullException(null, string.Format(format, args)); + } + return reference; + } + + /// Ensures the truth of an expression involving parameters to the calling method. + public static void CheckArgument([DoesNotReturnIf(false)] bool expression) + { + if (!expression) + { + throw new ArgumentException(); + } + } + + /// Ensures the truth of an expression involving parameters to the calling method. + public static void CheckArgument([DoesNotReturnIf(false)] bool expression, object? errorMessage) + { + if (!expression) + { + throw new ArgumentException(errorMessage?.ToString()); + } + } + + /// Ensures the truth of an expression involving parameters to the calling method. + public static void CheckArgument( + [DoesNotReturnIf(false)] bool expression, string format, params object?[] args) + { + if (!expression) + { + throw new ArgumentException(string.Format(format, args)); + } + } + + /// Ensures the truth of an expression involving the state of the calling instance. + public static void CheckState([DoesNotReturnIf(false)] bool expression) + { + if (!expression) + { + throw new InvalidOperationException(); + } + } + + /// Ensures the truth of an expression involving the state of the calling instance. + public static void CheckState([DoesNotReturnIf(false)] bool expression, object? errorMessage) + { + if (!expression) + { + throw new InvalidOperationException(errorMessage?.ToString()); + } + } + + /// Ensures the truth of an expression involving the state of the calling instance. + public static void CheckState( + [DoesNotReturnIf(false)] bool expression, string format, params object?[] args) + { + if (!expression) + { + throw new InvalidOperationException(string.Format(format, args)); + } + } +} diff --git a/src/Copybara.Core/Copybara.Core.csproj b/src/Copybara.Core/Copybara.Core.csproj new file mode 100644 index 000000000..47a90ee1f --- /dev/null +++ b/src/Copybara.Core/Copybara.Core.csproj @@ -0,0 +1,15 @@ + + + Copybara + Copybara.Core + + + + + + + + + + + diff --git a/src/Copybara.Core/_Placeholder.cs b/src/Copybara.Core/_Placeholder.cs new file mode 100644 index 000000000..d4f5e89ac --- /dev/null +++ b/src/Copybara.Core/_Placeholder.cs @@ -0,0 +1 @@ +namespace Copybara { internal static class _Placeholder { } } diff --git a/src/Starlark/Starlark.csproj b/src/Starlark/Starlark.csproj new file mode 100644 index 000000000..73df4986c --- /dev/null +++ b/src/Starlark/Starlark.csproj @@ -0,0 +1,9 @@ + + + Starlark + Starlark + + + + + diff --git a/src/Starlark/_Placeholder.cs b/src/Starlark/_Placeholder.cs new file mode 100644 index 000000000..2232ac1c2 --- /dev/null +++ b/src/Starlark/_Placeholder.cs @@ -0,0 +1 @@ +namespace Starlark { internal static class _Placeholder { } } diff --git a/tests/Copybara.Tests/Copybara.Tests.csproj b/tests/Copybara.Tests/Copybara.Tests.csproj new file mode 100644 index 000000000..688e0eb60 --- /dev/null +++ b/tests/Copybara.Tests/Copybara.Tests.csproj @@ -0,0 +1,17 @@ + + + Copybara.Tests + false + + + + + + + + + + + + + diff --git a/tests/Copybara.Tests/_Placeholder.cs b/tests/Copybara.Tests/_Placeholder.cs new file mode 100644 index 000000000..11e30260c --- /dev/null +++ b/tests/Copybara.Tests/_Placeholder.cs @@ -0,0 +1 @@ +namespace Copybara.Tests { public class _Placeholder { } } From 60d46082bb226ad6c62ad8bf9a5fab240adba4a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 06:02:57 +0000 Subject: [PATCH 02/15] Add stable Starlark public surface (attributes + core value types) Defines [StarlarkBuiltin]/[StarlarkMethod]/[Param]/[ParamType] attributes, IStarlarkValue, NoneType, EvalException, and the Starlark helper class so domain types can be ported and compiled ahead of the full interpreter. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- src/Copybara.Core/_Placeholder.cs | 1 - src/Starlark/Annot/Annotations.cs | 79 ++++++++++++++++++++++++++++ src/Starlark/Annot/ParamAttribute.cs | 65 +++++++++++++++++++++++ src/Starlark/Eval/StarlarkCore.cs | 68 ++++++++++++++++++++++++ src/Starlark/_Placeholder.cs | 1 - 5 files changed, 212 insertions(+), 2 deletions(-) delete mode 100644 src/Copybara.Core/_Placeholder.cs create mode 100644 src/Starlark/Annot/Annotations.cs create mode 100644 src/Starlark/Annot/ParamAttribute.cs create mode 100644 src/Starlark/Eval/StarlarkCore.cs delete mode 100644 src/Starlark/_Placeholder.cs diff --git a/src/Copybara.Core/_Placeholder.cs b/src/Copybara.Core/_Placeholder.cs deleted file mode 100644 index d4f5e89ac..000000000 --- a/src/Copybara.Core/_Placeholder.cs +++ /dev/null @@ -1 +0,0 @@ -namespace Copybara { internal static class _Placeholder { } } diff --git a/src/Starlark/Annot/Annotations.cs b/src/Starlark/Annot/Annotations.cs new file mode 100644 index 000000000..8b9a0f252 --- /dev/null +++ b/src/Starlark/Annot/Annotations.cs @@ -0,0 +1,79 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Annot; + +/// +/// Marks a class or interface that represents a Starlark data type. Port of +/// net.starlark.java.annot.StarlarkBuiltin. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, Inherited = false)] +public sealed class StarlarkBuiltinAttribute : Attribute +{ + public StarlarkBuiltinAttribute(string name) => Name = name; + + /// The name of this data type, as returned by type(x). + public string Name { get; } + + /// Module documentation in HTML. + public string Doc { get; set; } = ""; + + /// Whether the module should appear in the documentation. + public bool Documented { get; set; } = true; + + /// Documentation category. + public string Category { get; set; } = ""; +} + +/// +/// Annotates a method (or property used as a struct field) callable from Starlark. Port of +/// net.starlark.java.annot.StarlarkMethod. +/// +/// Convention for the .NET port: apply to the method, +/// and describe each argument with a on the corresponding C# +/// parameter (rather than Java's nested parameters = {..} array). Special interpreter-supplied +/// parameters (StarlarkThread, StarlarkSemantics) are matched by C# parameter type. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true)] +public sealed class StarlarkMethodAttribute : Attribute +{ + public StarlarkMethodAttribute(string name) => Name = name; + + /// Name of the method, as exposed to Starlark. + public string Name { get; } + + /// Documentation text (may contain HTML). + public string Doc { get; set; } = ""; + + /// If true, the function appears in the Starlark documentation. + public bool Documented { get; set; } = true; + + /// If true, this member is accessed as a field (bar.foo not bar.foo()). + public bool StructField { get; set; } + + /// If true, the enclosing value is callable and this method handles the call. + public bool SelfCall { get; set; } + + /// Permits a null result, which is converted to None. + public bool AllowReturnNones { get; set; } + + /// If true, the StarlarkThread is passed to the method. + public bool UseStarlarkThread { get; set; } + + /// If true, the StarlarkSemantics is passed to the method. + public bool UseStarlarkSemantics { get; set; } + + /// Whether this method can act as a type in a type expression. + public bool TrustReturnsValid { get; set; } +} diff --git a/src/Starlark/Annot/ParamAttribute.cs b/src/Starlark/Annot/ParamAttribute.cs new file mode 100644 index 000000000..ad6380f89 --- /dev/null +++ b/src/Starlark/Annot/ParamAttribute.cs @@ -0,0 +1,65 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Annot; + +/// +/// Describes a parameter of a -annotated method. In the .NET +/// port this is applied to the corresponding C# parameter. Port of +/// net.starlark.java.annot.Param. +/// +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] +public sealed class ParamAttribute : Attribute +{ + public ParamAttribute() { } + + public ParamAttribute(string name) => Name = name; + + /// The name of the parameter as seen from Starlark. + public string Name { get; set; } = ""; + + /// Documentation for the parameter. + public string Doc { get; set; } = ""; + + /// + /// Default value as a Starlark expression string; empty means the parameter is required. + /// + public string DefaultValue { get; set; } = ""; + + /// Whether the parameter may be specified positionally. + public bool Positional { get; set; } = true; + + /// Whether the parameter may be specified by name. + public bool Named { get; set; } + + /// Whether None is a permissible value. + public bool Noneable { get; set; } + + /// Allowed Starlark types for this parameter (optional documentation/validation). + public Type[] AllowedTypes { get; set; } = Array.Empty(); +} + +/// +/// Declares an allowed type for a parameter. Port of net.starlark.java.annot.ParamType. +/// +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] +public sealed class ParamTypeAttribute : Attribute +{ + public ParamTypeAttribute(Type type) => Type = type; + + public Type Type { get; } + + /// Generic element type, when the declared type is a generic collection. + public Type? Generic1 { get; set; } +} diff --git a/src/Starlark/Eval/StarlarkCore.cs b/src/Starlark/Eval/StarlarkCore.cs new file mode 100644 index 000000000..1cc9fde9d --- /dev/null +++ b/src/Starlark/Eval/StarlarkCore.cs @@ -0,0 +1,68 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Eval; + +/// +/// Marker interface for a value that may be exposed to Starlark. Port of +/// net.starlark.java.eval.StarlarkValue. Members such as Repr, Str, +/// Truth, and IsImmutable are added by the interpreter port (Phase 2); this stable +/// surface exists so domain types can be annotated and compiled ahead of the full interpreter. +/// +public interface IStarlarkValue +{ + /// Whether the value is truthy in a boolean context. Defaults to true. + bool Truth() => true; + + /// Whether the value is deeply immutable. Defaults to true. + bool IsImmutable() => true; +} + +/// The type of the Starlark None value. Port of net.starlark.java.eval.NoneType. +public sealed class NoneType : IStarlarkValue +{ + public static readonly NoneType None = new(); + + private NoneType() { } + + public bool Truth() => false; + + public override string ToString() => "None"; +} + +/// +/// An exception thrown by Starlark evaluation, including by builtin methods invoked from Starlark. +/// Port of net.starlark.java.eval.EvalException. +/// +public class EvalException : Exception +{ + public EvalException(string message) : base(message) { } + + public EvalException(string message, Exception? cause) : base(message, cause) { } +} + +/// +/// Static entry points and helpers of the Starlark interpreter. Port of +/// net.starlark.java.eval.Starlark. Only the surface used by domain code is defined here; +/// the interpreter port (Phase 2) extends this class with evaluation, calling, and value helpers. +/// +public static partial class Starlark +{ + /// The Starlark None value. + public static readonly NoneType None = NoneType.None; + + /// Creates a formatted . Port of Starlark.errorf. + public static EvalException Errorf(string format, params object?[] args) => + new(args.Length == 0 ? format : string.Format(format, args)); +} diff --git a/src/Starlark/_Placeholder.cs b/src/Starlark/_Placeholder.cs deleted file mode 100644 index 2232ac1c2..000000000 --- a/src/Starlark/_Placeholder.cs +++ /dev/null @@ -1 +0,0 @@ -namespace Starlark { internal static class _Placeholder { } } From 7ec6500214e74fb390c961a213665d9509de57c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 06:06:31 +0000 Subject: [PATCH 03/15] Move original README into java/, add .NET port README; record regex decision - Original upstream README moved to java/README.md - New root README describes the C# / .NET 10 port and layout - Record decision to use native .NET regex engine (accepted deviation from re2j) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- CLAUDE.md | 6 +- README.md | 290 +++++++++++-------------------------------------- TODO.md | 5 +- java/README.md | 265 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 335 insertions(+), 231 deletions(-) create mode 100644 java/README.md diff --git a/CLAUDE.md b/CLAUDE.md index 20a3fb7e7..7ea62642c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,9 +108,9 @@ Follow these consistently so the port stays coherent across contributors/agents. Notes / gotchas: -- **re2j is not a drop-in for .NET Regex.** Copybara exposes `core.replace` - regex groups with RE2 semantics. Start with `System.Text.RegularExpressions` - and record any semantic divergence in TODO; consider a real RE2 port later. +- **Regex: use the native .NET engine.** Upstream uses re2j, but this port uses + `System.Text.RegularExpressions` — an accepted deviation. The vast majority of + Copybara patterns behave identically; note any observed divergence in TODO. - **Starlark is the critical path.** Almost every domain object is a `StarlarkValue` with `@StarlarkMethod` fields, and the config loader executes Starlark. The `Starlark` project must land before most `*Module` classes can diff --git a/README.md b/README.md index f43724009..8282eef68 100644 --- a/README.md +++ b/README.md @@ -1,265 +1,103 @@ -# Copybara +# Copybara for .NET -*A tool for transforming and moving code between repositories.* +*A tool for transforming and moving code between repositories — a C# / .NET 10 +port of [Google Copybara](https://github.com/google/copybara).* -Copybara is a tool used internally at Google. It transforms and moves code between repositories. +Copybara transforms and moves source code between repositories. A common use +case is keeping an internal (confidential) repository in sync with a public +one: you declare a *workflow* that reads changes from an **origin**, applies a +chain of **transformations** (move files, replace strings, scrub metadata, …), +and writes the result to a **destination**, recording state in the destination +commit message so the process is stateless and reproducible. -Often, source code needs to exist in multiple repositories, and Copybara allows you to transform -and move source code between these repositories. A common case is a project that involves -maintaining a confidential repository and a public repository in sync. - -Copybara requires you to choose one of the repositories to be the authoritative repository, so that -there is always one source of truth. However, the tool allows contributions to any repository, and -any repository can be used to cut a release. - -The most common use case involves repetitive movement of code from one repository to another. -Copybara can also be used for moving code once to a new repository. - -Examples uses of Copybara include: - - - Importing sections of code from a confidential repository to a public repository. - - - Importing code from a public repository to a confidential repository. - - - Importing a change from a non-authoritative repository into the authoritative repository. When - a change is made in the non-authoritative repository (for example, a contributor in the public - repository), Copybara transforms and moves that change into the appropriate place in the - authoritative repository. Any merge conflicts are dealt with in the same way as an out-of-date - change within the authoritative repository. - -One of the main features of Copybara is that it is stateless, or more specifically, that it stores -the state in the destination repository (As a label in the commit message). This allows several -users (or a service) to use Copybara for the same config/repositories and get the same result. - -Currently, the only supported type of repository is Git. Copybara is also able -to read from Mercurial repositories, but the feature is still experimental. -The extensible architecture allows adding bespoke origins and destinations -for almost any use case. -Official support for other repositories types will be added in the future. - -## Example +Configuration is written in **Starlark** (the Python-like dialect used by +Bazel) in a file conventionally named `copy.bara.sky`. ```python core.workflow( name = "default", origin = git.github_origin( - url = "https://github.com/google/copybara.git", - ref = "master", + url = "https://github.com/google/copybara.git", + ref = "master", ), destination = git.destination( url = "file:///tmp/foo", ), - - # Copy everything but don't remove a README_INTERNAL.txt file if it exists. + # Copy everything but don't remove README_INTERNAL.txt if it exists. destination_files = glob(["third_party/copybara/**"], exclude = ["README_INTERNAL.txt"]), - authoring = authoring.pass_thru("Default email "), transformations = [ core.replace( - before = "//third_party/bazel/bashunit", - after = "//another/path:bashunit", - paths = glob(["**/BUILD"])), - core.move("", "third_party/copybara") + before = "//third_party/bazel/bashunit", + after = "//another/path:bashunit", + ), + core.move("foo/bar", "baz/bar"), ], ) ``` -Run: - -```shell -$ (mkdir /tmp/foo ; cd /tmp/foo ; git init --bare) -$ copybara copy.bara.sky -``` - -## Getting Started using Copybara +## About this repository -The easiest way to start is with weekly "snapshot" releases, that include pre-built a binary. -Note that these are released automatically without any manual testing, version compatibility or correctness guarantees. +This repository is a **port of Copybara from Java to C# targeting .NET 10**, +distributed as a **.NET global tool** named `copybara`. -Choose a release from https://github.com/google/copybara/releases. +- The original Java/Bazel implementation is preserved, unmodified, under + [`java/`](java/) and is the reference for behavior. Its original README lives + at [`java/README.md`](java/README.md). +- The C# port lives under [`src/`](src/) and [`tests/`](tests/). +- [`CLAUDE.md`](CLAUDE.md) documents the architecture, porting conventions, and + Java→C# mappings. [`TODO.md`](TODO.md) is the living work breakdown and status. -### Building from Source +> **Status:** the port is a work in progress. See [`TODO.md`](TODO.md) for what +> is implemented and what remains. -To use an unreleased version of copybara, so you need to compile from HEAD. -In order to do that, you need to do the following: - - * [Install JDK 11](https://www.oracle.com/java/technologies/downloads/#java11). - * [Install Bazel](https://bazel.build/install). - * Clone the copybara source locally: - * `git clone https://github.com/google/copybara.git` - * Build: - * `bazel build //java/com/google/copybara` - * `bazel build //java/com/google/copybara:copybara_deploy.jar` to create an executable uberjar. - * Tests: `bazel test //...` if you want to ensure you are not using a broken version. Note that - certain tests require the underlying tool to be installed(e.g. Mercurial, Quilt, etc.). It is - fine to skip those tests if your Pull Request is unrelated to those modules (And our CI will - run all the tests anyway). - -### System packages - -These packages can be installed using the appropriate package manager for your -system. - -#### Arch Linux - - * [`aur/copybara-git`][install/archlinux/aur-git] - -[install/archlinux/aur-git]: https://aur.archlinux.org/packages/copybara-git "Copybara on the AUR" - -### Using Intellij with Bazel plugin - -If you use Intellij and the Bazel plugin, use this project configuration: +## Layout ``` -directories: - copybara/integration - java/com/google/copybara - javatests/com/google/copybara - third_party - -targets: - //copybara/integration/... - //java/com/google/copybara/... - //javatests/com/google/copybara/... - //third_party/... +src/ + Copybara.Common/ Guava-like helpers (Preconditions, ImmutableListMultimap) + Starlark/ Port of the Starlark interpreter (net.starlark.java) + Copybara.Core/ The engine (origins, destinations, transformations, git, …) + Copybara.Cli/ The `copybara` .NET tool entry point +tests/ + Copybara.Tests/ xUnit tests +java/ The original Java/Bazel implementation (reference only) ``` -Note: configuration files can be stored in any place, even in a local folder. -We recommend using a VCS (like git) to store them; treat them as source code. - -### Using pre-built Copybara in Bazel - -If using a weekly snapshot release, install Copybara as follows: - -1. Copybara ships with class files with version 65.0, so it must be run with Java Runtime 21 or greater. Add to your `.bazelrc` file: `run --java_runtime_version=remotejdk_21` -2. Use `http_jar` to download the release artifact. - - In WORKSPACE: `load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar")` - - In MODULE.bazel: `http_jar = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar")` -3. In WORKSPACE or MODULE.bazel, fill in the `[version]` placeholder: - ```starlark - http_jar( - name = "com_github_google_copybara", - # Fill in from https://github.com/google/copybara/releases/download/[version]/copybara_deploy.jar.sha256 - # sha256 = "", - urls = ["https://github.com/google/copybara/releases/download/[version]/copybara_deploy.jar"], - ) - ``` -4. In any BUILD file (perhaps `/tools/BUILD.bazel`) declare the `java_binary`: - ```starlark - load("@rules_java//java:java_binary.bzl", "java_binary") - java_binary( - name = "copybara", - main_class = "com.google.copybara.Main", - runtime_deps = ["@com_github_google_copybara//jar"], - ) - ``` -5. Use that target with `bazel run`, for example `bazel run //tools:copybara -- migrate copy.bara.sky` - -### Building Copybara from Source as an external Bazel repository +## Building -There are convenience macros defined for all of Copybara's dependencies. Add the -following code to your `WORKSPACE` file, replacing `{{ sha256sum }}` and -`{{ commit }}` as necessary. +Requires the **.NET 10 SDK**. -```bzl -http_archive( - name = "com_github_google_copybara", - sha256 = "{{ sha256sum }}", - strip_prefix = "copybara-{{ commit }}", - url = "https://github.com/google/copybara/archive/{{ commit }}.zip", -) - -load("@com_github_google_copybara//:repositories.bzl", "copybara_repositories") - -copybara_repositories() - -load("@com_github_google_copybara//:repositories.maven.bzl", "copybara_maven_repositories") - -copybara_maven_repositories() - -load("@com_github_google_copybara//:repositories.go.bzl", "copybara_go_repositories") - -copybara_go_repositories() -``` - -You can then build and run the Copybara tool from within your workspace: - -```sh -bazel run @com_github_google_copybara//java/com/google/copybara -- -``` - -### Using Docker to build and run Copybara - -*NOTE: Docker use is currently experimental, and we encourage feedback or contributions.* - -You can build copybara using Docker like so - -```sh -docker build --rm -t copybara . +```bash +dotnet build Copybara.slnx +dotnet test Copybara.slnx ``` -Once this has finished building, you can run the image like so from the root of -the code you are trying to use Copybara on: - -```sh -docker run -it -v "$(pwd)":/usr/src/app copybara help -``` +## Running -#### Environment variables +```bash +# From source: +dotnet run --project src/Copybara.Cli -- migrate copy.bara.sky -In addition to passing cmd args to the container, you can also set the following -environment variables as an alternative: -* `COPYBARA_SUBCOMMAND=migrate` - * allows you to change the command run, defaults to `migrate` -* `COPYBARA_CONFIG=copy.bara.sky` - * allows you to specify a path to a config file, defaults to root `copy.bara.sky` -* `COPYBARA_WORKFLOW=default` - * allows you to specify the workflow to run, defaults to `default` -* `COPYBARA_SOURCEREF=''` - * allows you to specify the sourceref, defaults to none -* `COPYBARA_OPTIONS=''` - * allows you to specify options for copybara, defaults to none - -```sh -docker run \ - -e COPYBARA_SUBCOMMAND='validate' \ - -e COPYBARA_CONFIG='other.config.sky' \ - -v "$(pwd)":/usr/src/app \ - -it copybara +# Packaged as a global tool: +dotnet pack src/Copybara.Cli -c Release +dotnet tool install --global --add-source src/Copybara.Cli/nupkg Copybara +copybara migrate copy.bara.sky ``` -#### Git Config and Credentials - -There are a number of ways by which to share your git config and ssh credentials -with the Docker container, an example is below: - -```sh -docker run \ - -v ~/.gitconfig:/root/.gitconfig:ro \ - -v ~/.ssh:/root/.ssh \ - -v ${SSH_AUTH_SOCK}:${SSH_AUTH_SOCK} -e SSH_AUTH_SOCK - -v "$(pwd)":/usr/src/app \ - -it copybara -``` - -## Documentation - -We are still working on the documentation. Here are some resources: - - * [Reference documentation](docs/reference.md) - * [Examples](docs/examples.md) - * [Tutorial on how to get started](https://blog.kubesimplify.com/moving-code-between-git-repositories-with-copybara) - -## Contact us - -If you have any questions about how Copybara works, please contact us at our -[mailing list](https://groups.google.com/forum/#!forum/copybara-discuss). +## Notable porting decisions -## Optional tips +- **Git** operations use the [`LibGit2Sharp`](https://www.nuget.org/packages/LibGit2Sharp) + NuGet package, with a fallback to invoking the `git` binary for features it + does not cover. +- **Regular expressions** use the native .NET regex engine + (`System.Text.RegularExpressions`) rather than RE2. This is an accepted + deviation from upstream (which uses re2j); the vast majority of Copybara + patterns behave identically. +- Starlark configuration semantics are preserved by porting the Bazel Starlark + interpreter to C# (see `src/Starlark`). -* If you want to see the test errors in Bazel, instead of having to `cat` the - logs, add this line to your `~/.bazelrc`: +## License - ``` - test --test_output=streamed - ``` +Apache License 2.0 — see [`LICENSE`](LICENSE). Copybara is a trademark of +Google LLC; this is an independent port. diff --git a/TODO.md b/TODO.md index 416c6ffc9..f32a20db2 100644 --- a/TODO.md +++ b/TODO.md @@ -139,8 +139,9 @@ Largest single module (175 files). Port in slices: ## Cross-cutting decisions / open questions -- 🔬 **RE2 vs .NET Regex** for `core.replace` — start with .NET Regex, flag - divergences. +- ✅ **RE2 vs .NET Regex** — DECIDED: use the native .NET regex engine + (`System.Text.RegularExpressions`). Accepted deviation from upstream re2j. + Flag any divergences observed during porting/testing. - 🔬 **Async vs sync** — repo I/O is naturally async in .NET; upstream is blocking. Decide per-boundary; keep the engine mostly synchronous initially to stay close to the source, use async only at process/network edges. diff --git a/java/README.md b/java/README.md new file mode 100644 index 000000000..f43724009 --- /dev/null +++ b/java/README.md @@ -0,0 +1,265 @@ +# Copybara + +*A tool for transforming and moving code between repositories.* + +Copybara is a tool used internally at Google. It transforms and moves code between repositories. + +Often, source code needs to exist in multiple repositories, and Copybara allows you to transform +and move source code between these repositories. A common case is a project that involves +maintaining a confidential repository and a public repository in sync. + +Copybara requires you to choose one of the repositories to be the authoritative repository, so that +there is always one source of truth. However, the tool allows contributions to any repository, and +any repository can be used to cut a release. + +The most common use case involves repetitive movement of code from one repository to another. +Copybara can also be used for moving code once to a new repository. + +Examples uses of Copybara include: + + - Importing sections of code from a confidential repository to a public repository. + + - Importing code from a public repository to a confidential repository. + + - Importing a change from a non-authoritative repository into the authoritative repository. When + a change is made in the non-authoritative repository (for example, a contributor in the public + repository), Copybara transforms and moves that change into the appropriate place in the + authoritative repository. Any merge conflicts are dealt with in the same way as an out-of-date + change within the authoritative repository. + +One of the main features of Copybara is that it is stateless, or more specifically, that it stores +the state in the destination repository (As a label in the commit message). This allows several +users (or a service) to use Copybara for the same config/repositories and get the same result. + +Currently, the only supported type of repository is Git. Copybara is also able +to read from Mercurial repositories, but the feature is still experimental. +The extensible architecture allows adding bespoke origins and destinations +for almost any use case. +Official support for other repositories types will be added in the future. + +## Example + +```python +core.workflow( + name = "default", + origin = git.github_origin( + url = "https://github.com/google/copybara.git", + ref = "master", + ), + destination = git.destination( + url = "file:///tmp/foo", + ), + + # Copy everything but don't remove a README_INTERNAL.txt file if it exists. + destination_files = glob(["third_party/copybara/**"], exclude = ["README_INTERNAL.txt"]), + + authoring = authoring.pass_thru("Default email "), + transformations = [ + core.replace( + before = "//third_party/bazel/bashunit", + after = "//another/path:bashunit", + paths = glob(["**/BUILD"])), + core.move("", "third_party/copybara") + ], +) +``` + +Run: + +```shell +$ (mkdir /tmp/foo ; cd /tmp/foo ; git init --bare) +$ copybara copy.bara.sky +``` + +## Getting Started using Copybara + +The easiest way to start is with weekly "snapshot" releases, that include pre-built a binary. +Note that these are released automatically without any manual testing, version compatibility or correctness guarantees. + +Choose a release from https://github.com/google/copybara/releases. + +### Building from Source + +To use an unreleased version of copybara, so you need to compile from HEAD. +In order to do that, you need to do the following: + + * [Install JDK 11](https://www.oracle.com/java/technologies/downloads/#java11). + * [Install Bazel](https://bazel.build/install). + * Clone the copybara source locally: + * `git clone https://github.com/google/copybara.git` + * Build: + * `bazel build //java/com/google/copybara` + * `bazel build //java/com/google/copybara:copybara_deploy.jar` to create an executable uberjar. + * Tests: `bazel test //...` if you want to ensure you are not using a broken version. Note that + certain tests require the underlying tool to be installed(e.g. Mercurial, Quilt, etc.). It is + fine to skip those tests if your Pull Request is unrelated to those modules (And our CI will + run all the tests anyway). + +### System packages + +These packages can be installed using the appropriate package manager for your +system. + +#### Arch Linux + + * [`aur/copybara-git`][install/archlinux/aur-git] + +[install/archlinux/aur-git]: https://aur.archlinux.org/packages/copybara-git "Copybara on the AUR" + +### Using Intellij with Bazel plugin + +If you use Intellij and the Bazel plugin, use this project configuration: + +``` +directories: + copybara/integration + java/com/google/copybara + javatests/com/google/copybara + third_party + +targets: + //copybara/integration/... + //java/com/google/copybara/... + //javatests/com/google/copybara/... + //third_party/... +``` + +Note: configuration files can be stored in any place, even in a local folder. +We recommend using a VCS (like git) to store them; treat them as source code. + +### Using pre-built Copybara in Bazel + +If using a weekly snapshot release, install Copybara as follows: + +1. Copybara ships with class files with version 65.0, so it must be run with Java Runtime 21 or greater. Add to your `.bazelrc` file: `run --java_runtime_version=remotejdk_21` +2. Use `http_jar` to download the release artifact. + - In WORKSPACE: `load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar")` + - In MODULE.bazel: `http_jar = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar")` +3. In WORKSPACE or MODULE.bazel, fill in the `[version]` placeholder: + ```starlark + http_jar( + name = "com_github_google_copybara", + # Fill in from https://github.com/google/copybara/releases/download/[version]/copybara_deploy.jar.sha256 + # sha256 = "", + urls = ["https://github.com/google/copybara/releases/download/[version]/copybara_deploy.jar"], + ) + ``` +4. In any BUILD file (perhaps `/tools/BUILD.bazel`) declare the `java_binary`: + ```starlark + load("@rules_java//java:java_binary.bzl", "java_binary") + java_binary( + name = "copybara", + main_class = "com.google.copybara.Main", + runtime_deps = ["@com_github_google_copybara//jar"], + ) + ``` +5. Use that target with `bazel run`, for example `bazel run //tools:copybara -- migrate copy.bara.sky` + +### Building Copybara from Source as an external Bazel repository + +There are convenience macros defined for all of Copybara's dependencies. Add the +following code to your `WORKSPACE` file, replacing `{{ sha256sum }}` and +`{{ commit }}` as necessary. + +```bzl +http_archive( + name = "com_github_google_copybara", + sha256 = "{{ sha256sum }}", + strip_prefix = "copybara-{{ commit }}", + url = "https://github.com/google/copybara/archive/{{ commit }}.zip", +) + +load("@com_github_google_copybara//:repositories.bzl", "copybara_repositories") + +copybara_repositories() + +load("@com_github_google_copybara//:repositories.maven.bzl", "copybara_maven_repositories") + +copybara_maven_repositories() + +load("@com_github_google_copybara//:repositories.go.bzl", "copybara_go_repositories") + +copybara_go_repositories() +``` + +You can then build and run the Copybara tool from within your workspace: + +```sh +bazel run @com_github_google_copybara//java/com/google/copybara -- +``` + +### Using Docker to build and run Copybara + +*NOTE: Docker use is currently experimental, and we encourage feedback or contributions.* + +You can build copybara using Docker like so + +```sh +docker build --rm -t copybara . +``` + +Once this has finished building, you can run the image like so from the root of +the code you are trying to use Copybara on: + +```sh +docker run -it -v "$(pwd)":/usr/src/app copybara help +``` + +#### Environment variables + +In addition to passing cmd args to the container, you can also set the following +environment variables as an alternative: +* `COPYBARA_SUBCOMMAND=migrate` + * allows you to change the command run, defaults to `migrate` +* `COPYBARA_CONFIG=copy.bara.sky` + * allows you to specify a path to a config file, defaults to root `copy.bara.sky` +* `COPYBARA_WORKFLOW=default` + * allows you to specify the workflow to run, defaults to `default` +* `COPYBARA_SOURCEREF=''` + * allows you to specify the sourceref, defaults to none +* `COPYBARA_OPTIONS=''` + * allows you to specify options for copybara, defaults to none + +```sh +docker run \ + -e COPYBARA_SUBCOMMAND='validate' \ + -e COPYBARA_CONFIG='other.config.sky' \ + -v "$(pwd)":/usr/src/app \ + -it copybara +``` + +#### Git Config and Credentials + +There are a number of ways by which to share your git config and ssh credentials +with the Docker container, an example is below: + +```sh +docker run \ + -v ~/.gitconfig:/root/.gitconfig:ro \ + -v ~/.ssh:/root/.ssh \ + -v ${SSH_AUTH_SOCK}:${SSH_AUTH_SOCK} -e SSH_AUTH_SOCK + -v "$(pwd)":/usr/src/app \ + -it copybara +``` + +## Documentation + +We are still working on the documentation. Here are some resources: + + * [Reference documentation](docs/reference.md) + * [Examples](docs/examples.md) + * [Tutorial on how to get started](https://blog.kubesimplify.com/moving-code-between-git-repositories-with-copybara) + +## Contact us + +If you have any questions about how Copybara works, please contact us at our +[mailing list](https://groups.google.com/forum/#!forum/copybara-discuss). + +## Optional tips + +* If you want to see the test errors in Bazel, instead of having to `cat` the + logs, add this line to your `~/.bazelrc`: + + ``` + test --test_output=streamed + ``` From fc4984d6f133b8d4b6609c11551dcb387ab12ead Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 06:11:46 +0000 Subject: [PATCH 04/15] WIP: port foundation modules to C# (exceptions, console, revision, authoring) Snapshot of in-progress .NET port. Not yet compiling end-to-end: the Starlark interpreter (syntax + eval) is still being ported, so Copybara.Core does not build yet. Consolidation/green-build pass to follow. Modules landed: Copybara.Exceptions, Copybara.Util.Console, Copybara.Revision, Copybara.Authoring, Copybara.TemplateToken. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- src/Copybara.Core/Authoring/Author.cs | 85 +++ src/Copybara.Core/Authoring/AuthorParser.cs | 83 +++ src/Copybara.Core/Authoring/Authoring.cs | 296 ++++++++ .../Authoring/InvalidAuthorException.cs | 32 + .../Exceptions/AccessValidationException.cs | 34 + .../Exceptions/CannotResolveLabel.cs | 33 + .../CannotResolveRevisionException.cs | 33 + .../Exceptions/ChangeRejectedException.cs | 28 + .../Exceptions/CommandLineException.cs | 33 + .../Exceptions/EmptyChangeException.cs | 34 + .../NonReversibleValidationException.cs | 36 + .../NotADestinationFileException.cs | 29 + .../Exceptions/RedundantChangeException.cs | 42 ++ src/Copybara.Core/Exceptions/RepoException.cs | 33 + .../Exceptions/ValidationException.cs | 125 ++++ .../Exceptions/VoidOperationException.cs | 29 + src/Copybara.Core/Revision/Change.cs | 211 ++++++ src/Copybara.Core/Revision/Changes.cs | 60 ++ src/Copybara.Core/Revision/OriginRef.cs | 57 ++ src/Copybara.Core/Revision/Revision.cs | 119 ++++ .../TemplateToken/LabelTemplate.cs | 83 +++ src/Copybara.Core/TemplateToken/Parser.cs | 88 +++ .../TemplateToken/RegexTemplateTokens.cs | 447 ++++++++++++ src/Copybara.Core/TemplateToken/Token.cs | 58 ++ .../Util/BadExitStatusWithOutputException.cs | 36 + src/Copybara.Core/Util/Command.cs | 62 ++ src/Copybara.Core/Util/CommandException.cs | 66 ++ src/Copybara.Core/Util/CommandOutput.cs | 45 ++ .../Util/CommandOutputWithStatus.cs | 39 ++ src/Copybara.Core/Util/CommandResult.cs | 40 ++ src/Copybara.Core/Util/CommandRunner.cs | 421 ++++++++++++ .../Util/CommandTimeoutException.cs | 50 ++ src/Copybara.Core/Util/Console/AnsiColor.cs | 60 ++ src/Copybara.Core/Util/Console/AnsiConsole.cs | 161 +++++ src/Copybara.Core/Util/Console/AnsiEscapes.cs | 24 + .../Util/Console/CapturingConsole.cs | 92 +++ src/Copybara.Core/Util/Console/Console.cs | 163 +++++ .../Util/Console/ConsoleFormat.cs | 92 +++ .../Util/Console/ConsolePrompt.cs | 72 ++ src/Copybara.Core/Util/Console/Consoles.cs | 98 +++ .../Util/Console/DelegateConsole.cs | 99 +++ .../Util/Console/EnhancedPredicate.cs | 43 ++ src/Copybara.Core/Util/Console/FileConsole.cs | 177 +++++ src/Copybara.Core/Util/Console/LogConsole.cs | 89 +++ src/Copybara.Core/Util/Console/Message.cs | 75 ++ .../Util/Console/MultiplexingConsole.cs | 57 ++ .../Util/Console/NoPromptConsole.cs | 44 ++ .../Util/Console/PrefixConsole.cs | 57 ++ .../Util/Console/StarlarkMode.cs | 26 + src/Copybara.Core/Util/DirFactory.cs | 68 ++ src/Copybara.Core/Util/ExitCode.cs | 72 ++ src/Copybara.Core/Util/GlobAtom.cs | 255 +++++++ src/Copybara.Core/Util/GlobPathMatcher.cs | 233 +++++++ src/Copybara.Core/Util/IPathMatcher.cs | 28 + src/Copybara.Core/Util/Identity.cs | 96 +++ .../Util/InsideGitDirException.cs | 36 + .../Util/LimitFilterOutputStream.cs | 122 ++++ src/Copybara.Core/Util/ReadablePathMatcher.cs | 65 ++ src/Copybara.Core/Util/ShellUtils.cs | 67 ++ src/Copybara.Core/Util/SymlinkException.cs | 28 + src/Copybara.Core/Util/TablePrinter.cs | 94 +++ src/Copybara.Core/Util/TerminationStatus.cs | 109 +++ src/Starlark/Eval/Mutability.cs | 139 ++++ src/Starlark/Eval/Printer.cs | 382 +++++++++++ src/Starlark/Eval/RangeList.cs | 177 +++++ src/Starlark/Eval/Sequence.cs | 132 ++++ src/Starlark/Eval/StarlarkFloat.cs | 300 ++++++++ src/Starlark/Eval/StarlarkInt.cs | 645 ++++++++++++++++++ src/Starlark/Eval/StarlarkSemantics.cs | 222 ++++++ src/Starlark/Eval/StarlarkValueInterfaces.cs | 114 ++++ src/Starlark/Syntax/Argument.cs | 102 +++ .../Syntax/BinaryOperatorExpression.cs | 75 ++ src/Starlark/Syntax/CallExpression.cs | 80 +++ src/Starlark/Syntax/Comment.cs | 56 ++ src/Starlark/Syntax/ConditionalExpression.cs | 44 ++ src/Starlark/Syntax/DictExpression.cs | 69 ++ src/Starlark/Syntax/DotExpression.cs | 44 ++ src/Starlark/Syntax/Ellipsis.cs | 35 + src/Starlark/Syntax/Expression.cs | 78 +++ src/Starlark/Syntax/FileLocations.cs | 129 ++++ src/Starlark/Syntax/FileOptions.cs | 162 +++++ src/Starlark/Syntax/FloatLiteral.cs | 46 ++ src/Starlark/Syntax/Identifier.cs | 107 +++ src/Starlark/Syntax/IndexExpression.cs | 49 ++ src/Starlark/Syntax/IntLiteral.cs | 181 +++++ src/Starlark/Syntax/ListExpression.cs | 96 +++ src/Starlark/Syntax/Location.cs | 115 ++++ src/Starlark/Syntax/Node.cs | 74 ++ src/Starlark/Syntax/Parameter.cs | 128 ++++ src/Starlark/Syntax/ParserInput.cs | 96 +++ src/Starlark/Syntax/SliceExpression.cs | 60 ++ src/Starlark/Syntax/Statement.cs | 45 ++ src/Starlark/Syntax/StringLiteral.cs | 72 ++ src/Starlark/Syntax/SyntaxError.cs | 82 +++ src/Starlark/Syntax/TokenKind.cs | 223 ++++++ .../Syntax/UnaryOperatorExpression.cs | 46 ++ 96 files changed, 9974 insertions(+) create mode 100644 src/Copybara.Core/Authoring/Author.cs create mode 100644 src/Copybara.Core/Authoring/AuthorParser.cs create mode 100644 src/Copybara.Core/Authoring/Authoring.cs create mode 100644 src/Copybara.Core/Authoring/InvalidAuthorException.cs create mode 100644 src/Copybara.Core/Exceptions/AccessValidationException.cs create mode 100644 src/Copybara.Core/Exceptions/CannotResolveLabel.cs create mode 100644 src/Copybara.Core/Exceptions/CannotResolveRevisionException.cs create mode 100644 src/Copybara.Core/Exceptions/ChangeRejectedException.cs create mode 100644 src/Copybara.Core/Exceptions/CommandLineException.cs create mode 100644 src/Copybara.Core/Exceptions/EmptyChangeException.cs create mode 100644 src/Copybara.Core/Exceptions/NonReversibleValidationException.cs create mode 100644 src/Copybara.Core/Exceptions/NotADestinationFileException.cs create mode 100644 src/Copybara.Core/Exceptions/RedundantChangeException.cs create mode 100644 src/Copybara.Core/Exceptions/RepoException.cs create mode 100644 src/Copybara.Core/Exceptions/ValidationException.cs create mode 100644 src/Copybara.Core/Exceptions/VoidOperationException.cs create mode 100644 src/Copybara.Core/Revision/Change.cs create mode 100644 src/Copybara.Core/Revision/Changes.cs create mode 100644 src/Copybara.Core/Revision/OriginRef.cs create mode 100644 src/Copybara.Core/Revision/Revision.cs create mode 100644 src/Copybara.Core/TemplateToken/LabelTemplate.cs create mode 100644 src/Copybara.Core/TemplateToken/Parser.cs create mode 100644 src/Copybara.Core/TemplateToken/RegexTemplateTokens.cs create mode 100644 src/Copybara.Core/TemplateToken/Token.cs create mode 100644 src/Copybara.Core/Util/BadExitStatusWithOutputException.cs create mode 100644 src/Copybara.Core/Util/Command.cs create mode 100644 src/Copybara.Core/Util/CommandException.cs create mode 100644 src/Copybara.Core/Util/CommandOutput.cs create mode 100644 src/Copybara.Core/Util/CommandOutputWithStatus.cs create mode 100644 src/Copybara.Core/Util/CommandResult.cs create mode 100644 src/Copybara.Core/Util/CommandRunner.cs create mode 100644 src/Copybara.Core/Util/CommandTimeoutException.cs create mode 100644 src/Copybara.Core/Util/Console/AnsiColor.cs create mode 100644 src/Copybara.Core/Util/Console/AnsiConsole.cs create mode 100644 src/Copybara.Core/Util/Console/AnsiEscapes.cs create mode 100644 src/Copybara.Core/Util/Console/CapturingConsole.cs create mode 100644 src/Copybara.Core/Util/Console/Console.cs create mode 100644 src/Copybara.Core/Util/Console/ConsoleFormat.cs create mode 100644 src/Copybara.Core/Util/Console/ConsolePrompt.cs create mode 100644 src/Copybara.Core/Util/Console/Consoles.cs create mode 100644 src/Copybara.Core/Util/Console/DelegateConsole.cs create mode 100644 src/Copybara.Core/Util/Console/EnhancedPredicate.cs create mode 100644 src/Copybara.Core/Util/Console/FileConsole.cs create mode 100644 src/Copybara.Core/Util/Console/LogConsole.cs create mode 100644 src/Copybara.Core/Util/Console/Message.cs create mode 100644 src/Copybara.Core/Util/Console/MultiplexingConsole.cs create mode 100644 src/Copybara.Core/Util/Console/NoPromptConsole.cs create mode 100644 src/Copybara.Core/Util/Console/PrefixConsole.cs create mode 100644 src/Copybara.Core/Util/Console/StarlarkMode.cs create mode 100644 src/Copybara.Core/Util/DirFactory.cs create mode 100644 src/Copybara.Core/Util/ExitCode.cs create mode 100644 src/Copybara.Core/Util/GlobAtom.cs create mode 100644 src/Copybara.Core/Util/GlobPathMatcher.cs create mode 100644 src/Copybara.Core/Util/IPathMatcher.cs create mode 100644 src/Copybara.Core/Util/Identity.cs create mode 100644 src/Copybara.Core/Util/InsideGitDirException.cs create mode 100644 src/Copybara.Core/Util/LimitFilterOutputStream.cs create mode 100644 src/Copybara.Core/Util/ReadablePathMatcher.cs create mode 100644 src/Copybara.Core/Util/ShellUtils.cs create mode 100644 src/Copybara.Core/Util/SymlinkException.cs create mode 100644 src/Copybara.Core/Util/TablePrinter.cs create mode 100644 src/Copybara.Core/Util/TerminationStatus.cs create mode 100644 src/Starlark/Eval/Mutability.cs create mode 100644 src/Starlark/Eval/Printer.cs create mode 100644 src/Starlark/Eval/RangeList.cs create mode 100644 src/Starlark/Eval/Sequence.cs create mode 100644 src/Starlark/Eval/StarlarkFloat.cs create mode 100644 src/Starlark/Eval/StarlarkInt.cs create mode 100644 src/Starlark/Eval/StarlarkSemantics.cs create mode 100644 src/Starlark/Eval/StarlarkValueInterfaces.cs create mode 100644 src/Starlark/Syntax/Argument.cs create mode 100644 src/Starlark/Syntax/BinaryOperatorExpression.cs create mode 100644 src/Starlark/Syntax/CallExpression.cs create mode 100644 src/Starlark/Syntax/Comment.cs create mode 100644 src/Starlark/Syntax/ConditionalExpression.cs create mode 100644 src/Starlark/Syntax/DictExpression.cs create mode 100644 src/Starlark/Syntax/DotExpression.cs create mode 100644 src/Starlark/Syntax/Ellipsis.cs create mode 100644 src/Starlark/Syntax/Expression.cs create mode 100644 src/Starlark/Syntax/FileLocations.cs create mode 100644 src/Starlark/Syntax/FileOptions.cs create mode 100644 src/Starlark/Syntax/FloatLiteral.cs create mode 100644 src/Starlark/Syntax/Identifier.cs create mode 100644 src/Starlark/Syntax/IndexExpression.cs create mode 100644 src/Starlark/Syntax/IntLiteral.cs create mode 100644 src/Starlark/Syntax/ListExpression.cs create mode 100644 src/Starlark/Syntax/Location.cs create mode 100644 src/Starlark/Syntax/Node.cs create mode 100644 src/Starlark/Syntax/Parameter.cs create mode 100644 src/Starlark/Syntax/ParserInput.cs create mode 100644 src/Starlark/Syntax/SliceExpression.cs create mode 100644 src/Starlark/Syntax/Statement.cs create mode 100644 src/Starlark/Syntax/StringLiteral.cs create mode 100644 src/Starlark/Syntax/SyntaxError.cs create mode 100644 src/Starlark/Syntax/TokenKind.cs create mode 100644 src/Starlark/Syntax/UnaryOperatorExpression.cs diff --git a/src/Copybara.Core/Authoring/Author.cs b/src/Copybara.Core/Authoring/Author.cs new file mode 100644 index 000000000..252d5d37e --- /dev/null +++ b/src/Copybara.Core/Authoring/Author.cs @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Authoring; + +/// +/// Represents the contributor of a change in the destination repository. A contributor can be either +/// an individual or a team. +/// +/// Author is lenient in name or email validation. +/// +[StarlarkBuiltin("author", Doc = "Represents the author of a change")] +public sealed class Author : IStarlarkValue +{ + private readonly string _name; + private readonly string _email; + + public Author(string name, string email) + { + _name = Preconditions.CheckNotNull(name); + _email = Preconditions.CheckNotNull(email); + } + + /// Returns the name of the author. + [StarlarkMethod("name", Doc = "The name of the author", StructField = true)] + public string Name => _name; + + /// Returns the email address of the author. + [StarlarkMethod("email", Doc = "The email of the author", StructField = true)] + public string Email => _email; + + /// + /// Returns the string representation of an author, which is the standard format + /// Name <email> used by most version control systems. + /// + public override string ToString() => $"{_name} <{_email}>"; + + public override bool Equals(object? o) + { + if (o is Author that) + { + // Authors with the same non-empty email are the same author. + return string.IsNullOrEmpty(_email) && string.IsNullOrEmpty(that._email) + ? string.Equals(_name, that._name) + : string.Equals(_email, that._email); + } + return false; + } + + /// Parse author from a String in the format of: "name <foo@bar.com>". + public static Author Parse(string authorStr) + { + try + { + return AuthorParser.Parse(authorStr); + } + catch (InvalidAuthorException e) + { + throw Starlark.Eval.Starlark.Errorf( + "Author '{0}' doesn't match the expected format 'name : {1}", + authorStr, + e.Message); + } + } + + public override int GetHashCode() => + string.IsNullOrEmpty(_email) ? _name.GetHashCode() : _email.GetHashCode(); +} diff --git a/src/Copybara.Core/Authoring/AuthorParser.cs b/src/Copybara.Core/Authoring/AuthorParser.cs new file mode 100644 index 000000000..49fb36e88 --- /dev/null +++ b/src/Copybara.Core/Authoring/AuthorParser.cs @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Concurrent; +using System.Text.RegularExpressions; +using Copybara.Common; + +namespace Copybara.Authoring; + +/// +/// A parser for the standard author format "Name <email>". +/// +/// This is the format used by most VCS (Git, Mercurial) and also by the Copybara configuration +/// itself. The parser is lenient: email can be empty, and it doesn't validate that is an +/// actual email. +/// +public class AuthorParser +{ + // Anchored to emulate Java Matcher.matches(), which requires the entire input to match. + private static readonly Regex AuthorPattern = + new(@"\A(?[^<]+)<(?[^>]*)>\z"); + + // The cache mirrors Guava's LoadingCache used by Copybara for repeated author loads. + private static readonly ConcurrentDictionary Cache = new(); + + /// Parses a Git author string into an . + public static Author Parse(string author) + { + Preconditions.CheckNotNull(author); + // Use a cache since repetitive load (thru --read-config-from-change) configs that + // define authors have a penalty because of the regex check/group. + if (Cache.TryGetValue(author, out var cached)) + { + return cached; + } + var parsed = InternalParse(author); + Cache[author] = parsed; + return parsed; + } + + private static Author InternalParse(string author) + { + if (IsInQuotes(author)) + { + author = author.Substring(1, author.Length - 2); // strip quotes + } + Match matcher = AuthorPattern.Match(author); + if (matcher.Success) + { + return new Author(matcher.Groups[1].Value.Trim(), matcher.Groups[2].Value.Trim()); + } + throw new InvalidAuthorException( + $"Invalid author '{author}'. Must be in the form of 'Name '"); + } + + private static bool IsInQuotes(string author) + { + // Equivalent to re2j pattern ("(\".+\")|(\'.+\')") with full-string match semantics: + // either a double-quoted or single-quoted string whose contents are at least one char. + if (author.Length >= 3 && author[0] == '"' && author[^1] == '"') + { + return true; + } + if (author.Length >= 3 && author[0] == '\'' && author[^1] == '\'') + { + return true; + } + return false; + } +} diff --git a/src/Copybara.Core/Authoring/Authoring.cs b/src/Copybara.Core/Authoring/Authoring.cs new file mode 100644 index 000000000..4f2ea4dec --- /dev/null +++ b/src/Copybara.Core/Authoring/Authoring.cs @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Authoring; + +/// +/// Represents the authors mapping between an origin and a destination. +/// +/// For a given author in the origin, always provides an author in the destination. +/// +[StarlarkBuiltin( + "authoring_class", + Doc = "The authors mapping between an origin and a destination")] +public sealed class Authoring : IStarlarkValue +{ + private readonly Author _defaultAuthor; + private readonly AuthoringMappingMode _mode; + private readonly IEvalThrowingPredicate _allowPredicate; + + public Authoring( + Author defaultAuthor, + AuthoringMappingMode mode, + IEvalThrowingPredicate allowPredicate) + { + _defaultAuthor = Preconditions.CheckNotNull(defaultAuthor); + _mode = mode; + _allowPredicate = Preconditions.CheckNotNull(allowPredicate); + } + + public Authoring(Author defaultAuthor, AuthoringMappingMode mode, ImmutableHashSet list) + : this(defaultAuthor, mode, new AllowlistPredicate(Preconditions.CheckNotNull(list))) + { + } + + /// Returns the mapping mode. + public AuthoringMappingMode GetMode() => _mode; + + /// + /// Returns the default author, used for squash workflows, + /// mode and for non-allowed authors. + /// + public Author GetDefaultAuthor() => _defaultAuthor; + + /// + /// Returns a predicate over allowed author identifiers. + /// + /// An identifier is typically an email but might have different representations depending + /// on the origin. + /// + public IEvalThrowingPredicate GetAllowPredicate() => _allowPredicate; + + /// Returns true if the user can be safely used. + public bool UseAuthor(string userId) => + _mode switch + { + AuthoringMappingMode.PassThru => true, + AuthoringMappingMode.Overwrite => false, + AuthoringMappingMode.Allowed => _allowPredicate.Test(userId), + _ => throw new InvalidOperationException($"Unexpected mode: {_mode}"), + }; + + /// Starlark Module for authoring. + [StarlarkBuiltin( + "authoring", + Doc = "The authors mapping between an origin and a destination")] + public sealed class Module : IStarlarkValue + { + [StarlarkMethod( + "overwrite", + Doc = + "Use the default author for all the submits in the destination. Note that some" + + " destinations might choose to ignore this author and use the current user" + + " running the tool (In other words they don't allow impersonation).")] + public Authoring Overwrite( + [Param(Name = "default", Named = true, Doc = "The default author for commits in the destination")] + string defaultAuthor) => + new(Author.Parse(defaultAuthor), AuthoringMappingMode.Overwrite, new RejectAllPredicate()); + + [StarlarkMethod( + "pass_thru", + Doc = "Use the origin author as the author in the destination, no filtering.")] + public Authoring PassThru( + [Param( + Name = "default", + Named = true, + Doc = + "The default author for commits in the destination. This is used" + + " in squash mode workflows or if author cannot be determined.")] + string defaultAuthor) => + new(Author.Parse(defaultAuthor), AuthoringMappingMode.PassThru, new RejectAllPredicate()); + + [StarlarkMethod( + "allowed", + Doc = "Create a list for an individual or team contributing code.", + UseStarlarkThread = true)] + public Authoring Allowed( + [Param( + Name = "default", + Named = true, + Doc = + "The default author for commits in the destination. This is used" + + " in squash mode workflows or when users are not on the list.")] + string defaultAuthor, + [Param( + Name = "allowlist", + Named = true, + DefaultValue = "None", + Doc = + "List of authors in the origin that are allowed to contribute code. The " + + "authors must be unique")] + IEnumerable? allowlist, + [Param( + Name = "allow_predicate", + Named = true, + DefaultValue = "None", + Doc = + "Starlark function to use to check if an author is allowed to contribute code." + + " The function should take a single argument (the author) and return" + + " true if the author is allowed, false otherwise. Allowlist is ignored if" + + " this is set.")] + IEvalThrowingPredicate? allowPred) + { + IEvalThrowingPredicate allowPredicate; + if (allowPred is null && allowlist is null) + { + throw Starlark.Eval.Starlark.Errorf( + "'allowed' function requires either an 'allowlist' or an 'allow_predicate' parameter."); + } + if (allowPred is not null) + { + allowPredicate = allowPred; + } + else + { + ImmutableHashSet allowedAuthors = CreateAllowlist(allowlist!); + allowPredicate = new AllowlistPredicate(allowedAuthors); + } + return new Authoring( + Author.Parse(defaultAuthor), AuthoringMappingMode.Allowed, allowPredicate); + } + + private static ImmutableHashSet CreateAllowlist(IEnumerable list) + { + var items = list.ToList(); + if (items.Count == 0) + { + throw Starlark.Eval.Starlark.Errorf( + "'allowed' function requires a non-empty 'allowlist' field. For default mapping," + + " use 'overwrite(...)' mode instead."); + } + var uniqueAuthors = new HashSet(); + foreach (var author in items) + { + if (!uniqueAuthors.Add(author)) + { + throw Starlark.Eval.Starlark.Errorf("Duplicated allowlist entry '{0}'", author); + } + } + return items.ToImmutableHashSet(); + } + } + + /// + /// Mode used for author mapping from origin to destination. + /// + /// This enum is our internal representation for the different Skylark built-in + /// functions. + /// + public enum AuthoringMappingMode + { + /// Corresponds with built-in function. + Overwrite, + + /// Corresponds with built-in function. + PassThru, + + /// Corresponds to built-in function. + Allowed, + } + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var authoring = (Authoring)o; + return Equals(_defaultAuthor, authoring._defaultAuthor) + && _mode == authoring._mode + && Equals(_allowPredicate, authoring._allowPredicate); + } + + public override int GetHashCode() => HashCode.Combine(_defaultAuthor, _mode, _allowPredicate); + + public override string ToString() => + $"Authoring{{defaultAuthor={_defaultAuthor}, mode={_mode}, allowPredicate={_allowPredicate}}}"; + + /// A predicate that can throw . + public interface IEvalThrowingPredicate + { + bool Test(T input); + } + + /// + /// A predicate that uses a Starlark function to check if an author is allowed to be attributed + /// for code. + /// + /// + /// Java wraps a StarlarkCallable invoked on a StarlarkThread. Until those types + /// are ported, this holds a delegate that evaluates the author, preserving the type surface. + /// + public sealed class AllowPredicate : IEvalThrowingPredicate + { + private readonly Func _allowPred; + + public AllowPredicate(Func allowPred) + { + _allowPred = allowPred; + } + + public bool Test(string author) => _allowPred(author); + + public override bool Equals(object? obj) => + obj is AllowPredicate other && Equals(_allowPred, other._allowPred); + + public override int GetHashCode() => _allowPred.GetHashCode(); + } + + /// + /// A predicate that uses a list of allowed authors to check if an author is allowed to be + /// attributed for code. + /// + public sealed class AllowlistPredicate : IEvalThrowingPredicate + { + private readonly ImmutableHashSet _allowedAuthors; + + public AllowlistPredicate(ImmutableHashSet allowedAuthors) + { + _allowedAuthors = allowedAuthors; + } + + public ImmutableHashSet AllowedAuthors => _allowedAuthors; + + public bool Test(string author) => _allowedAuthors.Contains(author); + + public override bool Equals(object? obj) => + obj is AllowlistPredicate other && _allowedAuthors.SetEquals(other._allowedAuthors); + + public override int GetHashCode() + { + int hash = 0; + foreach (var a in _allowedAuthors) + { + hash ^= a.GetHashCode(); + } + return hash; + } + + public override string ToString() => + $"AllowlistPredicate{{allowedAuthors=[{string.Join(", ", _allowedAuthors)}]}}"; + } + + /// A predicate that always returns false for attribution. + public sealed class RejectAllPredicate : IEvalThrowingPredicate + { + public bool Test(string author) => false; + + public override bool Equals(object? obj) => obj is RejectAllPredicate; + + public override int GetHashCode() => typeof(RejectAllPredicate).GetHashCode(); + + public override string ToString() => "RejectAllPredicate{}"; + } +} diff --git a/src/Copybara.Core/Authoring/InvalidAuthorException.cs b/src/Copybara.Core/Authoring/InvalidAuthorException.cs new file mode 100644 index 000000000..cb950e37a --- /dev/null +++ b/src/Copybara.Core/Authoring/InvalidAuthorException.cs @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Authoring; + +/// +/// Indicates that the author does not conform to the expected format. +/// +/// This exception does not extend other exceptions ValidationException or +/// RepoException because a bad parsed author could come from a configuration or a repo, and +/// it's wrapped into the proper one by the caller. +/// +public class InvalidAuthorException : Exception +{ + internal InvalidAuthorException(string message) + : base(message) + { + } +} diff --git a/src/Copybara.Core/Exceptions/AccessValidationException.cs b/src/Copybara.Core/Exceptions/AccessValidationException.cs new file mode 100644 index 000000000..c86c68e81 --- /dev/null +++ b/src/Copybara.Core/Exceptions/AccessValidationException.cs @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// A special case of when the error is likely to be a user error +/// related to access to the repo (access denied, not found, etc.) +/// +public class AccessValidationException : ValidationException +{ + public AccessValidationException(string message) + : base(message) + { + } + + public AccessValidationException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Exceptions/CannotResolveLabel.cs b/src/Copybara.Core/Exceptions/CannotResolveLabel.cs new file mode 100644 index 000000000..94909f3fc --- /dev/null +++ b/src/Copybara.Core/Exceptions/CannotResolveLabel.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// An exception thrown when a dependency expressed as a label in a content cannot be resolved. +/// +public class CannotResolveLabel : ValidationException +{ + public CannotResolveLabel(string message) + : base(message) + { + } + + public CannotResolveLabel(string message, Exception? exception) + : base(message, exception) + { + } +} diff --git a/src/Copybara.Core/Exceptions/CannotResolveRevisionException.cs b/src/Copybara.Core/Exceptions/CannotResolveRevisionException.cs new file mode 100644 index 000000000..100e5a66e --- /dev/null +++ b/src/Copybara.Core/Exceptions/CannotResolveRevisionException.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// Indicates that a revision could not be resolved in a repository. +/// +public class CannotResolveRevisionException : ValidationException +{ + public CannotResolveRevisionException(string message) + : base(message) + { + } + + public CannotResolveRevisionException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Exceptions/ChangeRejectedException.cs b/src/Copybara.Core/Exceptions/ChangeRejectedException.cs new file mode 100644 index 000000000..a5be3674d --- /dev/null +++ b/src/Copybara.Core/Exceptions/ChangeRejectedException.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// User rejected the change and aborted execution. +/// +public class ChangeRejectedException : ValidationException +{ + public ChangeRejectedException(string message) + : base(message) + { + } +} diff --git a/src/Copybara.Core/Exceptions/CommandLineException.cs b/src/Copybara.Core/Exceptions/CommandLineException.cs new file mode 100644 index 000000000..f5737a390 --- /dev/null +++ b/src/Copybara.Core/Exceptions/CommandLineException.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// An exception due to a command line error. +/// +public class CommandLineException : ValidationException +{ + public CommandLineException(string message) + : base(message) + { + } + + public CommandLineException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Exceptions/EmptyChangeException.cs b/src/Copybara.Core/Exceptions/EmptyChangeException.cs new file mode 100644 index 000000000..152d8b9d1 --- /dev/null +++ b/src/Copybara.Core/Exceptions/EmptyChangeException.cs @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// An exception thrown by destinations when they detect that there is no change to submit. Usually +/// this means that the change was already applied. +/// +public class EmptyChangeException : ValidationException +{ + public EmptyChangeException(string message) + : base(message) + { + } + + public EmptyChangeException(Exception? cause, string message) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Exceptions/NonReversibleValidationException.cs b/src/Copybara.Core/Exceptions/NonReversibleValidationException.cs new file mode 100644 index 000000000..86871d7b0 --- /dev/null +++ b/src/Copybara.Core/Exceptions/NonReversibleValidationException.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Starlark.Eval; + +namespace Copybara.Exceptions; + +/// +/// Exception thrown when a Transformation is not reversible but the configuration asked for the +/// reverse. +/// +public class NonReversibleValidationException : EvalException +{ + public NonReversibleValidationException(string message) + : base(message) + { + } + + public NonReversibleValidationException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Exceptions/NotADestinationFileException.cs b/src/Copybara.Core/Exceptions/NotADestinationFileException.cs new file mode 100644 index 000000000..9d7469def --- /dev/null +++ b/src/Copybara.Core/Exceptions/NotADestinationFileException.cs @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// An exception that indicates a file that was to be written to a destination is not actually +/// included in destination_files, indicating a bad configuration. +/// +public class NotADestinationFileException : ValidationException +{ + public NotADestinationFileException(string message) + : base(message) + { + } +} diff --git a/src/Copybara.Core/Exceptions/RedundantChangeException.cs b/src/Copybara.Core/Exceptions/RedundantChangeException.cs new file mode 100644 index 000000000..0cf541c2b --- /dev/null +++ b/src/Copybara.Core/Exceptions/RedundantChangeException.cs @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// An exception thrown by destinations when they detect that there is no change to submit, because +/// the same change is already pending in the destination and allow_empty_diff=False was +/// present in the config. +/// +public class RedundantChangeException : EmptyChangeException +{ + public RedundantChangeException(string message, string pendingRevision) + : base(message) + { + PendingRevision = pendingRevision; + } + + public RedundantChangeException(Exception? cause, string message, string pendingRevision) + : base(cause, message) + { + PendingRevision = pendingRevision; + } + + /// + /// A ref (e.g. SHA1) of the pending change that makes the current workflow redundant. + /// + public string PendingRevision { get; } +} diff --git a/src/Copybara.Core/Exceptions/RepoException.cs b/src/Copybara.Core/Exceptions/RepoException.cs new file mode 100644 index 000000000..f0426fb4c --- /dev/null +++ b/src/Copybara.Core/Exceptions/RepoException.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// Exceptions that happen during repository manipulation. +/// +public class RepoException : Exception +{ + public RepoException(string message) + : base(message) + { + } + + public RepoException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Exceptions/ValidationException.cs b/src/Copybara.Core/Exceptions/ValidationException.cs new file mode 100644 index 000000000..9b73c5be2 --- /dev/null +++ b/src/Copybara.Core/Exceptions/ValidationException.cs @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; + +namespace Copybara.Exceptions; + +/// +/// Indicates that the configuration is wrong or some error attributable to the user happened. For +/// example wrong flag usage, errors in fields or errors that we discover during execution. +/// +public class ValidationException : Exception +{ + public ValidationException(string message) + : base(message) + { + } + + public ValidationException(string message, Exception? cause) + : base(message, cause) + { + } + + /// + /// Check a condition and throw if false. + /// + /// if is false + public static void CheckCondition(bool condition, string format, params object?[] args) + { + if (!condition) + { + // Don't try to format if there are no args. This allows strings like '%Fooooo'. + if (args.Length == 0) + { + throw new ValidationException(format); + } + + throw new ValidationException(Format(format, args)); + } + } + + /// + /// Check a condition and throw if false. + /// + /// if is false + public static void CheckCondition(bool condition, string msg) + { + if (!condition) + { + throw new ValidationException(msg); + } + } + + /// + /// Throw a that can be retried. + /// + public static ValidationException RetriableException(string message) + { + return new ValidationException(message); + } + + /// + /// Translates a Java/printf-style format string (using conversions such as %s and + /// %d) into a .NET composite format string and applies . + /// Conversions are consumed sequentially from . A literal percent sign is + /// written using %%. + /// + private static string Format(string fmt, object?[] args) + { + var builder = new StringBuilder(fmt.Length + 16); + var argIndex = 0; + + for (var i = 0; i < fmt.Length; i++) + { + var c = fmt[i]; + if (c == '%' && i + 1 < fmt.Length) + { + var next = fmt[i + 1]; + if (next == '%') + { + builder.Append('%'); + i++; + continue; + } + + // Treat any single-letter conversion (e.g. %s, %d, %b) as a positional argument. + if (char.IsLetter(next)) + { + builder.Append('{').Append(argIndex++).Append('}'); + i++; + continue; + } + } + + // Escape braces so they are treated literally by string.Format. + if (c == '{') + { + builder.Append("{{"); + } + else if (c == '}') + { + builder.Append("}}"); + } + else + { + builder.Append(c); + } + } + + return string.Format(builder.ToString(), args); + } +} diff --git a/src/Copybara.Core/Exceptions/VoidOperationException.cs b/src/Copybara.Core/Exceptions/VoidOperationException.cs new file mode 100644 index 000000000..3ba45dad2 --- /dev/null +++ b/src/Copybara.Core/Exceptions/VoidOperationException.cs @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Exceptions; + +/// +/// An exception that indicates that an operation, as defined, does not do anything to the +/// repository. This usually indicates a problem with the configuration. +/// +public class VoidOperationException : ValidationException +{ + public VoidOperationException(string message) + : base(message) + { + } +} diff --git a/src/Copybara.Core/Revision/Change.cs b/src/Copybara.Core/Revision/Change.cs new file mode 100644 index 000000000..850bd1240 --- /dev/null +++ b/src/Copybara.Core/Revision/Change.cs @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Authoring; +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Revision; + +/// Represents a change in a Repository. +[StarlarkBuiltin( + "change", + Doc = "A change metadata. Contains information like author, change message or detected labels")] +public sealed class Change : OriginRef, IStarlarkValue + where R : class, IRevision +{ + private readonly R _revision; + private readonly Author _author; + private readonly string _message; + private readonly DateTimeOffset _dateTime; + private readonly ImmutableListMultimap _labels; + private Author? _mappedAuthor; + private readonly bool _merge; + private readonly ImmutableArray? _parents; + private readonly ImmutableHashSet? _changeFiles; + + public Change( + R revision, + Author author, + string message, + DateTimeOffset dateTime, + ImmutableListMultimap labels) + : this(revision, author, message, dateTime, labels, changeFiles: null) + { + } + + public Change( + R revision, + Author author, + string message, + DateTimeOffset dateTime, + ImmutableListMultimap labels, + ISet? changeFiles) + : this(revision, author, message, dateTime, labels, changeFiles, merge: false, parents: null) + { + } + + public Change( + R revision, + Author author, + string message, + DateTimeOffset dateTime, + ImmutableListMultimap labels, + ISet? changeFiles, + bool merge, + ImmutableArray? parents) + : base(Preconditions.CheckNotNull(revision).AsString()) + { + _revision = Preconditions.CheckNotNull(revision); + _author = Preconditions.CheckNotNull(author); + _message = Preconditions.CheckNotNull(message); + _dateTime = dateTime; + _labels = labels; + _changeFiles = changeFiles is null ? null : changeFiles.ToImmutableHashSet(); + _merge = merge; + _parents = parents; + } + + /// Reference of the change. For example a SHA-1 reference in git. + public R GetRevision() => _revision; + + /// + /// Return the parent revisions if the origin provides that information. Currently only for Git + /// and Hg. Otherwise null. + /// + public ImmutableArray? GetParents() => _parents; + + [StarlarkMethod( + "original_author", + Doc = "The author of the change before any mapping", + StructField = true)] + public Author GetAuthor() => _author; + + /// The author of the change. Can already be mapped using metadata.map_author. + [StarlarkMethod("author", Doc = "The author of the change", StructField = true)] + public Author GetMappedAuthor() => Preconditions.CheckNotNull(_mappedAuthor ?? _author); + + public void SetMappedAuthor(Author mappedAuthor) => _mappedAuthor = mappedAuthor; + + [StarlarkMethod("message", Doc = "The message of the change", StructField = true)] + public string GetMessage() => _message; + + [StarlarkMethod( + "labels", + Doc = + "A dictionary with the labels detected for the change. If the label is present multiple" + + " times it returns the last value. Note that this is a heuristic and it could" + + " include things that are not labels.", + StructField = true)] + public IReadOnlyDictionary GetLabelsForSkylark() + { + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var key in _labels.Keys) + { + var values = _labels.Get(key); + builder[key] = values[values.Length - 1]; + } + return builder.ToImmutable(); + } + + [StarlarkMethod( + "labels_all_values", + Doc = + "A dictionary with the labels detected for the change. Note that the value is a" + + " collection of the values for each time the label was found. Use 'labels' instead" + + " if you are only interested in the last value. Note that this is a heuristic and" + + " it could include things that are not labels.", + StructField = true)] + public IReadOnlyDictionary> GetLabelsAllForSkylark() + { + var builder = ImmutableDictionary.CreateBuilder>(); + foreach (var key in _labels.Keys) + { + builder[key] = _labels.Get(key); + } + return builder.ToImmutable(); + } + + /// If not null, the files that were affected in this change. + public ImmutableHashSet? GetChangeFiles() => _changeFiles; + + public DateTimeOffset GetDateTime() => _dateTime; + + [StarlarkMethod( + "date_time_iso_offset", + Doc = "Return a ISO offset date time. Example: 2011-12-03T10:15:30+01:00'", + StructField = true)] + public string DateTimeFmt() => + GetDateTime().ToString("yyyy-MM-ddTHH:mm:sszzz", System.Globalization.CultureInfo.InvariantCulture); + + public ImmutableListMultimap GetLabels() => _labels; + + /// Returns the first line of the change. Usually a summary. + [StarlarkMethod("first_line_message", Doc = "The message of the change", StructField = true)] + public string FirstLineMessage() => ExtractFirstLine(_message); + + /// Get the first line of a message. + public static string ExtractFirstLine(string message) + { + int idx = message.IndexOf('\n'); + return idx == -1 ? message : message.Substring(0, idx); + } + + /// Returns true if the change represents a merge. + [StarlarkMethod("merge", Doc = "Returns true if the change represents a merge", StructField = true)] + public bool IsMerge() => _merge; + + public override string ToString() => + $"Change{{revision={_revision}, author={_author}, dateTime={_dateTime}, message={_message}, " + + $"merge={_merge}, parents={(_parents is null ? "null" : string.Join(", ", _parents))}}}"; + + public Change WithLabels(ImmutableListMultimap newLabels) => + new( + _revision, + _author, + _message, + _dateTime, + IRevision.AddNewLabels(_labels, newLabels), + _changeFiles, + _merge, + _parents); + + public Change WithChangeFiles(ImmutableHashSet newChangeFiles) => + new(_revision, _author, _message, _dateTime, _labels, newChangeFiles, _merge, _parents); + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var change = (Change)o; + return Equals(_revision, change._revision) + && Equals(_author, change._author) + && string.Equals(_message, change._message) + && Equals(_dateTime, change._dateTime) + && Equals(_labels, change._labels); + } + + public override int GetHashCode() => + HashCode.Combine(_revision, _author, _message, _dateTime, _labels); +} diff --git a/src/Copybara.Core/Revision/Changes.cs b/src/Copybara.Core/Revision/Changes.cs new file mode 100644 index 000000000..2c5b33c6e --- /dev/null +++ b/src/Copybara.Core/Revision/Changes.cs @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Revision; + +/// Information about the changes being imported. +/// +/// Java holds Sequence<? extends Change<?>>. As is generic +/// over an unbounded revision type, the heterogeneous collection is modelled here as a list of +/// object (each element being some ), mirroring the Java wildcard. +/// +[StarlarkBuiltin( + "Changes", + Doc = + "Data about the set of changes that are being migrated. " + + "Each change includes information like: original author, change message, " + + "labels, etc. You receive this as a field in TransformWork object for user defined " + + "transformations")] +public sealed class Changes : IStarlarkValue +{ + public static readonly Changes Empty = + new(ImmutableArray.Empty, ImmutableArray.Empty); + + private readonly IReadOnlyList _current; + private readonly IReadOnlyList _migrated; + + public Changes(IEnumerable current, IEnumerable migrated) + { + _current = current.ToImmutableArray(); + _migrated = migrated.ToImmutableArray(); + } + + [StarlarkMethod("current", Doc = "List of changes that will be migrated", StructField = true)] + public IReadOnlyList GetCurrent() => _current; + + [StarlarkMethod( + "migrated", + Doc = + "List of changes that where migrated in previous Copybara executions or if using" + + " ITERATIVE mode in previous iterations of this workflow.", + StructField = true)] + public IReadOnlyList GetMigrated() => _migrated; +} diff --git a/src/Copybara.Core/Revision/OriginRef.cs b/src/Copybara.Core/Revision/OriginRef.cs new file mode 100644 index 000000000..229aa4c7d --- /dev/null +++ b/src/Copybara.Core/Revision/OriginRef.cs @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Revision; + +/// +/// Reference to the change/review read from the origin. +/// +[StarlarkBuiltin("origin_ref", Doc = "Reference to the change/review in the origin.")] +public class OriginRef : IStarlarkValue +{ + private readonly string _ref; + + public OriginRef(string id) + { + _ref = Preconditions.CheckNotNull(id); + } + + /// Origin reference. + [StarlarkMethod("ref", Doc = "Origin reference ref", StructField = true)] + public string Ref => _ref; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var originRef = (OriginRef)o; + return string.Equals(_ref, originRef._ref); + } + + public override int GetHashCode() => _ref.GetHashCode(); + + public override string ToString() => $"OriginRef{{ref={_ref}}}"; +} diff --git a/src/Copybara.Core/Revision/Revision.cs b/src/Copybara.Core/Revision/Revision.cs new file mode 100644 index 000000000..241ea333b --- /dev/null +++ b/src/Copybara.Core/Revision/Revision.cs @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; + +namespace Copybara.Revision; + +/// +/// A revision of Copybara.Origin. +/// +/// For example, in Git it would be a commit SHA-1. +/// +public interface IRevision +{ + /// + /// Reads the timestamp of this revision from the repository, or null if this repo type + /// does not support it. This is the instant from the UNIX epoch when the revision was submitted + /// to the source repository. + /// + DateTimeOffset? ReadTimestamp(); + + /// + /// String representation of the revision that can be parsed by + /// Copybara.Origin.Resolve(string). + /// + /// Unlike the method, this method is guaranteed to be + /// stable. + /// + string AsString(); + + /// + /// If not null, returns a stable name representing the reference from where this + /// was created. + /// + /// For example if the user passed 'master' in the command line, the + /// would return the SHA-1 and this method would return 'master'. Note that it is a valid + /// response to return here if the implementation chooses to. + /// + string? ContextReference() => null; + + /// + /// If not null, returns a fixed value identifying the repo state for this + /// . + /// + /// For example if the user passed 'main' in the command line, this would return the SHA-1 + /// of the current HEAD of main while would return 'main'. + /// + string? FixedReference() => null; + + /// + /// Returns a stable name representing the reference from where this was + /// created. + /// + /// The difference between this and is that this method + /// returns a complete reference path, if possible. For example, if the user passed in `main`, + /// this method would return `refs/heads/main`, while would return + /// `main`. + /// + string? FullReference() => ContextReference(); + + /// + /// Return any associated label with the revision. Keys are the label name and values are the + /// content of the label. + /// + /// Labels should only be set when the origin knows for sure that the reference is in the + /// context of the current migration. + /// + ImmutableListMultimap AssociatedLabels() => + ImmutableListMultimap.Empty; + + /// Find a specific label. + IReadOnlyList AssociatedLabel(string label) => AssociatedLabels().Get(label); + + /// The url of the revision repository. + string? GetUrl() => null; + + /// + /// String that represents the revision type. + /// + /// The purpose of this string is to be used as an identifier in other systems. Once it has + /// been defined, it shouldn't be changed. + /// + string? GetRevisionType() => null; + + /// Given a list of labels it adds new labels without repeating them. + static ImmutableListMultimap AddNewLabels( + ImmutableListMultimap existingLabels, + ImmutableListMultimap newLabels) + { + var builder = ImmutableListMultimap.CreateBuilder(); + builder.PutAll(existingLabels); + foreach (var key in newLabels.Keys) + { + foreach (var v in newLabels.Get(key)) + { + if (existingLabels.ContainsEntry(key, v)) + { + continue; + } + builder.Put(key, v); + } + } + return builder.Build(); + } +} diff --git a/src/Copybara.Core/TemplateToken/LabelTemplate.cs b/src/Copybara.Core/TemplateToken/LabelTemplate.cs new file mode 100644 index 000000000..ff5bc1637 --- /dev/null +++ b/src/Copybara.Core/TemplateToken/LabelTemplate.cs @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; + +namespace Copybara.TemplateToken; + +/// +/// A template system that for texts like "This ${LABEL} is a template". +/// TODO(malcon): Consolidate this class and Parser/Token. +/// +public class LabelTemplate +{ + // ([\w-]+) is coming from LabelFinder.VALID_LABEL_EXPR. Due to a dependency + // issue we have it here inlined. It is not a big deal as the labels need to exist + // and also will be refactored into Parser/Token. + private static readonly Regex VarPattern = new(@"\$\{([\w-]+)}"); + + private readonly HashSet _labels = new(); + private readonly string _template; + + /// + /// Construct the template object from a String. + /// + /// a String in the form of "Foo ${LABEL} ${OTHER} Bar". + public LabelTemplate(string template) + { + _template = template; + foreach (Match match in VarPattern.Matches(template)) + { + _labels.Add(match.Groups[1].Value); + } + } + + /// Resolve the template string for a particular set of labels. + /// if a label cannot be found. + public string Resolve(Func labelFinder) + { + var labelValues = new Dictionary(); + foreach (string label in _labels) + { + string? value = labelFinder(label); + if (value == null) + { + throw new LabelNotFoundException(label); + } + labelValues[label] = value; + } + + string result = _template; + foreach (var entry in labelValues) + { + result = result.Replace("${" + entry.Key + "}", entry.Value); + } + return result; + } + + /// Thrown when a label cannot be found in the message. + public class LabelNotFoundException : Exception + { + internal LabelNotFoundException(string label) + : base("Cannot find label " + label) + { + Label = label; + } + + /// Get the label that couldn't be found. + public string Label { get; } + } +} diff --git a/src/Copybara.Core/TemplateToken/Parser.cs b/src/Copybara.Core/TemplateToken/Parser.cs new file mode 100644 index 000000000..45217b1a4 --- /dev/null +++ b/src/Copybara.Core/TemplateToken/Parser.cs @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; + +namespace Copybara.TemplateToken; + +/// +/// Parse strings like "foo${bar}baz" in a series of literal and interpolation variables. +/// +public class Parser +{ + public Parser() + { + } + + /// + /// Parses a template. In the raw string representation, interpolation is done with + /// ${var_name}. Literal dollar signs can be represented with $$. + /// + /// if the template is malformed. + public IReadOnlyList Parse(string template) + { + var result = ImmutableArray.CreateBuilder(); + var currentLiteral = new StringBuilder(); + int c = 0; + while (c < template.Length) + { + char thisChar = template[c]; + c++; + if (thisChar != '$') + { + currentLiteral.Append(thisChar); + continue; + } + if (c >= template.Length) + { + throw Starlark.Eval.Starlark.Errorf( + "Expect $ or {{ after every $ in string: {0}", template); + } + thisChar = template[c]; + c++; + switch (thisChar) + { + case '$': + currentLiteral.Append('$'); + break; + case '{': + result.Add(new Token(currentLiteral.ToString(), TokenType.Literal)); + currentLiteral = new StringBuilder(); + int terminating = template.IndexOf('}', c); + if (terminating == -1) + { + throw Starlark.Eval.Starlark.Errorf( + "Unterminated '${{'. Expected '}}': {0}", template); + } + if (c == terminating) + { + throw Starlark.Eval.Starlark.Errorf( + "Expect non-empty interpolated value name: {0}", template); + } + result.Add( + new Token(template.Substring(c, terminating - c), TokenType.Interpolation)); + c = terminating + 1; + break; + default: + throw Starlark.Eval.Starlark.Errorf( + "Expect $ or {{ after every $ in string: {0}", template); + } + } + result.Add(new Token(currentLiteral.ToString(), TokenType.Literal)); + return result.ToImmutable(); + } +} diff --git a/src/Copybara.Core/TemplateToken/RegexTemplateTokens.cs b/src/Copybara.Core/TemplateToken/RegexTemplateTokens.cs new file mode 100644 index 000000000..0a2a26de9 --- /dev/null +++ b/src/Copybara.Core/TemplateToken/RegexTemplateTokens.cs @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Starlark.Eval; +using Starlark.Syntax; + +namespace Copybara.TemplateToken; + +/// +/// A string which is interpolated with named variables. The string is composed of interpolated and +/// non-interpolated (literal) pieces called tokens. +/// +public sealed class RegexTemplateTokens +{ + private readonly string _template; + private readonly Regex _before; + private readonly ImmutableListMultimap _groupIndexes; + private readonly IReadOnlyList _tokens; + private readonly ISet _unusedGroups; + private readonly Location _location; + + public RegexTemplateTokens( + string template, + IReadOnlyDictionary regexGroups, + bool repeatedGroups, + Location location) + : this(template, regexGroups, repeatedGroups, matchExactly: false, location) + { + } + + public RegexTemplateTokens( + string template, + IReadOnlyDictionary regexGroups, + bool repeatedGroups, + bool matchExactly, + Location location) + { + _template = Preconditions.CheckNotNull(template); + + _tokens = new Parser().Parse(template).ToList(); + _location = Preconditions.CheckNotNull(location); + + var groupIndexesBuilder = ImmutableListMultimap.CreateBuilder(); + _before = BuildBefore(regexGroups, repeatedGroups, matchExactly, groupIndexesBuilder); + _groupIndexes = groupIndexesBuilder.Build(); + + var used = new HashSet(_groupIndexes.Keys); + _unusedGroups = new HashSet(regexGroups.Keys.Where(k => !used.Contains(k))); + } + + /// + /// How this template can be used when it is the "before" value of core.replace - as a regex to + /// search for. + /// + public Regex GetBefore() => _before; + + public IReadOnlyList GetTokens() => _tokens; + + public bool IsEmpty() => _template.Length == 0; + + /// Is this template regex-free. + public bool IsLiteral() => + IsEmpty() || (_tokens.Count == 1 && _tokens[0].GetTokenType() == TokenType.Literal); + + public ImmutableListMultimap GetGroupIndexes() => _groupIndexes; + + public Replacer CreateReplacer( + RegexTemplateTokens after, + bool firstOnly, + bool multiline, + IReadOnlyList? patternsToIgnore) => + new(this, _before, after, callback: null, firstOnly, multiline, patternsToIgnore, _location); + + public Replacer CallbackReplacer( + RegexTemplateTokens after, + IAlterAfterTemplate callback, + bool firstOnly, + bool multiline, + IReadOnlyList? patternsToIgnore) => + new(this, _before, after, callback, firstOnly, multiline, patternsToIgnore, _location); + + public sealed class Replacer + { + private readonly RegexTemplateTokens _owner; + private readonly Regex _before; + private readonly RegexTemplateTokens _after; + private readonly bool _firstOnly; + private readonly bool _multiline; + private readonly string _afterReplaceTemplate; + private readonly ImmutableListMultimap _repeatedGroups; + private readonly Location _location; + private readonly IReadOnlyList? _patternsToIgnore; + private readonly IAlterAfterTemplate? _callback; + + internal Replacer( + RegexTemplateTokens owner, + Regex before, + RegexTemplateTokens after, + IAlterAfterTemplate? callback, + bool firstOnly, + bool multiline, + IReadOnlyList? patternsToIgnore, + Location location) + { + _owner = owner; + _before = before; + _after = after; + _afterReplaceTemplate = _after.After(owner); + // Precompute the repeated groups as this should be used only on rare occasions and we + // don't want to iterate over the map for every line. + var repeatedBuilder = ImmutableListMultimap.CreateBuilder(); + foreach (var key in owner._groupIndexes.Keys) + { + var values = owner._groupIndexes.Get(key); + if (values.Length > 1) + { + repeatedBuilder.PutAll(key, values); + } + } + _repeatedGroups = repeatedBuilder.Build(); + _firstOnly = firstOnly; + _multiline = multiline; + _callback = callback; + _patternsToIgnore = patternsToIgnore; + _location = location; + } + + public string Replace(string content) + { + IReadOnlyList originalRanges = _multiline + ? new[] { content } + : content.Split('\n'); + + var newRanges = new List(originalRanges.Count); + foreach (string line in originalRanges) + { + newRanges.Add(ReplaceLine(line)); + } + return string.Join("\n", newRanges); + } + + private string ReplaceLine(string line) + { + if (_patternsToIgnore != null) + { + foreach (Regex patternToIgnore in _patternsToIgnore) + { + if (IsFullMatch(patternToIgnore, line)) + { + return line; + } + } + } + + var sb = new StringBuilder(line.Length); + int lastAppend = 0; + Match matcher = _before.Match(line); + while (matcher.Success) + { + bool skipLine = false; + foreach (var key in _repeatedGroups.Keys) + { + // Check that all the references of the repeated group match the same string. + var groupIndexes = _repeatedGroups.Get(key); + string value = matcher.Groups[groupIndexes[0]].Value; + for (int i = 1; i < groupIndexes.Length; i++) + { + if (!value.Equals(matcher.Groups[groupIndexes[i]].Value)) + { + skipLine = true; + break; + } + } + if (skipLine) + { + break; + } + } + if (skipLine) + { + return line; + } + + string replaceTemplate; + if (_callback != null) + { + var groupValues = new Dictionary(); + for (int i = 0; i <= GroupCount(matcher); i++) + { + groupValues[i] = matcher.Groups[i].Value; + } + replaceTemplate = _callback.Alter(groupValues, _afterReplaceTemplate); + } + else + { + replaceTemplate = _afterReplaceTemplate; + } + + // Emulate Matcher.appendReplacement: append the text between the last match and this + // one, then the expanded replacement. + sb.Append(line, lastAppend, matcher.Index - lastAppend); + sb.Append(ExpandReplacement(replaceTemplate, matcher)); + lastAppend = matcher.Index + matcher.Length; + + if (_firstOnly) + { + break; + } + matcher = matcher.NextMatch(); + } + // appendTail. + sb.Append(line, lastAppend, line.Length - lastAppend); + return sb.ToString(); + } + + public override string ToString() => + $"s/{_owner}/{_after}/{(_firstOnly ? "" : "g")}"; + + public Location GetLocation() => _location; + + public bool IsFirstOnly() => _firstOnly; + } + + /// + /// How this template can be used when it is the "after" value of core.replace - as a string to + /// insert in place of the regex, possibly including $N, referring to captured groups. + /// + /// Returns a template in which the literals are escaped (if they are a $ or {) and the + /// interpolations appear as $N, where N is the group's index as given by groupIndexes. + /// + private string After(RegexTemplateTokens before) + { + var template = new StringBuilder(); + foreach (Token token in _tokens) + { + switch (token.GetTokenType()) + { + case TokenType.Interpolation: + template.Append('$').Append(before._groupIndexes.Get(token.GetValue())[0]); + break; + case TokenType.Literal: + string value = token.GetValue(); + for (int c = 0; c < value.Length; c++) + { + char thisChar = value[c]; + if (thisChar == '$' || thisChar == '\\') + { + template.Append('\\'); + } + template.Append(thisChar); + } + break; + } + } + return template.ToString(); + } + + public override string ToString() => + _template + .Replace("\n", "\\n") + .Replace("\r", "\\r") + .Replace("\t", "\\t"); + + public override bool Equals(object? other) + { + if (other is RegexTemplateTokens comp) + { + return _before.ToString() == comp._before.ToString() && _tokens.SequenceEqual(comp._tokens); + } + return false; + } + + public override int GetHashCode() => HashCode.Combine(_before.ToString(), TokensHash(_tokens)); + + private static int TokensHash(IReadOnlyList tokens) + { + var hash = default(HashCode); + foreach (var t in tokens) + { + hash.Add(t); + } + return hash.ToHashCode(); + } + + /// + /// Converts this sequence of tokens into a regex which can be used to search a string. It + /// automatically quotes literals and represents interpolations as named groups. + /// + /// It also fills groupIndexes with all the interpolation locations. + /// + private Regex BuildBefore( + IReadOnlyDictionary regexesByInterpolationName, + bool repeatedGroups, + bool matchExactly, + ImmutableListMultimap.Builder groupIndexes) + { + // Track the group counts registered per-name so far, to detect repeated usage. + var seenNames = new HashSet(); + int groupCount = 1; + var fullPattern = new StringBuilder(); + if (matchExactly) + { + fullPattern.Append('^'); + } + foreach (Token token in _tokens) + { + switch (token.GetTokenType()) + { + case TokenType.Interpolation: + Regex? subPattern = + regexesByInterpolationName.TryGetValue(token.GetValue(), out var sp) ? sp : null; + Check( + subPattern != null, + "Interpolation is used but not defined: {0}", + token.GetValue()); + fullPattern.Append($"({subPattern!})"); + Check( + !seenNames.Contains(token.GetValue()) || repeatedGroups, + "Regex group is used in template multiple times: {0}. " + + "If you require multiple references to the same regex group, " + + "set `repeated_groups=True`.", + token.GetValue()); + seenNames.Add(token.GetValue()); + groupIndexes.Put(token.GetValue(), groupCount); + groupCount += GroupCount(subPattern!) + 1; + break; + case TokenType.Literal: + fullPattern.Append(Regex.Escape(token.GetValue())); + break; + } + } + if (matchExactly) + { + fullPattern.Append('$'); + } + return new Regex(fullPattern.ToString(), RegexOptions.Multiline); + } + + /// + /// Checks that the set of interpolated tokens matches definedInterpolations. + /// + /// if not all interpolations are used in this template. + public void ValidateUnused() + { + Check( + _unusedGroups.Count == 0, + "Following interpolations are defined but not used: [{0}]", + string.Join(", ", _unusedGroups)); + } + + // Port of com.google.copybara.starlark.StarlarkUtil.check: throws EvalException when false. + private static void Check(bool condition, string format, params object?[] args) + { + if (!condition) + { + throw Starlark.Eval.Starlark.Errorf(format, args); + } + } + + // Number of capturing groups in a regex (excluding the whole-match group 0), matching + // re2j/java.util.regex Pattern.groupCount(). + private static int GroupCount(Regex regex) => regex.GetGroupNumbers().Length - 1; + + private static int GroupCount(Match match) => match.Groups.Count - 1; + + private static bool IsFullMatch(Regex regex, string input) + { + Match m = regex.Match(input); + return m.Success && m.Index == 0 && m.Length == input.Length; + } + + // Expands a replacement template containing $N group references against the given match, + // emulating java.util.regex Matcher.appendReplacement semantics (\\ and $ are escapes). + private static string ExpandReplacement(string template, Match match) + { + var sb = new StringBuilder(); + for (int i = 0; i < template.Length; i++) + { + char ch = template[i]; + if (ch == '\\') + { + i++; + if (i < template.Length) + { + sb.Append(template[i]); + } + } + else if (ch == '$') + { + i++; + var num = new StringBuilder(); + while (i < template.Length && char.IsDigit(template[i])) + { + num.Append(template[i]); + i++; + } + i--; // step back; the for loop will advance. + if (num.Length > 0) + { + int groupNum = int.Parse(num.ToString()); + if (groupNum < match.Groups.Count && match.Groups[groupNum].Success) + { + sb.Append(match.Groups[groupNum].Value); + } + } + } + else + { + sb.Append(ch); + } + } + return sb.ToString(); + } + + /// Callback for . + public interface IAlterAfterTemplate + { + /// + /// Upon encountering a match, the replacer will call the callback with the matched groups + /// and the template to be used in the replace. The return value of this function will be + /// used in place of template, i.e. group tokens like '$1' in the return value will be + /// replaced with the group values. Note that the groupValues are immutable. + /// + /// + /// The values of the groups in the before pattern. 0 holds the entire match. + /// + /// The replacement template the replacer would normally use. + /// The template to be used instead. + string Alter(IReadOnlyDictionary groupValues, string template); + } +} diff --git a/src/Copybara.Core/TemplateToken/Token.cs b/src/Copybara.Core/TemplateToken/Token.cs new file mode 100644 index 000000000..e6dc83b47 --- /dev/null +++ b/src/Copybara.Core/TemplateToken/Token.cs @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.TemplateToken; + +/// +/// Either a string literal or interpolated value. +/// +public sealed class Token +{ + private readonly string _value; + private readonly TokenType _type; + + internal Token(string value, TokenType type) + { + _value = Preconditions.CheckNotNull(value); + _type = type; + } + + /// Create an interpolation token. + public static Token Interpolation(string name) => new(name, TokenType.Interpolation); + + /// Create a literal token. + public static Token Literal(string name) => new(name, TokenType.Literal); + + public string GetValue() => _value; + + public TokenType GetTokenType() => _type; + + public override string ToString() => $"Token{{value={_value}, type={_type}}}"; + + public override bool Equals(object? obj) => + obj is Token other && _value == other._value && _type == other._type; + + public override int GetHashCode() => HashCode.Combine(_value, _type); +} + +/// The type of the token. +public enum TokenType +{ + Literal, + Interpolation, +} diff --git a/src/Copybara.Core/Util/BadExitStatusWithOutputException.cs b/src/Copybara.Core/Util/BadExitStatusWithOutputException.cs new file mode 100644 index 000000000..bef2cf680 --- /dev/null +++ b/src/Copybara.Core/Util/BadExitStatusWithOutputException.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util; + +/// +/// An exception that represents a program that did not exit with a 0 exit code, while retaining the +/// collected stdout/stderr. Port of +/// com.google.copybara.util.BadExitStatusWithOutputException. +/// +public class BadExitStatusWithOutputException : AbnormalTerminationException +{ + public BadExitStatusWithOutputException( + Command command, CommandResult result, string message, byte[] stdout, byte[] stderr) + : base(command, result, message) + { + Output = new CommandOutputWithStatus(result.TerminationStatus, stdout, stderr); + } + + public CommandOutputWithStatus Output { get; } + + public CommandOutputWithStatus GetOutput() => Output; +} diff --git a/src/Copybara.Core/Util/Command.cs b/src/Copybara.Core/Util/Command.cs new file mode 100644 index 000000000..aa1fb657e --- /dev/null +++ b/src/Copybara.Core/Util/Command.cs @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; + +namespace Copybara.Util; + +/// +/// A description of a subprocess to run: the argv, an optional environment, and an optional working +/// directory. This is the .NET-port equivalent of the Bazel shell library's Command value +/// that Copybara's consumes. Actual execution is performed by +/// using System.Diagnostics.Process. +/// +public sealed class Command +{ + private readonly ImmutableArray _commandLineElements; + private readonly ImmutableDictionary? _environmentVariables; + private readonly string? _workingDirectory; + + public Command(string[] commandLineElements) + : this(commandLineElements, null, null) + { + } + + public Command( + string[] commandLineElements, + IReadOnlyDictionary? environmentVariables, + string? workingDirectory) + { + Preconditions.CheckNotNull(commandLineElements); + Preconditions.CheckArgument( + commandLineElements.Length > 0, "command line elements must not be empty"); + _commandLineElements = commandLineElements.ToImmutableArray(); + _environmentVariables = environmentVariables?.ToImmutableDictionary(); + _workingDirectory = workingDirectory; + } + + /// The complete argument vector, including the program name at index 0. + public IReadOnlyList GetCommandLineElements() => _commandLineElements; + + /// The environment variables to run with, or null to inherit the current environment. + public IReadOnlyDictionary? GetEnvironmentVariables() => _environmentVariables; + + /// The working directory to run in, or null to inherit the current one. + public string? GetWorkingDirectory() => _workingDirectory; + + public override string ToString() => ShellUtils.PrettyPrintArgv(_commandLineElements); +} diff --git a/src/Copybara.Core/Util/CommandException.cs b/src/Copybara.Core/Util/CommandException.cs new file mode 100644 index 000000000..52c41f2de --- /dev/null +++ b/src/Copybara.Core/Util/CommandException.cs @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util; + +/// +/// Superclass of all exceptions that may be thrown during command execution. Port of the Bazel +/// shell library's CommandException. +/// +public class CommandException : Exception +{ + public Command Command { get; } + + public CommandException(Command command, string message) : base(message) => Command = command; + + public CommandException(Command command, Exception cause) : base(cause.Message, cause) => + Command = command; + + public CommandException(Command command, string message, Exception? cause) + : base(message, cause) => Command = command; + + public Command GetCommand() => Command; +} + +/// +/// Thrown when a command terminates abnormally (non-zero exit, killed by a signal, etc.). Port of +/// the Bazel shell library's AbnormalTerminationException. +/// +public class AbnormalTerminationException : CommandException +{ + public CommandResult Result { get; } + + public AbnormalTerminationException(Command command, CommandResult result, string message) + : base(command, message) => Result = result; + + public AbnormalTerminationException( + Command command, CommandResult result, string message, Exception? cause) + : base(command, message, cause) => Result = result; + + public CommandResult GetResult() => Result; +} + +/// +/// Thrown when a command exits with a non-zero exit code. Port of the Bazel shell library's +/// BadExitStatusException. +/// +public class BadExitStatusException : AbnormalTerminationException +{ + public BadExitStatusException(Command command, CommandResult result, string message) + : base(command, result, message) + { + } +} diff --git a/src/Copybara.Core/Util/CommandOutput.cs b/src/Copybara.Core/Util/CommandOutput.cs new file mode 100644 index 000000000..185e1b772 --- /dev/null +++ b/src/Copybara.Core/Util/CommandOutput.cs @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using Copybara.Common; + +namespace Copybara.Util; + +/// +/// Holds the stdout and stderr contents of a command execution. Port of +/// com.google.copybara.util.CommandOutput. +/// +public class CommandOutput +{ + private readonly byte[] _stdout; + private readonly byte[] _stderr; + + public CommandOutput(byte[] stdout, byte[] stderr) + { + _stdout = Preconditions.CheckNotNull(stdout); + _stderr = Preconditions.CheckNotNull(stderr); + } + + public string GetStdout() => Encoding.UTF8.GetString(_stdout); + + public byte[] GetStdoutBytes() => _stdout; + + public string GetStderr() => Encoding.UTF8.GetString(_stderr); + + public override string ToString() => + $"CommandOutput{{stdout={GetStdout()}, stderr={GetStderr()}}}"; +} diff --git a/src/Copybara.Core/Util/CommandOutputWithStatus.cs b/src/Copybara.Core/Util/CommandOutputWithStatus.cs new file mode 100644 index 000000000..6d054683d --- /dev/null +++ b/src/Copybara.Core/Util/CommandOutputWithStatus.cs @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util; + +/// +/// A that also carries a . Port of +/// com.google.copybara.util.CommandOutputWithStatus. +/// +public sealed class CommandOutputWithStatus : CommandOutput +{ + public CommandOutputWithStatus(TerminationStatus terminationStatus, byte[] stdout, byte[] stderr) + : base(stdout, stderr) + { + TerminationStatus = Preconditions.CheckNotNull(terminationStatus); + } + + public TerminationStatus TerminationStatus { get; } + + public TerminationStatus GetTerminationStatus() => TerminationStatus; + + public override string ToString() => + $"CommandOutputWithStatus{{{base.ToString()}, terminationStatus={TerminationStatus}}}"; +} diff --git a/src/Copybara.Core/Util/CommandResult.cs b/src/Copybara.Core/Util/CommandResult.cs new file mode 100644 index 000000000..5d11cf1b4 --- /dev/null +++ b/src/Copybara.Core/Util/CommandResult.cs @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util; + +/// +/// The result of executing a . Port of the Bazel shell library's +/// CommandResult subset used by Copybara. +/// +public sealed class CommandResult +{ + private readonly byte[] _stdout; + private readonly byte[] _stderr; + + public CommandResult(byte[] stdout, byte[] stderr, TerminationStatus terminationStatus) + { + _stdout = stdout; + _stderr = stderr; + TerminationStatus = terminationStatus; + } + + public byte[] GetStdout() => _stdout; + + public byte[] GetStderr() => _stderr; + + public TerminationStatus TerminationStatus { get; } +} diff --git a/src/Copybara.Core/Util/CommandRunner.cs b/src/Copybara.Core/Util/CommandRunner.cs new file mode 100644 index 000000000..83e013cf5 --- /dev/null +++ b/src/Copybara.Core/Util/CommandRunner.cs @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Diagnostics; +using System.Text; +using Copybara.Common; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Copybara.Util; + +/// +/// Allows running a with easier stderr/stdout and logging management. Port of +/// com.google.copybara.util.CommandRunner. Execution is performed with +/// System.Diagnostics.Process rather than the Bazel shell library; stdout/stderr are captured, +/// a timeout kills the process, and the exit status is reported through +/// and the command exception hierarchy. +/// +public sealed class CommandRunner +{ + private static readonly ILogger Logger = NullLogger.Instance; + + /// No input for the command. + public static readonly byte[] NoInput = Array.Empty(); + + /// By default, we kill the command after 15 minutes. + public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(15); + + public const int MaxCommandLength = 40000; + + public static readonly byte[] AfterLimitSuffix = + Encoding.UTF8.GetBytes("... (Rest of the output skipped)\n"); + + private readonly Command _cmd; + private readonly bool _verbose; + private readonly byte[] _input; + private readonly int _maxOutLogLines; + private readonly TimeSpan _timeout; + private readonly Stream? _asyncStdoutStream; + private readonly Stream? _asyncErrStream; + + private CommandRunner( + Command cmd, + bool verbose, + byte[] input, + int maxOutLogLines, + TimeSpan timeout, + Stream? asyncStdoutStream, + Stream? asyncErrStream) + { + _cmd = Preconditions.CheckNotNull(cmd); + _verbose = verbose; + _input = Preconditions.CheckNotNull(input); + _maxOutLogLines = maxOutLogLines; + _timeout = timeout; + _asyncStdoutStream = asyncStdoutStream; + _asyncErrStream = asyncErrStream; + } + + public CommandRunner(Command cmd) + : this(cmd, false, NoInput, -1, DefaultTimeout, null, null) + { + } + + public CommandRunner(Command cmd, TimeSpan timeout) + : this(cmd, false, NoInput, -1, timeout, null, null) + { + } + + /// Sets the verbose level for the command execution. + public CommandRunner WithVerbose(bool verbose) => + new(_cmd, verbose, _input, _maxOutLogLines, _timeout, _asyncStdoutStream, _asyncErrStream); + + /// Sets the input for the command execution. + public CommandRunner WithInput(byte[] input) => + new(_cmd, _verbose, input, _maxOutLogLines, _timeout, _asyncStdoutStream, _asyncErrStream); + + /// Sets the maximum number of output lines logged per stream. + public CommandRunner WithMaxStdOutLogLines(int lines) => + new(_cmd, _verbose, _input, lines, _timeout, _asyncStdoutStream, _asyncErrStream); + + /// Sets a stream to redirect stdOut output to. + public CommandRunner WithStdOutStream(Stream stream) => + new(_cmd, _verbose, _input, _maxOutLogLines, _timeout, stream, _asyncErrStream); + + /// Sets a stream to redirect stdErr output to. + public CommandRunner WithStdErrStream(Stream stream) => + new(_cmd, _verbose, _input, _maxOutLogLines, _timeout, _asyncStdoutStream, stream); + + /// + /// Executes the with the given input and writes to the console and the log + /// depending on the exit code of the command and the verbose flag. + /// + public CommandOutputWithStatus Execute() + { + var stopwatch = Stopwatch.StartNew(); + var argv = _cmd.GetCommandLineElements(); + string startMsg = ShellUtils.PrettyPrintArgv(argv); + startMsg = startMsg.Length > MaxCommandLength + ? startMsg.Substring(0, MaxCommandLength) + "..." + : startMsg; + string validStartMsg = "Executing [" + startMsg + "]"; + Logger.LogInformation("{Msg}", validStartMsg); + if (_verbose) + { + Console.Error.WriteLine(validStartMsg); + } + + var stdoutCollector = new MemoryStream(); + var stderrCollector = new MemoryStream(); + if (_asyncStdoutStream != null) + { + byte[] note = Encoding.UTF8.GetBytes("stdOut redirected to external observer."); + stdoutCollector.Write(note, 0, note.Length); + } + if (_asyncErrStream != null) + { + byte[] note = Encoding.UTF8.GetBytes("stdErr redirected to external observer."); + stderrCollector.Write(note, 0, note.Length); + } + + Stream stdoutStream = CommandOutputStream(_asyncStdoutStream ?? stdoutCollector, _maxOutLogLines); + Stream stderrStream = CommandOutputStream(_asyncErrStream ?? stderrCollector, _maxOutLogLines); + + string commandName = argv[0]; + TerminationStatus? exitStatus = null; + bool timedOut = false; + try + { + var startInfo = new ProcessStartInfo + { + FileName = commandName, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + for (int i = 1; i < argv.Count; i++) + { + startInfo.ArgumentList.Add(argv[i]); + } + var env = _cmd.GetEnvironmentVariables(); + if (env != null) + { + startInfo.Environment.Clear(); + foreach (var kvp in env) + { + startInfo.Environment[kvp.Key] = kvp.Value; + } + } + var workingDir = _cmd.GetWorkingDirectory(); + if (workingDir != null) + { + startInfo.WorkingDirectory = workingDir; + } + + using var process = new Process { StartInfo = startInfo }; + + var stdoutDone = new ManualResetEventSlim(false); + var stderrDone = new ManualResetEventSlim(false); + + try + { + process.Start(); + } + catch (Exception e) + { + throw new CommandException(_cmd, e); + } + + var pumpOut = PumpAsync(process.StandardOutput.BaseStream, stdoutStream, stdoutDone); + var pumpErr = PumpAsync(process.StandardError.BaseStream, stderrStream, stderrDone); + + // Write input then close stdin. + try + { + if (_input.Length > 0) + { + process.StandardInput.BaseStream.Write(_input, 0, _input.Length); + } + process.StandardInput.BaseStream.Flush(); + process.StandardInput.Close(); + } + catch (IOException) + { + // The process may have already exited and closed its stdin; ignore. + } + + bool exited = process.WaitForExit((int)Math.Min(_timeout.TotalMilliseconds, int.MaxValue)); + if (!exited) + { + timedOut = true; + try + { + process.Kill(entireProcessTree: true); + } + catch (Exception) + { + // best effort + } + process.WaitForExit(); + } + + // Ensure the output pumps drain fully after exit. + stdoutDone.Wait(); + stderrDone.Wait(); + pumpOut.GetAwaiter().GetResult(); + pumpErr.GetAwaiter().GetResult(); + + int rawResult = timedOut ? (128 + 15 /* SIGTERM */) : process.ExitCode; + var status = new TerminationStatus(rawResult); + exitStatus = status; + + byte[] stdoutBytes = stdoutCollector.ToArray(); + byte[] stderrBytes = stderrCollector.ToArray(); + + if (timedOut) + { + var result = new CommandResult(stdoutBytes, stderrBytes, status); + MaybeTreatTimeout(stdoutBytes, stderrBytes, true, + new AbnormalTerminationException(_cmd, result, status.ToString())); + } + + if (!status.Success()) + { + var result = new CommandResult(stdoutBytes, stderrBytes, status); + string message = + $"Process '{commandName}' exited with status {status.ToShortString()}"; + throw new BadExitStatusWithOutputException(_cmd, result, message, stdoutBytes, stderrBytes); + } + + return new CommandOutputWithStatus(status, stdoutBytes, stderrBytes); + } + finally + { + if (_maxOutLogLines != 0) + { + LogOutput( + LogLevel.Information, $"'{commandName}' STDOUT: ", stdoutCollector, _maxOutLogLines); + LogOutput( + LogLevel.Information, $"'{commandName}' STDERR: ", stderrCollector, _maxOutLogLines); + } + + string finishMsg; + if (timedOut) + { + finishMsg = string.Format( + "Command '{0}' was killed after timeout. Execution time {1}. {2}", + commandName, + FormatDuration(stopwatch.Elapsed), + exitStatus != null ? exitStatus.ToString() : "(No exit status)"); + Logger.LogError("{Msg}", finishMsg); + } + else + { + finishMsg = string.Format( + "Command '{0}' finished in {1}. {2}", + commandName, + FormatDuration(stopwatch.Elapsed), + exitStatus != null ? exitStatus.ToString() : "(No exit status)"); + Logger.LogInformation("{Msg}", finishMsg); + } + if (_verbose) + { + Console.Error.WriteLine(finishMsg); + } + } + } + + private static Task PumpAsync(Stream source, Stream destination, ManualResetEventSlim done) + { + return Task.Run(() => + { + try + { + var buffer = new byte[8192]; + int read; + while ((read = source.Read(buffer, 0, buffer.Length)) > 0) + { + destination.Write(buffer, 0, read); + } + destination.Flush(); + } + catch (Exception) + { + // Stream closed on process exit; ignore. + } + finally + { + done.Set(); + } + }); + } + + /// + /// Format a duration to a human-readable string. Assumes the duration is less than 24 hours, + /// which should always be true for a command. + /// + private static string FormatDuration(TimeSpan duration) => duration.ToString(@"mm\:ss\.fff"); + + private void MaybeTreatTimeout( + byte[] stdout, byte[] stderr, bool hasTimedOut, AbnormalTerminationException e) + { + if (!hasTimedOut) + { + return; + } + string msg = string.Format( + "Command '{0}' killed by Copybara after timeout ({1}s)." + + " If this fails during a fetch or push use --repo-timeout flag." + + " If this fails during checkout or another point, use --commands-timeout flag.\n" + + "Exit info: {2}", + _cmd.GetCommandLineElements()[0], + (long)_timeout.TotalSeconds, + e.GetResult().TerminationStatus); + throw new CommandTimeoutException( + e.GetCommand(), e.GetResult(), msg, stdout, stderr, _timeout); + } + + /// Creates the OutputStream to feed the process output to. + private Stream CommandOutputStream(Stream outputStream, int maxOutLogLines) + { + // If verbose we stream to the user console too. + if (!_verbose) + { + return outputStream; + } + var console = Console.OpenStandardError(); + var limited = new LimitFilterOutputStream( + console, + // Assume a line has ~200 characters. If no limit, ~10k lines. + maxOutLogLines > 0 ? maxOutLogLines * 200 : 10000 * 200, + AfterLimitSuffix); + return new MultiplexOutputStream(limited, outputStream); + } + + private static void LogOutput( + LogLevel level, string prefix, MemoryStream outputBytes, int maxLogLines) + { + string s = Encoding.UTF8.GetString(outputBytes.ToArray()).Trim(); + if (s.Length == 0) + { + return; + } + int lines = 0; + foreach (var line in s.Split(Environment.NewLine)) + { + Logger.Log(level, "{Prefix}{Line}", prefix, line); + lines++; + if (maxLogLines >= 0 && lines >= maxLogLines) + { + Logger.Log(level, "{Prefix}... truncated after {Max} line(s)", prefix, maxLogLines); + break; + } + } + } + + /// An that writes to two underlying streams. + private sealed class MultiplexOutputStream : Stream + { + private readonly Stream _s1; + private readonly Stream _s2; + + public MultiplexOutputStream(Stream s1, Stream s2) + { + _s1 = s1; + _s2 = s2; + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _s1.Write(buffer, offset, count); + _s2.Write(buffer, offset, count); + } + + public override void WriteByte(byte value) + { + _s1.WriteByte(value); + _s2.WriteByte(value); + } + + public override void Flush() + { + _s1.Flush(); + _s2.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + } +} diff --git a/src/Copybara.Core/Util/CommandTimeoutException.cs b/src/Copybara.Core/Util/CommandTimeoutException.cs new file mode 100644 index 000000000..2c0cdb6ec --- /dev/null +++ b/src/Copybara.Core/Util/CommandTimeoutException.cs @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util; + +/// +/// An exception that represents a program that timed out and was killed. Port of +/// com.google.copybara.util.CommandTimeoutException. +/// +public class CommandTimeoutException : AbnormalTerminationException +{ + public CommandTimeoutException( + Command command, + CommandResult result, + string message, + byte[] stdout, + byte[] stderr, + TimeSpan timeout) + : base(Preconditions.CheckNotNull(command), result, Preconditions.CheckNotNull(message)) + { + Timeout = timeout; + Output = new CommandOutputWithStatus( + result.TerminationStatus, + Preconditions.CheckNotNull(stdout), + Preconditions.CheckNotNull(stderr)); + } + + public CommandOutputWithStatus Output { get; } + + public TimeSpan Timeout { get; } + + public CommandOutputWithStatus GetOutput() => Output; + + public TimeSpan GetTimeout() => Timeout; +} diff --git a/src/Copybara.Core/Util/Console/AnsiColor.cs b/src/Copybara.Core/Util/Console/AnsiColor.cs new file mode 100644 index 000000000..e9b0419cd --- /dev/null +++ b/src/Copybara.Core/Util/Console/AnsiColor.cs @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util.Console; + +/// +/// Colors to print messages in the console. +/// +public enum AnsiColor +{ + Reset, + Black, + Red, + Green, + Yellow, + Blue, + Purple, + Cyan, + White, +} + +/// +/// Helpers associated with that map each color to its ANSI escape code and +/// wrap text with the color, resetting afterwards. +/// +public static class AnsiColors +{ + private static string Code(AnsiColor color) => color switch + { + AnsiColor.Reset => "", + AnsiColor.Black => "", + AnsiColor.Red => "", + AnsiColor.Green => "", + AnsiColor.Yellow => "", + AnsiColor.Blue => "", + AnsiColor.Purple => "", + AnsiColor.Cyan => "", + AnsiColor.White => "", + _ => "", + }; + + /// Wraps with the color's ANSI code, resetting afterwards. + public static string Write(this AnsiColor color, string text) + { + return Code(color) + text + Code(AnsiColor.Reset); + } +} diff --git a/src/Copybara.Core/Util/Console/AnsiConsole.cs b/src/Copybara.Core/Util/Console/AnsiConsole.cs new file mode 100644 index 000000000..f54920872 --- /dev/null +++ b/src/Copybara.Core/Util/Console/AnsiConsole.cs @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util.Console; + +/// +/// A console that prints the output using fancy ANSI capabilities. +/// +public sealed class AnsiConsole : Console +{ + private static readonly string RemoveLine = AnsiEscapes.OneLineUp() + AnsiEscapes.DeleteLine(); + + private readonly TextReader _input; + private readonly TextWriter _output; + private readonly object _lock = new(); + private readonly bool _verbose; + + private int _lastProgressLines = 0; + + // blue red yellow blue green red + public AnsiConsole(TextReader input, TextWriter output, bool verbose) + { + _input = Preconditions.CheckNotNull(input); + _output = Preconditions.CheckNotNull(output); + _verbose = verbose; + } + + public void StartupMessage(string version) + { + // Just because we can! + _output.WriteLine( + AnsiColor.Blue.Write("C") + + AnsiColor.Red.Write("o") + + AnsiColor.Yellow.Write("p") + + AnsiColor.Blue.Write("y") + + AnsiColor.Green.Write("b") + + AnsiColor.Red.Write("a") + + AnsiColor.Blue.Write("r") + + AnsiColor.Red.Write("a") + + " source mover (Version: " + version + ")"); + } + + public void Error(string message) + { + lock (_lock) + { + _lastProgressLines = 0; + _output.WriteLine(AnsiColor.Red.Write("ERROR: ") + message); + } + } + + public void Warn(string message) + { + lock (_lock) + { + _lastProgressLines = 0; + _output.WriteLine(AnsiColor.Yellow.Write("WARN: ") + message); + } + } + + public void Info(string message) + { + lock (_lock) + { + _lastProgressLines = 0; + _output.WriteLine(AnsiColor.Green.Write("INFO: ") + message); + } + } + + public string Ask(string msg, string? defaultAnswer, Func validator) + { + _output.Write(AnsiColor.Blue.Write("Question: ") + msg); + string? line; + while ((line = _input.ReadLine()) != null) + { + string answer = line.Trim(); + if (string.IsNullOrEmpty(answer) && defaultAnswer != null) + { + return defaultAnswer; + } + if (validator(answer)) + { + return answer; + } + Error("Invalid answer: " + answer); + _output.Write(AnsiColor.Blue.Write("Question: ") + msg); + } + // TODO(malcon): Refactor console to throw interrupted in this case. + throw new IOException("Cancelled by user"); + } + + public string AskWithErrorMessage( + string msg, string? defaultAnswer, EnhancedPredicate enhancedValidator) + { + return ((Console)this).AskWithErrorMessage(_input, _output, msg, defaultAnswer, enhancedValidator); + } + + public bool IsVerbose => _verbose; + + public void Progress(string progress) + { + lock (_lock) + { + if (_lastProgressLines > 0) + { + _output.Write(Repeat(RemoveLine, _lastProgressLines)); + } + _output.WriteLine(AnsiColor.Green.Write("Task: ") + progress); + _lastProgressLines = 1 + CountNewlines(progress); + } + } + + public bool PromptConfirmation(string message) + { + return new ConsolePrompt( + _input, + new DelegatingPromptPrinter(msg => + { + lock (_lock) + { + _lastProgressLines = 0; + _output.Write(AnsiColor.Yellow.Write("WARN: ") + msg + " [y/n] "); + } + })).PromptConfirmation(message); + } + + public string Colorize(AnsiColor ansiColor, string message) => ansiColor.Write(message); + + private static int CountNewlines(string text) + { + int count = 0; + foreach (char c in text) + { + if (c == '\n') + { + count++; + } + } + return count; + } + + private static string Repeat(string value, int times) + { + return times <= 0 ? string.Empty : string.Concat(Enumerable.Repeat(value, times)); + } +} diff --git a/src/Copybara.Core/Util/Console/AnsiEscapes.cs b/src/Copybara.Core/Util/Console/AnsiEscapes.cs new file mode 100644 index 000000000..a9c1b79dd --- /dev/null +++ b/src/Copybara.Core/Util/Console/AnsiEscapes.cs @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util.Console; + +internal static class AnsiEscapes +{ + internal static string OneLineUp() => "[1A"; + + internal static string DeleteLine() => "[2K"; +} diff --git a/src/Copybara.Core/Util/Console/CapturingConsole.cs b/src/Copybara.Core/Util/Console/CapturingConsole.cs new file mode 100644 index 000000000..84bdb1aa6 --- /dev/null +++ b/src/Copybara.Core/Util/Console/CapturingConsole.cs @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using MessageType = Copybara.Util.Console.Message.MessageType; + +namespace Copybara.Util.Console; + +/// +/// A that captures the error/warn/info messages preserving the order. +/// +/// All the methods delegate on another . +/// +/// Uses a list behind a lock and it's unbounded. +/// +public class CapturingConsole : DelegateConsole +{ + protected static readonly ImmutableHashSet AllTypes = + Enum.GetValues().ToImmutableHashSet(); + + private readonly List _messages = new(); + private readonly ISet _captureTypes; + private readonly object _lock = new(); + + /// Creates a new that captures all s. + public static CapturingConsole CaptureAllConsole(Console @delegate) + { + return new CapturingConsole(@delegate, AllTypes); + } + + /// + /// Creates a new that captures only the specified + /// s. + /// + public static CapturingConsole CaptureOnlyConsole( + Console @delegate, MessageType first, params MessageType[] others) + { + var builder = ImmutableHashSet.CreateBuilder(); + builder.Add(first); + foreach (var other in others) + { + builder.Add(other); + } + return new CapturingConsole(@delegate, builder.ToImmutable()); + } + + protected CapturingConsole(Console @delegate, ISet captureTypes) + : base(@delegate) + { + _captureTypes = captureTypes; + } + + public ImmutableArray GetMessages() + { + lock (_lock) + { + return _messages.ToImmutableArray(); + } + } + + public void ClearMessages() + { + lock (_lock) + { + _messages.Clear(); + } + } + + protected override void HandleMessage(MessageType type, string message) + { + lock (_lock) + { + if (_captureTypes.Contains(type)) + { + _messages.Add(new Message(type, message)); + } + } + } +} diff --git a/src/Copybara.Core/Util/Console/Console.cs b/src/Copybara.Core/Util/Console/Console.cs new file mode 100644 index 000000000..dbfef0337 --- /dev/null +++ b/src/Copybara.Core/Util/Console/Console.cs @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util.Console; + +/// +/// Write user messages to the console. +/// +public interface Console : IDisposable +{ + /// Print the Copybara welcome message. + void StartupMessage(string version); + + /// Print an error in the console. + void Error(string message); + + /// Print a format string as error on the console. + void ErrorFmt(string format, params object?[] args) + { + Error(ConsoleFormat.Printf(format, args)); + } + + /// Print a warning in the console. + void Warn(string message); + + /// Print a format string as warn on the console. + void WarnFmt(string format, params object?[] args) + { + Warn(ConsoleFormat.Printf(format, args)); + } + + /// Console warn if is true, otherwise do nothing. + void WarnFmtIf(bool condition, string format, params object?[] args) + { + if (condition) + { + WarnFmt(format, args); + } + } + + /// Returns true if verbose. + bool IsVerbose { get; } + + /// Print an informational message in the console, if verbose logging is enabled. + void Verbose(string message) + { + if (IsVerbose) + { + Info(message); + } + } + + /// Print a format string as info on the console, if verbose logging is enabled. + void VerboseFmt(string format, params object?[] args) + { + Verbose(ConsoleFormat.Printf(format, args)); + } + + /// + /// Print an informational message in the console. + /// + /// Warning: Do not abuse the usage of this method. We don't want to spam our users. When in + /// doubt, use verbose. + /// + void Info(string message); + + /// Print a format string as info on the console. + void InfoFmt(string format, params object?[] args) + { + Info(ConsoleFormat.Printf(format, args)); + } + + /// Print a progress message in the console. + void Progress(string progress); + + /// Print a format string as progress on the console. + void ProgressFmt(string format, params object?[] args) + { + Progress(ConsoleFormat.Printf(format, args)); + } + + /// + /// Returns true if this Console's input registers Y/y after showing the prompt message. + /// + bool PromptConfirmation(string message); + + /// Like , but takes a format String as argument. + bool PromptConfirmationFmt(string format, params object?[] args) + { + return PromptConfirmation(ConsoleFormat.Printf(format, args)); + } + + string Ask(string msg, string? defaultAnswer, Func validator) + { + throw new InvalidOperationException("Interactive prompt not allowed in " + GetType()); + } + + // TODO(malcon): Delete method + string AskWithErrorMessage( + TextReader input, + TextWriter output, + string msg, + string? defaultAnswer, + EnhancedPredicate enhancedValidator) + { + output.Write(Colorize(AnsiColor.Blue, "Question: ") + msg); + string? line; + while ((line = input.ReadLine()) != null) + { + string answer = line.Trim(); + if (string.IsNullOrEmpty(answer) && defaultAnswer != null) + { + return defaultAnswer; + } + if (enhancedValidator.Predicate(answer)) + { + return answer; + } + Error("Invalid answer: " + answer); + Error(enhancedValidator.ErrorMsg); + output.Write(AnsiColor.Blue.Write("Question: ") + msg); + } + throw new IOException("Cancelled by user"); + } + + string AskWithErrorMessage(string msg, string? defaultAnswer, EnhancedPredicate enhancedValidator) + { + throw new InvalidOperationException("Interactive prompt not allowed in " + GetType()); + } + + /// + /// Given a message and a console that support colors, return a string that prints the message in + /// the . + /// + /// Note that not all consoles support colors, so messages should be readable without + /// colors. + /// + string Colorize(AnsiColor ansiColor, string message); + + /// Prints a prompt message. + interface PromptPrinter + { + void Print(string message); + } + + /// Close this console, freeing resources. + void IDisposable.Dispose() + { + } +} diff --git a/src/Copybara.Core/Util/Console/ConsoleFormat.cs b/src/Copybara.Core/Util/Console/ConsoleFormat.cs new file mode 100644 index 000000000..ff7ec4aa1 --- /dev/null +++ b/src/Copybara.Core/Util/Console/ConsoleFormat.cs @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; + +namespace Copybara.Util.Console; + +/// +/// Translates the Java String.format printf-style format strings used throughout the console +/// subsystem (the *Fmt methods) into concrete strings. +/// +/// Callers pass printf conversions such as %s and %d. This helper consumes the +/// format arguments sequentially, replacing conversions with the corresponding argument. %% +/// is emitted as a literal %. Any conversion is rendered simply via the argument's string +/// representation, which is sufficient for the console's usage. +/// +internal static class ConsoleFormat +{ + public static string Printf(string format, params object?[] args) + { + if (format == null) + { + throw new ArgumentNullException(nameof(format)); + } + args ??= Array.Empty(); + + var sb = new StringBuilder(format.Length + 16); + int argIndex = 0; + for (int i = 0; i < format.Length; i++) + { + char c = format[i]; + if (c != '%') + { + sb.Append(c); + continue; + } + + // Lone '%' at the end: emit as-is. + if (i + 1 >= format.Length) + { + sb.Append('%'); + break; + } + + char next = format[i + 1]; + if (next == '%') + { + sb.Append('%'); + i++; + continue; + } + + if (next == 'n') + { + sb.Append(Environment.NewLine); + i++; + continue; + } + + // Any other conversion (e.g. %s, %d) consumes the next argument sequentially. + if (argIndex < args.Length) + { + sb.Append(Convert(args[argIndex])); + argIndex++; + } + else + { + // Not enough arguments: keep the conversion literally. + sb.Append(c); + sb.Append(next); + } + i++; + } + + return sb.ToString(); + } + + private static string Convert(object? arg) => arg?.ToString() ?? "null"; +} diff --git a/src/Copybara.Core/Util/Console/ConsolePrompt.cs b/src/Copybara.Core/Util/Console/ConsolePrompt.cs new file mode 100644 index 000000000..d31789f9b --- /dev/null +++ b/src/Copybara.Core/Util/Console/ConsolePrompt.cs @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util.Console; + +/// Reads the input from a . +internal sealed class ConsolePrompt +{ + private static readonly HashSet Yes = new(StringComparer.Ordinal) { "y", "yes" }; + private static readonly HashSet No = new(StringComparer.Ordinal) { "n", "no" }; + + private readonly TextReader _input; + private readonly Console.PromptPrinter _promptPrinter; + + internal ConsolePrompt(TextReader input, Console.PromptPrinter promptPrinter) + { + _input = Preconditions.CheckNotNull(input); + _promptPrinter = Preconditions.CheckNotNull(promptPrinter); + } + + internal bool PromptConfirmation(string message) + { + _promptPrinter.Print(message); + string? line; + while ((line = _input.ReadLine()) != null) + { + string answer = line.Trim().ToLowerInvariant(); + if (Yes.Contains(answer)) + { + return true; + } + if (No.Contains(answer)) + { + return false; + } + _promptPrinter.Print(message); + } + // EOF while reading from the input (user cancelled) + return false; + } +} + +/// +/// Adapts a delegate to the interface, replacing the Java +/// lambda-based prompt printers. +/// +internal sealed class DelegatingPromptPrinter : Console.PromptPrinter +{ + private readonly Action _print; + + internal DelegatingPromptPrinter(Action print) + { + _print = Preconditions.CheckNotNull(print); + } + + public void Print(string message) => _print(message); +} diff --git a/src/Copybara.Core/Util/Console/Consoles.cs b/src/Copybara.Core/Util/Console/Consoles.cs new file mode 100644 index 000000000..4eaffa65f --- /dev/null +++ b/src/Copybara.Core/Util/Console/Consoles.cs @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using Microsoft.Extensions.Logging; + +namespace Copybara.Util.Console; + +/// +/// Utility methods for working with s. +/// +public static class Consoles +{ + /// + /// Logs text as separate lines using . If is an + /// empty string, does nothing. + /// + public static void LogLines(Console console, string prefix, string text) + { + ConsoleLogLines(prefix, text, console.Info); + } + + /// + /// Logs text as separate lines using . If is an + /// empty string, does nothing. + /// + public static void ErrorLogLines(Console console, string prefix, string text) + { + ConsoleLogLines(prefix, text, console.Error); + } + + /// + /// Logs text as separate lines using if verbose is enabled. + /// + public static void VerboseLogLines(Console console, string prefix, string text) + { + ConsoleLogLines(prefix, text, console.Verbose); + } + + private static void ConsoleLogLines(string prefix, string text, Action logLevel) + { + string[] lines = text.Split('\n'); + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + bool isLast = i == lines.Length - 1; + if (line.Length == 0 && isLast) + { + break; + } + logLevel(prefix + line); + } + } + + public static void PrintCauseChain( + LogLevel level, Console console, string[] args, Exception e, ILogger? logger = null) + { + var error = new StringBuilder(e.Message).Append('\n'); + var suppressed = new List(); + Exception? cause = e.InnerException; + while (cause != null) + { + error.Append(" CAUSED BY: ").Append(PrintException(cause)).Append('\n'); + cause = cause.InnerException; + } + foreach (Exception t in suppressed) + { + PrintCauseChain(level, console, args, t, logger); + } + console.Error(error.ToString()); + logger?.Log(level, e, "{Message}", FormatLogError(e.Message, args)); + } + + private static string? PrintException(Exception t) + { + // In the Java original this special-cases EvalException to use getMessageWithStack(); the + // C# EvalException port only exposes Message, so the plain message is used for all types. + return t.Message; + } + + public static string FormatLogError(string message, string[] args) + { + return $"{message} (command args: [{string.Join(", ", args)}])"; + } +} diff --git a/src/Copybara.Core/Util/Console/DelegateConsole.cs b/src/Copybara.Core/Util/Console/DelegateConsole.cs new file mode 100644 index 000000000..9bf397fc6 --- /dev/null +++ b/src/Copybara.Core/Util/Console/DelegateConsole.cs @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using MessageType = Copybara.Util.Console.Message.MessageType; + +namespace Copybara.Util.Console; + +/// +/// A simple console that can be extended to delegate automatically to another console. +/// +/// This console delegates all the methods on the delegate, while implementors can handle the +/// messages written to the console, while not having to deal with the other methods. The reason is +/// to have implementors that can output the console contents to files or other formats while not +/// having to implement the delegate pattern over and over again. +/// +public abstract class DelegateConsole : Console +{ + private readonly Console _delegate; + + protected DelegateConsole(Console @delegate) + { + _delegate = Preconditions.CheckNotNull(@delegate); + } + + public virtual void StartupMessage(string version) + { + HandleMessage(MessageType.Info, "Copybara (Version: " + version + ")"); + _delegate.StartupMessage(version); + } + + public virtual void Error(string message) + { + HandleMessage(MessageType.Error, message); + _delegate.Error(message); + } + + public virtual void Warn(string message) + { + HandleMessage(MessageType.Warning, message); + _delegate.Warn(message); + } + + public virtual bool IsVerbose => _delegate.IsVerbose; + + public virtual void Info(string message) + { + HandleMessage(MessageType.Info, message); + _delegate.Info(message); + } + + public virtual void Progress(string message) + { + HandleMessage(MessageType.Progress, message); + _delegate.Progress(message); + } + + public virtual void Verbose(string message) + { + HandleMessage(MessageType.Verbose, message); + _delegate.Verbose(message); + } + + public virtual string Ask(string msg, string? defaultAnswer, Func validator) + { + return _delegate.Ask(msg, defaultAnswer, validator); + } + + public virtual bool PromptConfirmation(string message) + { + return _delegate.PromptConfirmation(message); + } + + public virtual string Colorize(AnsiColor ansiColor, string message) + { + return _delegate.Colorize(ansiColor, message); + } + + public virtual void Dispose() + { + _delegate.Dispose(); + } + + /// Handle the message type and contents. + protected abstract void HandleMessage(MessageType info, string message); +} diff --git a/src/Copybara.Core/Util/Console/EnhancedPredicate.cs b/src/Copybara.Core/Util/Console/EnhancedPredicate.cs new file mode 100644 index 000000000..9302d3204 --- /dev/null +++ b/src/Copybara.Core/Util/Console/EnhancedPredicate.cs @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util.Console; + +/// Enhanced Predicate object for use with Copybara console objects. +public sealed class EnhancedPredicate +{ + private readonly Func _predicate; + private readonly string _errorMsg; + + private EnhancedPredicate(Func predicate, string errorMsg) + { + _predicate = predicate; + _errorMsg = errorMsg; + } + + public static EnhancedPredicate Create(Func predicate, string errorMsg) + { + return new EnhancedPredicate( + Preconditions.CheckNotNull(predicate), Preconditions.CheckNotNull(errorMsg)); + } + + /// Tests against the underlying predicate. + public bool Predicate(string value) => _predicate(value); + + public string ErrorMsg => _errorMsg; +} diff --git a/src/Copybara.Core/Util/Console/FileConsole.cs b/src/Copybara.Core/Util/Console/FileConsole.cs new file mode 100644 index 000000000..d915b5e1e --- /dev/null +++ b/src/Copybara.Core/Util/Console/FileConsole.cs @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Globalization; +using System.Text; +using Copybara.Common; +using MessageType = Copybara.Util.Console.Message.MessageType; + +namespace Copybara.Util.Console; + +/// +/// A capable of writing the output into a file and to a delegate console. +/// +/// If any of the file operations fail, the console won't try to write to the file anymore and +/// will only call to the delegate console. +/// +/// Caller is responsible for closing this console to free resources. +/// +/// The console can be configured to flush on a fixed rate intervals. +/// +public class FileConsole : DelegateConsole +{ + private const string DatePrefixFmt = "MMdd HH:mm:ss.fff"; + + protected readonly string FilePath; + private readonly Timer? _flushingTimer; + + // Serializes writes to the underlying writer, standing in for the Java single-thread executor. + private readonly object _loggingLock = new(); + private readonly object _writerLock = new(); + + private bool _shutdown = false; + private bool _failed = false; + private TextWriter? _writer; + + /// Creates a new . + /// A delegate console. + /// + /// A file path to write to. The parent directories must be created in advance. + /// + /// How often to flush this file console. + public FileConsole(Console @delegate, string filePath, TimeSpan consoleFlushRate) + : base(@delegate) + { + FilePath = Preconditions.CheckNotNull(filePath); + if (consoleFlushRate <= TimeSpan.Zero) + { + _flushingTimer = null; + } + else + { + _flushingTimer = new Timer(_ => Flush(), null, consoleFlushRate, consoleFlushRate); + } + } + + protected override void HandleMessage(MessageType type, string message) + { + TextWriter? writer = GetWriter(); + if (writer == null) + { + return; + } + // Submitting to a shut-down executor throws, so this has to be synchronized to avoid shutting + // down in-between from another thread. + lock (_loggingLock) + { + if (_shutdown) + { + return; + } + string now = DateTime.Now.ToString(DatePrefixFmt, CultureInfo.InvariantCulture); + DoWrite(writer, $"{now} {type}: {message}\n"); + } + } + + private void DoWrite(TextWriter writer, string s) + { + if (_failed) + { + return; + } + try + { + writer.Write(s); + } + catch (IOException) + { + _failed = true; + } + } + + private void Flush() + { + if (_failed) + { + return; + } + TextWriter? writer = GetWriter(); + if (writer != null) + { + try + { + lock (_loggingLock) + { + writer.Flush(); + } + } + catch (IOException) + { + _failed = true; + } + } + } + + private TextWriter? GetWriter() + { + lock (_writerLock) + { + if (_writer == null) + { + _writer = InitWriter(); + } + return _writer; + } + } + + private TextWriter? InitWriter() + { + try + { + // TRUNCATE_EXISTING if the file exists, otherwise CREATE_NEW: FileMode.Create matches + // both cases (create or truncate). + var stream = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + return new StreamWriter(stream, new UTF8Encoding(false)); + } + catch (IOException) + { + _failed = true; + } + return null; + } + + public override void Dispose() + { + base.Dispose(); + lock (_loggingLock) + { + _shutdown = true; + } + _flushingTimer?.Dispose(); + try + { + if (_writer == null) + { + return; + } + _writer.Dispose(); + } + catch (IOException) + { + _failed = true; + } + } +} diff --git a/src/Copybara.Core/Util/Console/LogConsole.cs b/src/Copybara.Core/Util/Console/LogConsole.cs new file mode 100644 index 000000000..f41a08691 --- /dev/null +++ b/src/Copybara.Core/Util/Console/LogConsole.cs @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util.Console; + +/// +/// A simple console logger that prefixes the output with the time. +/// +public sealed class LogConsole : Console +{ + private const string DatePrefixFmt = "MMdd HH:mm:ss.fff"; + + private readonly TextReader? _input; + private readonly TextWriter _output; + private readonly bool _verbose; + + /// Creates a new instance of with write capabilities, only. + public static LogConsole WriteOnlyConsole(TextWriter output, bool verbose) + { + return new LogConsole(null, Preconditions.CheckNotNull(output), verbose); + } + + /// Creates a new instance of with read and write capabilities. + public static LogConsole ReadWriteConsole(TextReader input, TextWriter output, bool verbose) + { + return new LogConsole( + Preconditions.CheckNotNull(input), Preconditions.CheckNotNull(output), verbose); + } + + private LogConsole(TextReader? input, TextWriter output, bool verbose) + { + _input = input; + _output = Preconditions.CheckNotNull(output); + _verbose = verbose; + } + + public void StartupMessage(string version) + { + _output.WriteLine("Copybara (Version: " + version + ")"); + } + + public bool IsVerbose => _verbose; + + public void Error(string message) => PrintMessage("ERROR", message); + + public void Warn(string message) => PrintMessage("WARN", message); + + public void Info(string message) => PrintMessage("INFO", message); + + public void Progress(string task) => PrintMessage("TASK", task); + + public bool PromptConfirmation(string message) + { + Preconditions.CheckState( + _input != null, + "LogConsole cannot read user input if system console is not present."); + return new ConsolePrompt( + _input!, + new DelegatingPromptPrinter(msg => _output.Write($"{NowToString()} WARN: {msg} [y/n] "))) + .PromptConfirmation(message); + } + + public string Colorize(AnsiColor ansiColor, string message) => message; + + private void PrintMessage(string messageKind, string message) + { + _output.WriteLine($"{NowToString()} {messageKind}: {message}"); + } + + private static string NowToString() + { + return DateTime.Now.ToString(DatePrefixFmt, System.Globalization.CultureInfo.InvariantCulture); + } +} diff --git a/src/Copybara.Core/Util/Console/Message.cs b/src/Copybara.Core/Util/Console/Message.cs new file mode 100644 index 000000000..dcb952543 --- /dev/null +++ b/src/Copybara.Core/Util/Console/Message.cs @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util.Console; + +/// +/// Represents a message registered in a console. +/// +public sealed class Message +{ + /// + /// The type of messages registered in a console. + /// + public enum MessageType + { + Error, + Warning, + Info, + Verbose, + Progress, + Prompt, + } + + private readonly MessageType _type; + private readonly string _text; + + public static Message Error(string text) => new(MessageType.Error, text); + + public static Message Warning(string text) => new(MessageType.Warning, text); + + public static Message Info(string text) => new(MessageType.Info, text); + + public Message(MessageType type, string text) + { + _type = type; + _text = Preconditions.CheckNotNull(text); + } + + public MessageType Type => _type; + + public string Text => _text; + + public override string ToString() => _type + ": " + _text; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var message = (Message)o; + return _type == message._type && string.Equals(_text, message._text); + } + + public override int GetHashCode() => HashCode.Combine(_type, _text); +} diff --git a/src/Copybara.Core/Util/Console/MultiplexingConsole.cs b/src/Copybara.Core/Util/Console/MultiplexingConsole.cs new file mode 100644 index 000000000..ced5a987b --- /dev/null +++ b/src/Copybara.Core/Util/Console/MultiplexingConsole.cs @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using MessageType = Copybara.Util.Console.Message.MessageType; + +namespace Copybara.Util.Console; + +/// +/// A console logging to two delegates. Prompt is not supported for delegate2. +/// +public class MultiplexingConsole : DelegateConsole +{ + private readonly Console _delegate2; + + public MultiplexingConsole(Console delegate1, Console delegate2) + : base(delegate1) + { + _delegate2 = Preconditions.CheckNotNull(delegate2); + } + + protected override void HandleMessage(MessageType type, string message) + { + switch (type) + { + case MessageType.Error: + _delegate2.Error(message); + break; + case MessageType.Warning: + _delegate2.Warn(message); + break; + case MessageType.Verbose: + _delegate2.Verbose(message); + break; + case MessageType.Progress: + _delegate2.Progress(message); + break; + case MessageType.Info: + case MessageType.Prompt: + _delegate2.Info(message); + break; + } + } +} diff --git a/src/Copybara.Core/Util/Console/NoPromptConsole.cs b/src/Copybara.Core/Util/Console/NoPromptConsole.cs new file mode 100644 index 000000000..bf071c408 --- /dev/null +++ b/src/Copybara.Core/Util/Console/NoPromptConsole.cs @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MessageType = Copybara.Util.Console.Message.MessageType; + +namespace Copybara.Util.Console; + +/// +/// A console that skips y/n prompts. +/// +public class NoPromptConsole : DelegateConsole +{ + private readonly bool _defaultAnswer; + + public NoPromptConsole(Console @delegate, bool defaultAnswer) + : base(@delegate) + { + _defaultAnswer = defaultAnswer; + } + + public override bool PromptConfirmation(string msg) + { + Info("Prompt: " + msg); + Info("Answering: " + (_defaultAnswer ? "yes" : "no")); + return _defaultAnswer; + } + + protected override void HandleMessage(MessageType info, string message) + { + } +} diff --git a/src/Copybara.Core/Util/Console/PrefixConsole.cs b/src/Copybara.Core/Util/Console/PrefixConsole.cs new file mode 100644 index 000000000..4515b321a --- /dev/null +++ b/src/Copybara.Core/Util/Console/PrefixConsole.cs @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util.Console; + +/// +/// A console that delegates to another console but adds a prefix to all its messages. +/// +public class PrefixConsole : Console +{ + private readonly string _prefix; + private readonly Console _delegate; + + public PrefixConsole(string prefix, Console @delegate) + { + _prefix = Preconditions.CheckNotNull(prefix); + _delegate = Preconditions.CheckNotNull(@delegate); + } + + public void StartupMessage(string version) => _delegate.StartupMessage(version); + + public bool IsVerbose => _delegate.IsVerbose; + + public void Error(string message) => _delegate.Error(Prefix(message)); + + public void Warn(string message) => _delegate.Warn(Prefix(message)); + + public void Info(string message) => _delegate.Info(Prefix(message)); + + public void Progress(string progress) => _delegate.Progress(Prefix(progress)); + + public bool PromptConfirmation(string message) => _delegate.PromptConfirmation(Prefix(message)); + + public string Ask(string msg, string? defaultAnswer, Func validator) + { + return _delegate.Ask(msg, defaultAnswer, validator); + } + + private string Prefix(string progress) => _prefix + progress; + + public string Colorize(AnsiColor ansiColor, string message) => message; +} diff --git a/src/Copybara.Core/Util/Console/StarlarkMode.cs b/src/Copybara.Core/Util/Console/StarlarkMode.cs new file mode 100644 index 000000000..dd535e4d3 --- /dev/null +++ b/src/Copybara.Core/Util/Console/StarlarkMode.cs @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util.Console; + +/// +/// Modes for parsing config files. +/// +public enum StarlarkMode +{ + Loose, + Strict, +} diff --git a/src/Copybara.Core/Util/DirFactory.cs b/src/Copybara.Core/Util/DirFactory.cs new file mode 100644 index 000000000..7b21f9681 --- /dev/null +++ b/src/Copybara.Core/Util/DirFactory.cs @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util; + +/// +/// A supplier of output directories under a given root. Port of +/// com.google.copybara.util.DirFactory. +/// +/// This factory allows Copybara to create all the files in a self-contained root, that can be +/// configured by users, and allows for temporary file cleanup, and directory reuse if necessary. +/// +public class DirFactory +{ + public const string Tmp = "temp"; + private const string Cache = "cache"; + + private readonly string _rootPath; + + public DirFactory(string rootPath) => _rootPath = Preconditions.CheckNotNull(rootPath); + + /// Get the cache directory for . + public string GetCacheDir(string name) + { + string dir = Path.Combine(_rootPath, Cache, name); + Directory.CreateDirectory(dir); + return dir; + } + + /// Creates a temp directory in the root path. + public string NewTempDir(string name) + { + string outputPath = GetTmpRoot(); + // Create the output if it does not exist. + Directory.CreateDirectory(outputPath); + // Mirror Files.createTempDirectory(outputPath, name): a unique directory whose name starts + // with the given prefix. + string dir = Path.Combine(outputPath, name + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + return dir; + } + + public void CleanupTempDirs() + { + string outputPath = GetTmpRoot(); + if (Directory.Exists(outputPath)) + { + FileUtil.DeleteRecursively(outputPath); + } + } + + public string GetTmpRoot() => Path.Combine(_rootPath, Tmp); +} diff --git a/src/Copybara.Core/Util/ExitCode.cs b/src/Copybara.Core/Util/ExitCode.cs new file mode 100644 index 000000000..ca3302d34 --- /dev/null +++ b/src/Copybara.Core/Util/ExitCode.cs @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util; + +/// +/// Exit codes to be used by the application. Port of com.google.copybara.util.ExitCode. +/// +public enum ExitCode +{ + /// Everything went well and the migration was successful. + Success = 0, + + /// An error parsing the command line. For example wrong arguments/options. + CommandLineError = 1, + + /// + /// An error in the configuration, flags values or in general an error attributable to the user. + /// + ConfigurationError = 2, + + /// An error that happened during repository manipulation. + RepositoryError = 3, + + /// + /// Execution resulted in no-op, which means that no changes were made in the destination. + /// + NoOp = 4, + + /// Execution was interrupted. + Interrupted = 8, + + /// + /// Any error transient or permanent due to the environment (Error accessing the network, + /// filesystem errors, etc.) + /// + EnvironmentError = 30, + + /// Any error that was unexpected. This would be a Copybara bug. + InternalError = 31, +} + +/// Helpers for . +public static class ExitCodeExtensions +{ + /// Returns the integer code associated with this exit code. + public static int GetCode(this ExitCode exitCode) => (int)exitCode; + + /// Returns the for the given integer code. + /// if the code does not correspond to a known value. + public static ExitCode ForCode(int code) + { + if (Enum.IsDefined(typeof(ExitCode), code)) + { + return (ExitCode)code; + } + throw new ArgumentException("Invalid exit code: " + code); + } +} diff --git a/src/Copybara.Core/Util/GlobAtom.cs b/src/Copybara.Core/Util/GlobAtom.cs new file mode 100644 index 000000000..2367bdb35 --- /dev/null +++ b/src/Copybara.Core/Util/GlobAtom.cs @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Copybara.Common; + +namespace Copybara.Util; + +/// +/// A wrapper around a single String literal passed to the Starlark glob(...) function. Port of +/// com.google.copybara.util.GlobAtom. +/// +public sealed class GlobAtom : IEquatable +{ + /// The format that the pattern takes. + public enum AtomType + { + JavaGlob, + SingleFile, + } + + private readonly AtomType _type; + private readonly string _pattern; + + private GlobAtom(string pattern, AtomType type) + { + _pattern = Preconditions.CheckNotNull(pattern); + _type = type; + } + + public static GlobAtom Of(string pattern, AtomType type) + { + Preconditions.CheckArgument(pattern.Length != 0, "unexpected empty string in glob list"); + FileUtil.CheckNormalizedRelative(pattern); + if (type == AtomType.JavaGlob) + { + // Try to create a matcher to check that the glob pattern is correct. + var unused = GlobPathMatcher.Translate(pattern); + } + return new GlobAtom(pattern, type); + } + + public static IEnumerable OfIterable(IEnumerable patterns, AtomType type) => + patterns.Select(p => Of(p, type)); + + public IPathMatcher Matcher(string root) => MatcherFor(_type, root, _pattern); + + public string Root(bool allowFiles) => RootFor(_type, _pattern, allowFiles).GetRoot(); + + public Root AnnotatedRoot(bool allowFiles) => RootFor(_type, _pattern, allowFiles); + + public string Pattern() => _pattern; + + public AtomType GetAtomType() => _type; + + public override string ToString() => _pattern; + + /// + /// Resolves the against the path, returning a + /// normalized, /-separated string. + /// + public static string GetRelativePath(string root, string filePath) + { + string rootStr = PathNormalizer.Normalize(root); + if (rootStr.Length > 0 && !rootStr.EndsWith('/')) + { + rootStr += "/"; + } + return NormalizePath(rootStr + filePath); + } + + public bool Equals(GlobAtom? other) => + other is not null && _type == other._type && _pattern == other._pattern; + + public override bool Equals(object? obj) => Equals(obj as GlobAtom); + + public override int GetHashCode() => HashCode.Combine(_type, _pattern); + + private static IPathMatcher MatcherFor(AtomType type, string root, string pattern) + { + switch (type) + { + case AtomType.JavaGlob: + return ReadablePathMatcher.RelativeGlob(root, pattern); + case AtomType.SingleFile: + string relativePath = GetRelativePath(root, pattern); + return new SingleFilePathMatcher(relativePath, pattern); + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + + private static Root RootFor(AtomType type, string pattern, bool allowFiles) + { + switch (type) + { + case AtomType.JavaGlob: + return JavaGlobRoot(pattern, allowFiles); + case AtomType.SingleFile: + int lastSlash = pattern.LastIndexOf('/'); + return lastSlash == -1 + ? new Root(false, "") + : new Root(false, pattern.Substring(0, lastSlash)); + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + + private static Root JavaGlobRoot(string pattern, bool allowFiles) + { + string root; + bool isSingleFile = true; + bool isRecursive = pattern.Contains("**"); + var components = new List(); + string[] parts = pattern.Split('/'); + for (int idx = 0; idx < parts.Length; idx++) + { + string component = parts[idx]; + components.Add(Unescape(component)); + if (IsMeta(component)) + { + isSingleFile = false; + bool hasNext = idx < parts.Length - 1; + isRecursive = component.Contains("**") || hasNext; + break; + } + } + if (!(allowFiles && isSingleFile)) + { + if (components.Count > 0) + { + components.RemoveAt(components.Count - 1); + } + } + root = components.Count == 0 ? "" : string.Join('/', components); + return new Root(isRecursive, root); + } + + private static readonly Regex UnescapeRegex = new(@"\\(.)"); + + private static string Unescape(string pathComponent) => + UnescapeRegex.Replace(pathComponent, "$1"); + + internal static bool IsMeta(string pathComponent) + { + int c = 0; + while (c < pathComponent.Length) + { + switch (pathComponent[c]) + { + case '*': + case '{': + case '[': + case '?': + return true; + case '\\': + c++; + break; + default: + break; + } + c++; + } + return false; + } + + private static string NormalizePath(string path) + { + string p = PathNormalizer.Normalize(path); + // Collapse "." and ".." components to mirror Java's Path.normalize(). + var stack = new List(); + bool absolute = p.StartsWith('/'); + foreach (var seg in p.Split('/')) + { + if (seg.Length == 0 || seg == ".") + { + continue; + } + if (seg == "..") + { + if (stack.Count > 0 && stack[^1] != "..") + { + stack.RemoveAt(stack.Count - 1); + } + else if (!absolute) + { + stack.Add(".."); + } + } + else + { + stack.Add(seg); + } + } + string joined = string.Join('/', stack); + return absolute ? "/" + joined : joined; + } + + /// A matcher matching a single normalized file path exactly. + private sealed class SingleFilePathMatcher : IPathMatcher + { + private readonly string _relativePath; + private readonly string _toString; + + public SingleFilePathMatcher(string relativePath, string toString) + { + _relativePath = relativePath; + _toString = toString; + } + + public bool Matches(string path) => NormalizePath(path) == _relativePath; + + public override string ToString() => _toString; + } +} + +/// +/// Describes the root of a glob atom: the recursive flag plus the leading non-meta path prefix. Port +/// of com.google.copybara.util.GlobAtom.Root. +/// +public sealed class Root : IEquatable +{ + private readonly bool _isRecursive; + private readonly string _root; + + public Root(bool isRecursive, string root) + { + _isRecursive = isRecursive; + _root = root; + } + + public bool IsRecursive() => _isRecursive; + + public string GetRoot() => _root; + + public bool Equals(Root? other) => + other is not null && _root == other._root && _isRecursive == other._isRecursive; + + public override bool Equals(object? obj) => Equals(obj as Root); + + public override int GetHashCode() => HashCode.Combine(_root, _isRecursive); +} diff --git a/src/Copybara.Core/Util/GlobPathMatcher.cs b/src/Copybara.Core/Util/GlobPathMatcher.cs new file mode 100644 index 000000000..a80aec3cb --- /dev/null +++ b/src/Copybara.Core/Util/GlobPathMatcher.cs @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using System.Text.RegularExpressions; + +namespace Copybara.Util; + +/// +/// A that implements the glob: syntax of +/// java.nio.file.FileSystem.getPathMatcher. The full glob string is anchored against the +/// whole (normalized, /-separated) path. +/// +/// Supported syntax (matching the Java glob: grammar): +/// +/// * matches zero or more characters, not crossing a /. +/// ** matches zero or more characters, crossing / boundaries. +/// ? matches exactly one character that is not a /. +/// [...] a character class; supports ! or ^ negation and +/// a-z ranges. Inside a class, *, ?, { etc. are literal. +/// {a,b,c} a group matching any of the comma-separated subpatterns +/// (not nestable, as in Java). +/// \ escapes the following metacharacter. +/// +/// +public sealed class GlobPathMatcher : IPathMatcher, IEquatable +{ + private readonly Regex _regex; + private readonly string _glob; + + private GlobPathMatcher(Regex regex, string glob) + { + _regex = regex; + _glob = glob; + } + + /// Compiles a matcher from a full glob string (already including any root prefix). + public static GlobPathMatcher Compile(string glob) + { + string regex = Translate(glob); + return new GlobPathMatcher(new Regex(regex, RegexOptions.Singleline), glob); + } + + public bool Matches(string path) => _regex.IsMatch(PathNormalizer.Normalize(path)); + + public override string ToString() => _glob; + + public bool Equals(GlobPathMatcher? other) => other is not null && other._glob == _glob; + + public override bool Equals(object? obj) => Equals(obj as GlobPathMatcher); + + public override int GetHashCode() => _glob.GetHashCode(); + + private const string RegexMeta = ".^$+{[]|()"; + + /// Translates a Java glob: pattern into an anchored .NET regular expression. + internal static string Translate(string glob) + { + var sb = new StringBuilder(); + sb.Append('^'); + int i = 0; + int n = glob.Length; + bool inGroup = false; + while (i < n) + { + char c = glob[i++]; + switch (c) + { + case '\\': + if (i >= n) + { + throw new ArgumentException( + "No character to escape at end of pattern: " + glob); + } + char next = glob[i++]; + AppendLiteral(sb, next); + break; + case '/': + sb.Append('/'); + break; + case '[': + i = TranslateBracket(glob, i, sb); + break; + case '{': + if (inGroup) + { + throw new ArgumentException("Cannot nest groups in pattern: " + glob); + } + inGroup = true; + sb.Append("(?:(?:"); + break; + case '}': + if (inGroup) + { + inGroup = false; + sb.Append("))"); + } + else + { + sb.Append('}'); + } + break; + case ',': + if (inGroup) + { + sb.Append(")|(?:"); + } + else + { + sb.Append(','); + } + break; + case '*': + if (i < n && glob[i] == '*') + { + // ** crosses directory boundaries. + sb.Append(".*"); + i++; + } + else + { + // * matches anything but the separator. + sb.Append("[^/]*"); + } + break; + case '?': + sb.Append("[^/]"); + break; + default: + AppendLiteral(sb, c); + break; + } + } + if (inGroup) + { + throw new ArgumentException("Missing '}' in pattern: " + glob); + } + sb.Append('$'); + return sb.ToString(); + } + + private static int TranslateBracket(string glob, int i, StringBuilder sb) + { + int n = glob.Length; + sb.Append('['); + if (i < n && (glob[i] == '!' || glob[i] == '^')) + { + sb.Append('^'); + i++; + } + // A ']' immediately following the (possibly negated) '[' is a literal ']'. + if (i < n && glob[i] == ']') + { + sb.Append("\\]"); + i++; + } + bool closed = false; + while (i < n) + { + char c = glob[i++]; + if (c == ']') + { + closed = true; + break; + } + if (c == '\\') + { + if (i >= n) + { + throw new ArgumentException("No character to escape in class: " + glob); + } + AppendClassLiteral(sb, glob[i++]); + } + else if (c == '/') + { + // Java glob classes cannot contain the separator. + throw new ArgumentException("Explicit 'name separator' in class: " + glob); + } + else if (c == '-') + { + // Preserve ranges as-is. + sb.Append('-'); + } + else + { + AppendClassLiteral(sb, c); + } + } + if (!closed) + { + throw new ArgumentException("Missing ']' in pattern: " + glob); + } + sb.Append(']'); + return i; + } + + private static void AppendLiteral(StringBuilder sb, char c) + { + if (RegexMeta.IndexOf(c) >= 0 || c == '*' || c == '?' || c == '\\' || c == '}') + { + sb.Append('\\'); + } + sb.Append(c); + } + + private static void AppendClassLiteral(StringBuilder sb, char c) + { + if (c == '\\' || c == ']' || c == '^' || c == '[') + { + sb.Append('\\'); + } + sb.Append(c); + } +} + +/// Normalizes a filesystem path for glob matching: backslashes become /. +internal static class PathNormalizer +{ + public static string Normalize(string path) => path.Replace('\\', '/'); +} diff --git a/src/Copybara.Core/Util/IPathMatcher.cs b/src/Copybara.Core/Util/IPathMatcher.cs new file mode 100644 index 000000000..545aea2a5 --- /dev/null +++ b/src/Copybara.Core/Util/IPathMatcher.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util; + +/// +/// Matches paths. The .NET-port equivalent of java.nio.file.PathMatcher. Paths are plain +/// strings and use / as the separator (backslashes are normalized to / before +/// matching), so the matching semantics are platform-independent as required by Copybara. +/// +public interface IPathMatcher +{ + /// Returns true if the given path matches this matcher. + bool Matches(string path); +} diff --git a/src/Copybara.Core/Util/Identity.cs b/src/Copybara.Core/Util/Identity.cs new file mode 100644 index 000000000..4bbe8e741 --- /dev/null +++ b/src/Copybara.Core/Util/Identity.cs @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Copybara.Util; + +/// +/// Utility methods for Identity, which computes an identity based on workflowName, contextReference, +/// configPath, and workflowIdentityUser and allows us to reuse the destination changes. Port of +/// com.google.copybara.util.Identity. +/// +public static class Identity +{ + private static readonly ILogger Logger = NullLogger.Instance; + + public static string ComputeIdentity( + string type, + string @ref, + string workflowName, + string configPath, + string? workflowIdentityUser) + { + // Mirrors Guava's MoreObjects.toStringHelper(type) output format: + // {type=workflow, config_path=, workflow_name=, context_ref=} + var helper = new ToStringHelper(type) + .Add("type", "workflow") + .Add("config_path", configPath) + .Add("workflow_name", workflowName) + .Add("context_ref", @ref); + return HashIdentity(helper, workflowIdentityUser); + } + + public static string HashIdentity(ToStringHelper helper, string? workflowIdentityUser) + { + helper.Add( + "user", + workflowIdentityUser ?? Environment.UserName); + string identity = helper.ToString(); + byte[] digest = MD5.HashData(Encoding.UTF8.GetBytes(identity)); + // Guava BaseEncoding.base16() produces uppercase hex. + string hash = Convert.ToHexString(digest); + // Important to log the source of the hash and the hash for debugging purposes. + Logger.LogInformation( + "Computed migration identity hash for {Identity} as {Hash} ", identity, hash); + return hash; + } +} + +/// +/// A minimal port of Guava's MoreObjects.ToStringHelper producing the same textual layout +/// used to derive a stable identity hash. +/// +public sealed class ToStringHelper +{ + private readonly string _className; + private readonly List<(string Name, string Value)> _entries = new(); + + public ToStringHelper(string className) => _className = className; + + public ToStringHelper Add(string name, string? value) + { + _entries.Add((name, value ?? "null")); + return this; + } + + public override string ToString() + { + var sb = new StringBuilder(_className).Append('{'); + for (int i = 0; i < _entries.Count; i++) + { + if (i > 0) + { + sb.Append(", "); + } + sb.Append(_entries[i].Name).Append('=').Append(_entries[i].Value); + } + return sb.Append('}').ToString(); + } +} diff --git a/src/Copybara.Core/Util/InsideGitDirException.cs b/src/Copybara.Core/Util/InsideGitDirException.cs new file mode 100644 index 000000000..dba2b9fca --- /dev/null +++ b/src/Copybara.Core/Util/InsideGitDirException.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util; + +/// +/// Thrown when trying to run git diff/apply inside a git directory for directories that shouldn't +/// be inside a git dir. Port of com.google.copybara.util.InsideGitDirException. +/// +public class InsideGitDirException : Exception +{ + public InsideGitDirException(string message, string gitDirPath, string path) : base(message) + { + GitDirPath = gitDirPath; + Path = path; + } + + /// The git directory that contains . + public string GitDirPath { get; } + + /// The offending path that is inside a git directory. + public string Path { get; } +} diff --git a/src/Copybara.Core/Util/LimitFilterOutputStream.cs b/src/Copybara.Core/Util/LimitFilterOutputStream.cs new file mode 100644 index 000000000..4d424fd09 --- /dev/null +++ b/src/Copybara.Core/Util/LimitFilterOutputStream.cs @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Util; + +/// +/// A that limits the output to a certain number of bytes. After that, it skips +/// writing to the delegated output. As an optional last step, it can write an +/// afterLimitSuffix byte sequence (generally text) to express that it has reached the stream +/// limit (e.g. "(Skipped the rest of the output)"). Port of +/// com.google.copybara.util.LimitFilterOutputStream (a FilterOutputStream in Java). +/// +public class LimitFilterOutputStream : Stream +{ + private readonly Stream _out; + private int _left; + private bool _suffixWritten; + private readonly byte[] _afterLimitSuffix; + + /// Construct a limited output stream. + /// delegate output stream to wrap. + /// + /// number of bytes to write before skipping any further write. Required to be > 0. + /// + /// + /// an optional suffix (generally a string) to write after reaching the limit. + /// + public LimitFilterOutputStream(Stream output, int byteLimit, byte[] afterLimitSuffix) + { + _out = output; + Preconditions.CheckArgument(byteLimit > 0, "byteLimit is expected to be greater than zero."); + _left = byteLimit; + _afterLimitSuffix = afterLimitSuffix; + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public void Write(int b) + { + if (_left > 0) + { + _out.WriteByte((byte)b); + _left--; + } + else + { + MaybeWriteLimitSuffix(); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (count <= 0) + { + return; + } + + if (_left == 0) + { + MaybeWriteLimitSuffix(); + return; + } + + if (count > _left) + { + int oldLeft = _left; + _left = 0; + _out.Write(buffer, offset, oldLeft); + MaybeWriteLimitSuffix(); + } + else + { + _left -= count; + _out.Write(buffer, offset, count); + } + } + + public override void WriteByte(byte value) => Write(value); + + /// Write at most once the suffix to the stream. + private void MaybeWriteLimitSuffix() + { + if (_afterLimitSuffix.Length > 0 && !_suffixWritten) + { + _out.Write(_afterLimitSuffix, 0, _afterLimitSuffix.Length); + _suffixWritten = true; + } + } + + public override void Flush() => _out.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); +} diff --git a/src/Copybara.Core/Util/ReadablePathMatcher.cs b/src/Copybara.Core/Util/ReadablePathMatcher.cs new file mode 100644 index 000000000..174ae727c --- /dev/null +++ b/src/Copybara.Core/Util/ReadablePathMatcher.cs @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util; + +/// +/// A path matcher which delegates to another path matcher but has a specifiable +/// value. Port of com.google.copybara.util.ReadablePathMatcher. +/// +public sealed class ReadablePathMatcher : IPathMatcher, IEquatable +{ + private readonly IPathMatcher _delegate; + private readonly string _toString; + + public ReadablePathMatcher(IPathMatcher @delegate, string toString) + { + _delegate = @delegate; + _toString = toString; + } + + public bool Matches(string path) => _delegate.Matches(path); + + public override string ToString() => _toString; + + /// + /// Creates a based on a glob relative to . The + /// string representation of the matcher is the actual glob. + /// + /// For example a glob "dir/**.java" would match any java file inside the {path}/dir + /// directory. + /// + public static ReadablePathMatcher RelativeGlob(string path, string glob) + { + FileUtil.CheckNormalizedRelative(glob); + + string root = PathNormalizer.Normalize(path); + if (root.Length > 0 && !root.EndsWith('/')) + { + root += "/"; + } + + return new ReadablePathMatcher(GlobPathMatcher.Compile(root + glob), glob); + } + + public bool Equals(ReadablePathMatcher? other) => + // Don't use the delegate as toString is unique. + other is not null && _toString == other._toString; + + public override bool Equals(object? obj) => Equals(obj as ReadablePathMatcher); + + public override int GetHashCode() => _toString.GetHashCode(); +} diff --git a/src/Copybara.Core/Util/ShellUtils.cs b/src/Copybara.Core/Util/ShellUtils.cs new file mode 100644 index 000000000..8d98b1534 --- /dev/null +++ b/src/Copybara.Core/Util/ShellUtils.cs @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; + +namespace Copybara.Util; + +/// +/// Utility functions for Bourne shell commands, including escaping. Port of the subset of Bazel's +/// ShellUtils used by Copybara's . +/// +public static class ShellUtils +{ + private const string SafePunctuation = "@%-_+:,./"; + + /// Escapes a word so it can be safely used as a single Bourne shell argument. + public static string ShellEscape(string word) + { + int len = word.Length; + if (len == 0) + { + // Empty string is a special case: needs to be quoted to ensure it becomes a separate + // argument. + return "''"; + } + for (int ii = 0; ii < len; ii++) + { + char c = word[ii]; + if (!char.IsLetterOrDigit(c) && SafePunctuation.IndexOf(c) == -1) + { + return "'" + word.Replace("'", "'\\''") + "'"; + } + } + return word; + } + + /// + /// Given an argv array such as might be passed to execve(2), returns a string that can be copied + /// and pasted into a Bourne shell for a similar effect. + /// + public static string PrettyPrintArgv(IEnumerable argv) + { + var buf = new StringBuilder(); + foreach (var arg in argv) + { + if (buf.Length > 0) + { + buf.Append(' '); + } + buf.Append(ShellEscape(arg)); + } + return buf.ToString(); + } +} diff --git a/src/Copybara.Core/Util/SymlinkException.cs b/src/Copybara.Core/Util/SymlinkException.cs new file mode 100644 index 000000000..266566f31 --- /dev/null +++ b/src/Copybara.Core/Util/SymlinkException.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util; + +/// +/// Base exception for symlink-related errors. Port of +/// com.google.copybara.util.SymlinkException (an IOException in Java). +/// +public class SymlinkException : IOException +{ + public SymlinkException(string message) : base(message) + { + } +} diff --git a/src/Copybara.Core/Util/TablePrinter.cs b/src/Copybara.Core/Util/TablePrinter.cs new file mode 100644 index 000000000..d8fba5200 --- /dev/null +++ b/src/Copybara.Core/Util/TablePrinter.cs @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; + +namespace Copybara.Util; + +/// +/// Utility for printing tabular data to an ASCII string. Currently only supports single-line cells. +/// Port of com.google.copybara.util.TablePrinter. +/// +public class TablePrinter +{ + private readonly ImmutableArray _headers; + private readonly List> _rows = new(); + private readonly int[] _columnWidths; + + public TablePrinter(params string[] header) + { + _headers = header.ToImmutableArray(); + _columnWidths = new int[header.Length]; + for (int col = 0; col < header.Length; col++) + { + _columnWidths[col] = header[col].Length; + } + } + + /// Add a row, which must have the same number of elements as the header. + public TablePrinter AddRow(params object?[] row) + { + if (row.Length != _headers.Length) + { + throw new ArgumentException(string.Format( + "Wrong number of values in row; expected {0}. Got: {1}", _headers.Length, row.Length)); + } + // null friendly, no line breaks. + var strings = row + .Select(o => ("" + o).Replace("\n", "")) + .ToImmutableArray(); + _rows.Add(strings); + for (int col = 0; col < strings.Length; col++) + { + _columnWidths[col] = Math.Max(strings[col].Length, _columnWidths[col]); + } + return this; + } + + /// Build the table. + public IReadOnlyList Build() + { + var lines = new List + { + PrintRow('+', '-', ImmutableArray.Empty), + PrintRow('|', ' ', _headers), + PrintRow('+', '-', ImmutableArray.Empty), + }; + foreach (var row in _rows) + { + lines.Add(PrintRow('|', ' ', row)); + } + lines.Add(PrintRow('+', '-', ImmutableArray.Empty)); + return lines; + } + + /// Build the table into a single newline-joined string. + public string Print() => string.Join('\n', Build()); + + private string PrintRow(char delim, char filler, IReadOnlyList vals) + { + var paddedVals = new List(); + for (int col = 0; col < _columnWidths.Length; col++) + { + string val = vals.Count > col ? vals[col] : ""; + paddedVals.Add(PadEnd(val, _columnWidths[col] + 1, filler)); + } + return delim + string.Join(delim, paddedVals) + delim; + } + + private static string PadEnd(string s, int minLength, char padChar) => + s.Length >= minLength ? s : s + new string(padChar, minLength - s.Length); +} diff --git a/src/Copybara.Core/Util/TerminationStatus.cs b/src/Copybara.Core/Util/TerminationStatus.cs new file mode 100644 index 000000000..4534dc164 --- /dev/null +++ b/src/Copybara.Core/Util/TerminationStatus.cs @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Util; + +/// +/// Represents the termination status of a subprocess. Port of the Bazel shell library's +/// TerminationStatus. Follows the Unix convention where a value of 128+signal indicates the +/// process was terminated by a signal. +/// +public sealed class TerminationStatus : IEquatable +{ + // We rely on the convention that the raw wait result is the exit status when the process exits + // normally, or 128+signalnumber when the process is terminated by a signal (signals in [1, 63]). + private const int Signal1 = 128 + 1; + private const int Signal63 = 128 + 63; + + private static readonly string[] SignalStrings = + { + "Signal 0", + "Hangup", + "Interrupt", + "Quit", + "Illegal instruction", + "Trace/breakpoint trap", + "Aborted", + "Bus error", + "Floating point exception", + "Killed", + "User defined signal 1", + "Segmentation fault", + "User defined signal 2", + "Broken pipe", + "Alarm clock", + "Terminated", + }; + + private readonly int _waitResult; + + public TerminationStatus(int waitResult) => _waitResult = waitResult; + + private static string GetSignalString(int signum) => + signum > 0 && signum < SignalStrings.Length + ? SignalStrings[signum] + : "Signal " + signum; + + /// + /// Returns the "raw" wait result. This value is not precisely defined; use + /// instead where possible. + /// + internal int GetRawResult() => _waitResult; + + /// Returns true iff the process exited with code 0. + public bool Success() => Exited() && GetExitCode() == 0; + + /// Returns true iff the process exited normally. + public bool Exited() => _waitResult < Signal1 || _waitResult > Signal63; + + /// Returns the exit code of the subprocess. Undefined if is false. + public int GetExitCode() + { + if (!Exited()) + { + throw new InvalidOperationException("GetExitCode() not defined"); + } + return _waitResult; + } + + /// + /// Returns the number of the signal that terminated the process. Undefined if + /// returns true. + /// + public int GetTerminatingSignal() + { + if (Exited()) + { + throw new InvalidOperationException("GetTerminatingSignal() not defined"); + } + return _waitResult - Signal1 + 1; + } + + /// A short string describing the termination status, e.g. "Exit 1" or "Hangup". + public string ToShortString() => + Exited() ? "Exit " + GetExitCode() : GetSignalString(GetTerminatingSignal()); + + public override string ToString() => + Exited() + ? "Process exited with status " + GetExitCode() + : "Process terminated by signal " + GetTerminatingSignal(); + + public override int GetHashCode() => _waitResult; + + public bool Equals(TerminationStatus? other) => other is not null && other._waitResult == _waitResult; + + public override bool Equals(object? obj) => Equals(obj as TerminationStatus); +} diff --git a/src/Starlark/Eval/Mutability.cs b/src/Starlark/Eval/Mutability.cs new file mode 100644 index 000000000..2f3552709 --- /dev/null +++ b/src/Starlark/Eval/Mutability.cs @@ -0,0 +1,139 @@ +// Copyright 2015 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Runtime.CompilerServices; + +namespace Starlark.Eval; + +/// +/// An object that manages the capability to mutate Starlark objects and their +/// s. Port of net.starlark.java.eval.Mutability. +/// +/// Once a thread is done evaluating, its Mutability is irreversibly closed ("frozen"). At that +/// point it is no longer possible to change the state of its objects. +/// +public sealed class Mutability : IDisposable +{ + // Maps each temporarily frozen Freezable value to the (positive) count of active iterators over + // the value. Null once permanently frozen. + private Dictionary? iteratorCount = new(ReferenceEqualityComparer.Instance); + + private readonly object?[] annotation; + + private readonly bool allowsUnsafeShallowFreeze; + + private Mutability(object?[] annotation, bool allowsUnsafeShallowFreeze) + { + this.annotation = annotation; + this.allowsUnsafeShallowFreeze = allowsUnsafeShallowFreeze; + } + + /// Creates a Mutability. + public static Mutability Create(params object?[] annotation) => new(annotation, false); + + /// Creates a Mutability whose objects can be individually frozen. + public static Mutability CreateAllowingShallowFreeze(params object?[] annotation) => + new(annotation, true); + + /// Returns the Mutability's "annotation", an internal name describing its purpose. + public string GetAnnotation() => string.Join(" ", annotation.Select(a => a?.ToString() ?? "null")); + + public override string ToString() => + (IsFrozen ? "(" : "[") + GetAnnotation() + (IsFrozen ? ")" : "]"); + + public bool IsFrozen => iteratorCount == null; + + // Defines the default behavior of mutable Freezable sequence values. + internal bool UpdateIteratorCount(IFreezable x, int delta) + { + if (IsFrozen) + { + return false; + } + iteratorCount!.TryGetValue(x, out int i); + if (delta > 0) + { + i++; + iteratorCount[x] = i; + } + else if (delta < 0) + { + i--; + if (i == 0) + { + iteratorCount.Remove(x); + } + else if (i > 0) + { + iteratorCount[x] = i; + } + else + { + throw new InvalidOperationException("zero value in iteratorCount"); + } + } + return i > 0; + } + + /// Freezes this Mutability, rendering all Freezables that refer to it immutable. + public Mutability Freeze() + { + iteratorCount = null; + return this; + } + + public void Dispose() => Freeze(); + + /// Whether Freezables having this Mutability allow the unsafe shallow freeze operation. + public bool AllowsUnsafeShallowFreeze => allowsUnsafeShallowFreeze; + + /// Throws if the precondition for unsafeShallowFreeze is violated. + public static void CheckUnsafeShallowFreezePrecondition(IFreezable freezable) + { + Mutability mutability = freezable.Mutability; + if (mutability.IsFrozen) + { + throw new ArgumentException( + "cannot call UnsafeShallowFreeze() on an object whose Mutability is already frozen"); + } + if (!mutability.AllowsUnsafeShallowFreeze) + { + throw new ArgumentException( + "cannot call UnsafeShallowFreeze() on a mutable object whose Mutability's " + + "AllowsUnsafeShallowFreeze == false"); + } + } + + /// A Mutability indicating that a value is deeply immutable. + public static readonly Mutability IMMUTABLE = Create("IMMUTABLE").Freeze(); +} + +/// +/// An object that refers to a to decide whether to allow mutation. Port of +/// net.starlark.java.eval.Mutability.Freezable. +/// +public interface IFreezable +{ + /// Returns the Mutability associated with this Freezable. + Mutability Mutability { get; } + + /// + /// Registers a change to this Freezable's iterator count and reports whether it is temporarily + /// immutable. + /// + bool UpdateIteratorCount(int delta) => Mutability.UpdateIteratorCount(this, delta); + + /// Freezes this object (and not its contents). Use with care. Optional operation. + void UnsafeShallowFreeze() => throw new NotSupportedException(); +} diff --git a/src/Starlark/Eval/Printer.cs b/src/Starlark/Eval/Printer.cs new file mode 100644 index 000000000..78a34e384 --- /dev/null +++ b/src/Starlark/Eval/Printer.cs @@ -0,0 +1,382 @@ +// Copyright 2015 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; +using System.Globalization; +using System.Text; + +namespace Starlark.Eval; + +/// +/// A printer of Starlark values. Port of net.starlark.java.eval.Printer. +/// +public class Printer +{ + private readonly StringBuilder buffer; + + // Stack of values in the middle of being printed (for cycle detection). + private object?[]? stack; + private int depth; + + public Printer(StringBuilder buffer) => this.buffer = buffer; + + public Printer() : this(new StringBuilder()) { } + + public Printer Append(char c) + { + buffer.Append(c); + return this; + } + + public Printer Append(string s) + { + buffer.Append(s); + return this; + } + + public Printer Append(string s, int start, int end) + { + buffer.Append(s, start, end - start); + return this; + } + + public Printer Append(int i) + { + buffer.Append(i.ToString(CultureInfo.InvariantCulture)); + return this; + } + + public Printer Append(long l) + { + buffer.Append(l.ToString(CultureInfo.InvariantCulture)); + return this; + } + + /// Appends a list to the buffer, each element rendered with repr. + public virtual Printer PrintList( + IEnumerable list, string before, string separator, string after, StarlarkSemantics semantics) + { + Append(before); + string sep = ""; + foreach (object? elem in list) + { + Append(sep); + sep = separator; + Repr(elem, semantics); + } + return Append(after); + } + + public override string ToString() => buffer.ToString(); + + /// Appends the debug representation of a value. + public Printer DebugPrint(object? o, StarlarkThread thread) + { + if (o is IStarlarkPrintableValue v) + { + v.DebugPrint(this, thread); + return this; + } + return Str(o, thread.GetSemantics()); + } + + /// Appends the str representation of a value (strings unquoted at top level). + public Printer Str(object? o, StarlarkSemantics semantics) + { + switch (o) + { + case string s: + return Append(s); + case IStarlarkPrintableValue v: + v.Str(this, semantics); + return this; + default: + return Repr(o, semantics); + } + } + + /// Appends the repr (quoted) representation of a value. + public Printer Repr(object? o, StarlarkSemantics semantics) + { + // atomic values (leaves of the object graph) + switch (o) + { + case null: + return Append("null"); + case string s: + return AppendQuoted(s); + case StarlarkInt starlarkInt: + starlarkInt.Repr(this, semantics); + return this; + case bool b: + return Append(b ? "True" : "False"); + case int i: + return Append(i); // a non-Starlark value + } + + // compound values (may form cycles) + if (!Push(o)) + { + return Append("..."); // elided cycle + } + try + { + switch (o) + { + case IStarlarkPrintableValue value: + value.Repr(this, semantics); + break; + case IDictionary map: + PrintList(map, "{", ", ", "}", semantics); + break; + case DictionaryEntry entry: + Repr(entry.Key, semantics).Append(": ").Repr(entry.Value, semantics); + break; + case IEnumerable list: + PrintList(list, "[", ", ", "]", semantics); + break; + default: + Append(o.ToString() ?? "null"); + break; + } + } + finally + { + Pop(); + } + return this; + } + + private Printer AppendQuoted(string s) + { + Append('"'); + foreach (char c in s) + { + EscapeCharacter(c); + } + return Append('"'); + } + + private Printer BackslashChar(char c) => Append('\\').Append(c); + + private Printer EscapeCharacter(char c) + { + if (c == '"') + { + return BackslashChar(c); + } + switch (c) + { + case '\\': + return BackslashChar('\\'); + case '\r': + return BackslashChar('r'); + case '\n': + return BackslashChar('n'); + case '\t': + return BackslashChar('t'); + default: + if (c < 32) + { + return Append("\\x" + ((int)c).ToString("x2", CultureInfo.InvariantCulture)); + } + return Append(c); + } + } + + private bool Push(object x) + { + for (int i = 0; i < depth; i++) + { + if (ReferenceEquals(x, stack![i])) + { + return false; + } + } + if (stack == null) + { + stack = new object?[4]; + } + else if (depth == stack.Length) + { + Array.Resize(ref stack, 2 * stack.Length); + } + stack[depth++] = x; + return true; + } + + private void Pop() => stack![--depth] = null; + + /// Appends a string, formatted as if by Starlark's str % tuple operator. + public static void Format( + Printer printer, StarlarkSemantics semantics, string format, params object?[] arguments) => + FormatWithList(printer, semantics, format, arguments); + + /// Same as Format, but with a list instead of variadic args. + public static void FormatWithList( + Printer printer, StarlarkSemantics semantics, string pattern, IReadOnlyList arguments) + { + int length = pattern.Length; + int argLength = arguments.Count; + int i = 0; // index of next character in pattern + int a = 0; // index of next argument + + while (i < length) + { + int p = pattern.IndexOf('%', i); + if (p == -1) + { + printer.Append(pattern, i, length); + break; + } + if (p > i) + { + printer.Append(pattern, i, p); + } + if (p == length - 1) + { + throw new FormatException( + "incomplete format pattern ends with %: " + Starlark.Repr(pattern, semantics)); + } + char conv = pattern[p + 1]; + i = p + 2; + + if (conv == '%') + { + printer.Append('%'); + continue; + } + + if (a >= argLength) + { + throw new FormatException( + "not enough arguments for format pattern " + + Starlark.Repr(pattern, semantics) + + ": " + + Starlark.Repr(Tuple.CopyOf(arguments), semantics)); + } + object? arg = arguments[a++]; + + switch (conv) + { + case 'd': + case 'o': + case 'x': + case 'X': + { + System.Numerics.BigInteger n = ToBigIntArg(arg, conv); + printer.Append(conv switch + { + 'd' => n.ToString(CultureInfo.InvariantCulture), + 'o' => ToOctal(n), + 'x' => ToHex(n, false), + _ => ToHex(n, true), + }); + break; + } + + case 'e': + case 'f': + case 'g': + case 'E': + case 'F': + case 'G': + { + double v = arg switch + { + int integer => integer, + StarlarkInt si => si.ToDouble(), + StarlarkFloat sf => sf.ToDouble(), + _ => throw new FormatException(string.Format( + "got {0} for '%{1}' format, want int or float", + Starlark.Type(arg), conv)), + }; + printer.Append(StarlarkFloat.Format(v, conv)); + break; + } + + case 'r': + printer.Repr(arg, semantics); + break; + + case 's': + printer.Str(arg, semantics); + break; + + default: + throw new FormatException(string.Format( + "unsupported format character \"{0}\" at index {1} in {2}", + conv, p + 1, Starlark.Repr(pattern, semantics))); + } + } + if (a < argLength) + { + throw new FormatException("not all arguments converted during string formatting"); + } + } + + private static System.Numerics.BigInteger ToBigIntArg(object? arg, char conv) + { + switch (arg) + { + case StarlarkInt si: + return si.ToBigInteger(); + case int integer: + return integer; + case StarlarkFloat sf: + try + { + return StarlarkFloat.FiniteDoubleToIntExact(sf.ToDouble()).ToBigInteger(); + } + catch (ArgumentException) + { + throw new FormatException("got " + arg + ", want a finite number"); + } + default: + throw new FormatException(string.Format( + "got {0} for '%{1}' format, want int or float", Starlark.Type(arg), conv)); + } + } + + private static string ToOctal(System.Numerics.BigInteger n) + { + if (n.Sign < 0) + { + return "-" + ToOctal(-n); + } + if (n.IsZero) + { + return "0"; + } + var sb = new StringBuilder(); + while (n > 0) + { + sb.Insert(0, (char)('0' + (int)(n % 8))); + n /= 8; + } + return sb.ToString(); + } + + private static string ToHex(System.Numerics.BigInteger n, bool upper) + { + if (n.Sign < 0) + { + return "-" + ToHex(-n, upper); + } + string s = n.ToString(upper ? "X" : "x", CultureInfo.InvariantCulture); + // BigInteger may prepend a leading 0 to keep sign positive; trim it. + s = s.TrimStart('0'); + return s.Length == 0 ? "0" : s; + } +} diff --git a/src/Starlark/Eval/RangeList.cs b/src/Starlark/Eval/RangeList.cs new file mode 100644 index 000000000..ae81c5528 --- /dev/null +++ b/src/Starlark/Eval/RangeList.cs @@ -0,0 +1,177 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; +using System.Globalization; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// A lazily-computed sequence returned by the range function. Port of +/// net.starlark.java.eval.RangeList. Also used internally to enumerate slice indices. +/// +[StarlarkBuiltin("range", Category = "core", Doc = "A language built-in type to support ranges.")] +public sealed class RangeList : ISequence +{ + private readonly int start; + private readonly int stop; + private readonly int step; + private readonly int size; + + public RangeList(int start, int stop, int step) + { + if (step == 0) + { + throw new ArgumentException("step must be non-zero"); + } + this.start = start; + this.stop = stop; + this.step = step; + + int low; + int high; + long absStep; + if (step > 0) + { + low = start; + high = stop; + absStep = step; + } + else + { + low = stop; + high = start; + absStep = -(long)step; + } + if (low >= high) + { + size = 0; + } + else + { + long diff = (long)high - low - 1; + long sz = diff / absStep + 1; + if ((int)sz != sz) + { + throw Starlark.Errorf( + "len({0}) exceeds signed 32-bit range", Starlark.Repr(this, StarlarkSemantics.DEFAULT)); + } + size = (int)sz; + } + } + + public int Count => size; + + public bool Contains(object? x) + { + if (x is not StarlarkInt si) + { + return false; + } + try + { + int i = si.ToIntUnchecked(); + if (step > 0) + { + return start <= i && i < stop && (i - start) % step == 0; + } + return stop < i && i <= start && (i - start) % step == 0; + } + catch (ArgumentException) + { + return false; + } + } + + public StarlarkInt this[int index] + { + get + { + if (index < 0 || index >= size) + { + throw new IndexOutOfRangeException(index + ":" + this); + } + return StarlarkInt.Of(At(index)); + } + } + + public override int GetHashCode() => size switch + { + 0 => 234982346, + 1 => start.GetHashCode(), + _ => HashCode.Combine(start, size, step), + }; + + public override bool Equals(object? other) + { + if (other is not RangeList that) + { + return false; + } + if (size != that.size) + { + return false; + } + if (size == 0) + { + return true; + } + if (start != that.start) + { + return false; + } + return size == 1 || step == that.step; + } + + public IEnumerator GetEnumerator() + { + long cursor = start; + while (step > 0 ? cursor < stop : cursor > stop) + { + yield return StarlarkInt.Of((int)cursor); + cursor += step; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public ISequence GetSlice(Mutability? mu, int sstart, int sstop, int sstep) + { + long sliceStep = (long)sstep * step; + if (sliceStep != (int)sliceStep) + { + sliceStep = sliceStep > 0 ? int.MaxValue : int.MinValue; + if (sstop > sstart) + { + sstop = sstart + 1; + } + else if (sstop < sstart) + { + sstop = sstart - 1; + } + } + return new RangeList(At(sstart), At(sstop), (int)sliceStep); + } + + // Like the indexer, but without bounds check. + internal int At(int i) => start + step * i; + + public void Repr(Printer printer, StarlarkSemantics semantics) + { + printer.Append(step == 1 + ? string.Format(CultureInfo.InvariantCulture, "range({0}, {1})", start, stop) + : string.Format(CultureInfo.InvariantCulture, "range({0}, {1}, {2})", start, stop, step)); + } +} diff --git a/src/Starlark/Eval/Sequence.cs b/src/Starlark/Eval/Sequence.cs new file mode 100644 index 000000000..348d04793 --- /dev/null +++ b/src/Starlark/Eval/Sequence.cs @@ -0,0 +1,132 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; + +namespace Starlark.Eval; + +/// +/// A Sequence is a finite iterable sequence of Starlark values, such as a list or tuple. Port of +/// net.starlark.java.eval.Sequence. +/// +/// In the .NET port this exposes the read-only list surface via +/// (rather than Java's full List), plus indexing/membership hooks. +/// +public interface ISequence : IStarlarkPrintableValue, IReadOnlyList, IStarlarkIndexable, + IStarlarkIterable +{ + bool IStarlarkValue.Truth() => Count != 0; + + /// Retrieves an entry from a Sequence. + object? IStarlarkIndexable.GetIndex(StarlarkSemantics semantics, object key) + { + int index = Starlark.ToInt(key, "sequence index"); + return this[EvalUtils.GetSequenceIndex(index, Count)]; + } + + bool IStarlarkMembershipTestable.ContainsKey(StarlarkSemantics semantics, object key) => + SequenceContains(this, key); + + /// + /// Returns the slice this[start:stop:step]. The caller must ensure the indices are valid + /// and step is non-zero. + /// + ISequence GetSlice(Mutability? mu, int start, int stop, int step); + + private static bool SequenceContains(IEnumerable seq, object key) + { + foreach (object? e in seq) + { + if (Equals(e, key)) + { + return true; + } + } + return false; + } +} + +/// Static helpers for sequences. Port of the static members of Java's Sequence. +public static class Sequence +{ + /// + /// Compares two sequences elementwise. If one is a prefix of another, compares by size. + /// + public static int Compare(IReadOnlyList x, IReadOnlyList y) + { + int n = Math.Min(x.Count, y.Count); + for (int i = 0; i < n; i++) + { + object? xelem = x[i]; + object? yelem = y[i]; + if (ReferenceEquals(xelem, yelem) || Equals(xelem, yelem)) + { + continue; + } + int cmp = Starlark.CompareUnchecked(xelem, yelem); + if (cmp == 0) + { + throw new InvalidOperationException(string.Format( + "x.equals(y) yet x.compareTo(y)==0 (x: {0}, y: {1})", + Starlark.Type(xelem), Starlark.Type(yelem))); + } + return cmp; + } + return x.Count.CompareTo(y.Count); + } + + /// Compares two sequences for equality (same size and elementwise equal). + public static bool SameElems(IReadOnlyList x, IReadOnlyList y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + if (x.Count != y.Count) + { + return false; + } + for (int i = 0; i < x.Count; i++) + { + if (!ReferenceEquals(x[i], y[i]) && !Equals(x[i], y[i])) + { + return false; + } + } + return true; + } + + /// + /// Casts a non-null Starlark value to a sequence, checking each element is of the given type. + /// + public static ISequence Cast(object x, string what) + { + if (x is not ISequence seq) + { + throw Starlark.Errorf("for {0}, got {1}, want sequence", what, Starlark.Type(x)); + } + int i = 0; + foreach (object? elem in seq) + { + if (elem is not T) + { + throw Starlark.Errorf( + "at index {0} of {1}, got element of type {2}, want {3}", + i, what, Starlark.Type(elem), typeof(T).Name); + } + i++; + } + return (ISequence)x; + } +} diff --git a/src/Starlark/Eval/StarlarkFloat.cs b/src/Starlark/Eval/StarlarkFloat.cs new file mode 100644 index 000000000..09a68442a --- /dev/null +++ b/src/Starlark/Eval/StarlarkFloat.cs @@ -0,0 +1,300 @@ +// Copyright 2020 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Globalization; +using System.Numerics; +using System.Text; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// The Starlark float data type. Port of net.starlark.java.eval.StarlarkFloat. +[StarlarkBuiltin("float", Category = "core", Doc = "The type of floating-point numbers in Starlark.")] +public sealed class StarlarkFloat : IStarlarkPrintableValue, IComparable +{ + private readonly double v; + + private StarlarkFloat(double v) => this.v = v; + + /// Returns the Starlark float value that represents x. + public static StarlarkFloat Of(double v) => new(v); + + /// Returns the value of this float. + public double ToDouble() => v; + + public override string ToString() => Format(v, 'g'); + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public bool IsImmutable() => true; + + public bool Truth() => v != 0.0; + + /// + /// Total order over float values. +0 and -0 compare equal. NaN compares equal to itself and + /// greater than +Inf. + /// + public int CompareTo(StarlarkFloat? that) + { + double x = v; + double y = that!.v; + if (x > y) + { + return +1; + } + if (x < y) + { + return -1; + } + if (x == y) + { + return 0; // 0.0 == -0.0 + } + // At least one operand is NaN. + long xbits = BitConverter.DoubleToInt64Bits(x); + long ybits = BitConverter.DoubleToInt64Bits(y); + return xbits.CompareTo(ybits); // NaN > non-NaN + } + + public override int GetHashCode() + { + if (double.IsFinite(v) && v == Math.Round(v, MidpointRounding.ToEven)) + { + return StarlarkInt.OfFiniteDouble(v).GetHashCode(); + } + long bits = BitConverter.DoubleToInt64Bits(v); + return unchecked((int)(bits ^ (long)((ulong)bits >> 32))); + } + + public override bool Equals(object? that) => + (that is StarlarkFloat f && Equal(v, f.v)) + || (that is StarlarkInt i && StarlarkInt.IntEqualsFloat(i, this)); + + private static bool Equal(double x, double y) => + x == y || (double.IsNaN(x) && double.IsNaN(y)); + + // Performs printf-style string conversion of a double. conv is one of [efgEFG]. + internal static string Format(double v, char conv) + { + if (!double.IsFinite(v)) + { + if (double.IsPositiveInfinity(v)) + { + return "+inf"; + } + if (double.IsNegativeInfinity(v)) + { + return "-inf"; + } + return "nan"; + } + + string s; + switch (conv) + { + case 'e': + s = FormatExp(v, false); + break; + case 'E': + s = FormatExp(v, true); + break; + case 'f': + case 'F': + s = v.ToString("F6", CultureInfo.InvariantCulture); + break; + case 'g': + s = FormatG(v, false); + break; + case 'G': + s = FormatG(v, true); + break; + default: + throw new ArgumentException("unsupported conversion: " + conv); + } + + if (conv is 'g' or 'G') + { + char e = conv == 'g' ? 'e' : 'E'; + int ei = s.IndexOf(e); + if (ei < 0) + { + int dot = s.IndexOf('.'); + if (dot < 0) + { + s += ".0"; + } + else + { + int i; + for (i = s.Length - 1; i > dot + 1 && s[i] == '0'; i--) + { + } + s = s[..(i + 1)]; + } + } + else + { + int i; + for (i = ei - 1; s[i] == '0'; i--) + { + } + if (s[i] == '.') + { + i--; + } + if (i < ei - 1) + { + s = new StringBuilder(i + 1 + s.Length - ei) + .Append(s, 0, i + 1) + .Append(s, ei, s.Length - ei) + .ToString(); + } + } + } + + return s; + } + + // Emulates C printf "%.17g": 17 significant digits, exponent form when needed. + private static string FormatG(double v, bool upper) + { + // "R"/"G17" round-trips; then convert to a %g-like shape (17 sig digits max). + string s = v.ToString("G17", CultureInfo.InvariantCulture); + // .NET uses "E+xx" uppercase; normalize the exponent marker. + int e = s.IndexOfAny(new[] { 'e', 'E' }); + if (e >= 0) + { + string mantissa = s[..e]; + string exp = s[(e + 1)..]; + // Ensure sign and at least two exponent digits, matching C. + char sign = '+'; + if (exp.StartsWith('+') || exp.StartsWith('-')) + { + sign = exp[0]; + exp = exp[1..]; + } + exp = exp.TrimStart('0'); + if (exp.Length < 2) + { + exp = exp.PadLeft(2, '0'); + } + char marker = upper ? 'E' : 'e'; + return mantissa + marker + sign + exp; + } + return s; + } + + private static string FormatExp(double v, bool upper) + { + string s = v.ToString("E6", CultureInfo.InvariantCulture); + if (!upper) + { + s = s.Replace('E', 'e'); + } + return s; + } + + /// Returns x // y (floor of division). + internal static StarlarkFloat Floordiv(double x, double y) + { + if (y == 0.0) + { + throw Starlark.Errorf("integer division by zero"); + } + return Of(Math.Floor(x / y)); + } + + /// Returns x / y (floating-point division). + internal static StarlarkFloat Div(double x, double y) + { + if (y == 0.0) + { + throw Starlark.Errorf("floating-point division by zero"); + } + return Of(x / y); + } + + /// Returns x % y (floating-point remainder). + internal static StarlarkFloat Mod(double x, double y) + { + if (y == 0.0) + { + throw Starlark.Errorf("floating-point modulo by zero"); + } + double z = x % y; + if ((x < 0) != (y < 0) && z != 0) + { + z += y; + } + return Of(z); + } + + /// + /// Returns the Starlark int value closest to x, truncating towards zero. Throws if x is not finite. + /// + internal static StarlarkInt FiniteDoubleToIntExact(double x) + { + if (long.MinValue <= x && x <= long.MaxValue) + { + return StarlarkInt.Of((long)x); + } + int shift = GetShift(x); + if (shift <= 0) + { + throw new InvalidOperationException("non-positive shift"); + } + long mantissa = GetMantissa(x); + return StarlarkInt.Of((BigInteger)mantissa << shift); + } + + private const int EXPONENT_MASK = (1 << 11) - 1; + + // Returns the effective signed mantissa of x. Precondition: x is finite. + internal static long GetMantissa(double x) + { + long bits = BitConverter.DoubleToInt64Bits(x); + long mantissa = bits & ((1L << 52) - 1); + int exp = (int)((ulong)bits >> 52) & EXPONENT_MASK; + switch (exp) + { + case 0: // denormal + break; + case EXPONENT_MASK: + throw new ArgumentException("not finite: " + x); + default: // normal + mantissa |= 1L << 52; + break; + } + return x < 0 ? -mantissa : mantissa; + } + + // Returns the effective left (+) or right (-) shift required of GetMantissa(x). Precondition: finite. + internal static int GetShift(double x) + { + long bits = BitConverter.DoubleToInt64Bits(x); + int exp = (int)((ulong)bits >> 52) & EXPONENT_MASK; + switch (exp) + { + case 0: // denormal + exp -= 1022; + break; + case EXPONENT_MASK: + throw new ArgumentException("not finite: " + x); + default: // normal + exp -= 1023; + break; + } + return exp - 52; + } +} diff --git a/src/Starlark/Eval/StarlarkInt.cs b/src/Starlark/Eval/StarlarkInt.cs new file mode 100644 index 000000000..2a40be18f --- /dev/null +++ b/src/Starlark/Eval/StarlarkInt.cs @@ -0,0 +1,645 @@ +// Copyright 2020 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Globalization; +using System.Numerics; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// The Starlark int data type. Port of net.starlark.java.eval.StarlarkInt. +/// +/// Mirrors the Java three-representation structure: Int32 (fits in a C# int), +/// Int64 (fits in a long), and Big (backed by ). +/// +[StarlarkBuiltin("int", Category = "core", Doc = "The type of integers in Starlark.")] +public abstract class StarlarkInt : IStarlarkPrintableValue, IComparable +{ + // A cache of small integers >= LEAST_SMALLINT. + private const int LEAST_SMALLINT = -128; + private static readonly Int32Value[] smallints = new Int32Value[100_000]; + + internal static readonly StarlarkInt ZERO = Of(0); + private static readonly StarlarkInt ONE = Of(1); + private static readonly StarlarkInt MINUS_ONE = Of(-1); + + private StarlarkInt() { } + + /// Returns the Starlark int value that represents x. + public static StarlarkInt Of(int x) + { + long index = (long)x - LEAST_SMALLINT; + if (0 <= index && index < smallints.Length) + { + Int32Value? xi = smallints[index]; + if (xi == null) + { + xi = new Int32Value(x); + smallints[index] = xi; + } + return xi; + } + return new Int32Value(x); + } + + /// Returns the Starlark int value that represents x. + public static StarlarkInt Of(long x) + { + if ((int)x == x) + { + return Of((int)x); + } + return new Int64Value(x); + } + + /// Returns the Starlark int value that represents x. + public static StarlarkInt Of(BigInteger x) + { + if (x >= long.MinValue && x <= long.MaxValue) + { + return Of((long)x); + } + return new BigValue(x); + } + + /// Returns the StarlarkInt value that most closely approximates x. + internal static StarlarkInt OfFiniteDouble(double x) => StarlarkFloat.FiniteDoubleToIntExact(x); + + /// + /// Returns the int denoted by a literal string in the specified base, as if by int(s, base). + /// + public static StarlarkInt Parse(string s, int @base) + { + string stringForErrors = s; + if (s.Length == 0) + { + throw new FormatException("empty string"); + } + + bool isNegative = false; + char c = s[0]; + if (c == '+') + { + s = s[1..]; + } + else if (c == '-') + { + s = s[1..]; + isNegative = true; + } + + string digits = s; + + if (s.Length > 1 && s[0] == '0') + { + int prefixBase = 0; + c = s[1]; + if (c is 'b' or 'B') + { + prefixBase = 2; + } + else if (c is 'o' or 'O') + { + prefixBase = 8; + } + else if (c is 'x' or 'X') + { + prefixBase = 16; + } + if (prefixBase != 0 && (@base == 0 || @base == prefixBase)) + { + @base = prefixBase; + digits = s[2..]; + } + } + + if (ReferenceEquals(digits, s) && @base == 0) + { + if (s.Length > 1 && s[0] == '0') + { + throw new FormatException( + "cannot infer base when string begins with a 0: " + + Starlark.Repr(stringForErrors, StarlarkSemantics.DEFAULT)); + } + @base = 10; + } + if (@base < 2 || @base > 36) + { + throw new FormatException( + string.Format(CultureInfo.InvariantCulture, "invalid base {0} (want 2 <= base <= 36)", @base)); + } + + if (digits.StartsWith('+') || digits.StartsWith('-')) + { + throw new FormatException(string.Format( + "invalid base-{0} literal: {1}", + @base, Starlark.Repr(stringForErrors, StarlarkSemantics.DEFAULT))); + } + + StarlarkInt result; + if (TryParseBigInteger(digits, @base, out BigInteger big)) + { + result = Of(big); + } + else + { + throw new FormatException(string.Format( + "invalid base-{0} literal: {1}", + @base, Starlark.Repr(stringForErrors, StarlarkSemantics.DEFAULT))); + } + return isNegative ? Uminus(result) : result; + } + + private static bool TryParseBigInteger(string digits, int @base, out BigInteger value) + { + value = BigInteger.Zero; + if (digits.Length == 0) + { + return false; + } + BigInteger b = @base; + foreach (char ch in digits) + { + int d; + if (ch >= '0' && ch <= '9') + { + d = ch - '0'; + } + else if (ch >= 'a' && ch <= 'z') + { + d = ch - 'a' + 10; + } + else if (ch >= 'A' && ch <= 'Z') + { + d = ch - 'A' + 10; + } + else + { + return false; + } + if (d >= @base) + { + return false; + } + value = value * b + d; + } + return true; + } + + // Subclass for values exactly representable in a C# int. + private sealed class Int32Value : StarlarkInt + { + internal readonly int V; + + internal Int32Value(int v) => V = v; + + public override int ToInt(string what) => V; + + public override long ToLong(string what) => V; + + internal override bool TryToLong(out long value) + { + value = V; + return true; + } + + public override BigInteger ToBigInteger() => V; + + public override object ToNumber() => V; + + public override int Signum() => Math.Sign(V); + + public override void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(V); + + public override int GetHashCode() => + unchecked(0x316c5239 * V.GetHashCode() ^ 0x67c4a7d5); + + public override bool Equals(object? that) => + (that is Int32Value o && V == o.V) + || (that is StarlarkFloat f && IntEqualsFloat(this, f)); + } + + // Subclass for values exactly representable in a C# long. + private sealed class Int64Value : StarlarkInt + { + internal readonly long V; + + internal Int64Value(long v) => V = v; + + public override long ToLong(string what) => V; + + internal override bool TryToLong(out long value) + { + value = V; + return true; + } + + public override BigInteger ToBigInteger() => V; + + public override object ToNumber() => V; + + public override int Signum() => Math.Sign(V); + + public override void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(V); + + public override int GetHashCode() => + unchecked((int)(0x67c4a7d5 * (long)V.GetHashCode() ^ 0xee914a1b)); + + public override bool Equals(object? that) => + (that is Int64Value o && V == o.V) + || (that is StarlarkFloat f && IntEqualsFloat(this, f)); + } + + // Subclass for values not exactly representable in a long. + private sealed class BigValue : StarlarkInt + { + internal readonly BigInteger V; + + internal BigValue(BigInteger v) => V = v; + + public override BigInteger ToBigInteger() => V; + + public override object ToNumber() => V; + + public override int Signum() => V.Sign; + + public override void Repr(Printer printer, StarlarkSemantics semantics) => + printer.Append(V.ToString(CultureInfo.InvariantCulture)); + + public override int GetHashCode() => + unchecked((int)(0xee914a1b * (long)V.GetHashCode() ^ 0x6406918f)); + + public override bool Equals(object? that) => + (that is BigValue o && V == o.V) + || (that is StarlarkFloat f && IntEqualsFloat(this, f)); + } + + /// Returns the value as a boxed Number (int, long, or BigInteger). + public abstract object ToNumber(); + + /// Returns the signum of this StarlarkInt (-1, 0, or +1). + public abstract int Signum(); + + public override string ToString() => this switch + { + Int32Value i => i.V.ToString(CultureInfo.InvariantCulture), + Int64Value l => l.V.ToString(CultureInfo.InvariantCulture), + _ => ToBigInteger().ToString(CultureInfo.InvariantCulture), + }; + + public abstract void Repr(Printer printer, StarlarkSemantics semantics); + + /// Returns the signed 32-bit value, or fails if not exactly representable. + public virtual int ToInt(string what) => + throw Starlark.Errorf("got {0} for {1}, want value in signed 32-bit range", this, what); + + /// Returns the signed 64-bit value, or fails if not exactly representable. + public virtual long ToLong(string what) => + throw Starlark.Errorf("got {0} for {1}, want value in the signed 64-bit range", this, what); + + // Fast path used by arithmetic: returns false instead of throwing when out of long range. + internal virtual bool TryToLong(out long value) + { + value = 0; + return false; + } + + /// Returns the nearest IEEE-754 double closest to this int, which may be +/-Inf. + public double ToDouble() => this switch + { + Int32Value i => i.V, + Int64Value l => l.V, + _ => (double)ToBigInteger(), + }; + + /// Returns the nearest finite double, or fails if too large. + public double ToFiniteDouble() + { + double d = ToDouble(); + if (!double.IsFinite(d)) + { + throw Starlark.Errorf("int too large to convert to float"); + } + return d; + } + + /// Returns the BigInteger value of this StarlarkInt. + public abstract BigInteger ToBigInteger(); + + /// Returns the value as a C# signed 32-bit int, or throws if out of range. + public int ToIntUnchecked() + { + if (this is Int32Value i) + { + return i.V; + } + throw new ArgumentException("not a signed 32-bit value"); + } + + /// Returns the result of truncating this value into the signed 32-bit range. + public int TruncateToInt() => this switch + { + Int32Value i => i.V, + Int64Value l => unchecked((int)l.V), + _ => unchecked((int)(long)(ToBigInteger() & 0xFFFFFFFF)), + }; + + public bool IsImmutable() => true; + + public bool Truth() => !ReferenceEquals(this, ZERO) && Signum() != 0; + + public int CompareTo(StarlarkInt? x) => Compare(this, x!); + + // binary operators + + /// Returns signum(x - y). + public static int Compare(StarlarkInt x, StarlarkInt y) + { + if (x.TryToLong(out long xl)) + { + if (y.TryToLong(out long yl)) + { + return xl.CompareTo(yl); + } + return -((BigValue)y).V.Sign; // (long, big) + } + return y is BigValue yb + ? ((BigValue)x).V.CompareTo(yb.V) // (big, big) + : ((BigValue)x).V.Sign; // (big, long) + } + + /// Returns x + y. + public static StarlarkInt Add(StarlarkInt x, StarlarkInt y) + { + if (x.TryToLong(out long xl) && y.TryToLong(out long yl)) + { + long zl = unchecked(xl + yl); + bool overflow = ((xl ^ zl) & (yl ^ zl)) < 0; + if (!overflow) + { + return Of(zl); + } + } + return Of(x.ToBigInteger() + y.ToBigInteger()); + } + + /// Returns x - y. + public static StarlarkInt Subtract(StarlarkInt x, StarlarkInt y) + { + if (x.TryToLong(out long xl) && y.TryToLong(out long yl)) + { + long zl = unchecked(xl - yl); + bool overflow = ((xl ^ yl) & (xl ^ zl)) < 0; + if (!overflow) + { + return Of(zl); + } + } + return Of(x.ToBigInteger() - y.ToBigInteger()); + } + + /// Returns x * y. + public static StarlarkInt Multiply(StarlarkInt x, StarlarkInt y) + { + if (x.TryToLong(out long xl) && y.TryToLong(out long yl)) + { + long hi = Math.BigMul(xl, yl, out ulong lo); + long lows = unchecked((long)lo); + // Check int128 result is within int64 range. + if (hi == (lows >> 63)) + { + return Of(lows); + } + } + return Of(x.ToBigInteger() * y.ToBigInteger()); + } + + /// Returns x // y (floor of integer division). + public static StarlarkInt Floordiv(StarlarkInt x, StarlarkInt y) + { + if (ReferenceEquals(y, ZERO) || y.Signum() == 0) + { + throw Starlark.Errorf("integer division by zero"); + } + if (x.TryToLong(out long xl) && y.TryToLong(out long yl) + && !(xl == long.MinValue && yl == -1)) + { + long quo = FloorDiv(xl, yl); + return Of(quo); + } + BigInteger xbig = x.ToBigInteger(); + BigInteger ybig = y.ToBigInteger(); + BigInteger quotient = BigInteger.DivRem(xbig, ybig, out BigInteger rem); + if ((xbig.Sign < 0) != (ybig.Sign < 0) && rem.Sign != 0) + { + quotient -= BigInteger.One; + } + return Of(quotient); + } + + /// Returns x % y. + public static StarlarkInt Mod(StarlarkInt x, StarlarkInt y) + { + if (ReferenceEquals(y, ZERO) || y.Signum() == 0) + { + throw Starlark.Errorf("integer modulo by zero"); + } + if (x.TryToLong(out long xl) && y.TryToLong(out long yl)) + { + return Of(FloorMod(xl, yl)); + } + BigInteger xbig = x.ToBigInteger(); + BigInteger ybig = y.ToBigInteger(); + BigInteger z = xbig % ybig; + if ((x.Signum() < 0) != (y.Signum() < 0) && z.Sign != 0) + { + z += ybig; + } + return Of(z); + } + + /// Returns x >> y. + public static StarlarkInt ShiftRight(StarlarkInt x, StarlarkInt y) + { + int yi = y.ToInt("shift count"); + if (yi < 0) + { + throw Starlark.Errorf("negative shift count: {0}", yi); + } + if (x.TryToLong(out long xl)) + { + if (yi >= 64) + { + return xl < 0 ? Of(-1) : ZERO; + } + return Of(xl >> yi); + } + return Of(x.ToBigInteger() >> yi); + } + + /// Returns x << y. + public static StarlarkInt ShiftLeft(StarlarkInt x, StarlarkInt y) + { + int yi = y.ToInt("shift count"); + if (yi < 0) + { + throw Starlark.Errorf("negative shift count: {0}", yi); + } + if (yi >= 512) + { + throw Starlark.Errorf("shift count too large: {0}", yi); + } + if (x.TryToLong(out long xl)) + { + long z = unchecked(xl << yi); + if (yi < 64 && (z >> yi) == xl) + { + return Of(z); + } + } + return Of(x.ToBigInteger() << yi); + } + + /// Returns x ^ y. + public static StarlarkInt Xor(StarlarkInt x, StarlarkInt y) + { + if (x.TryToLong(out long xl) && y.TryToLong(out long yl)) + { + return Of(xl ^ yl); + } + return Of(x.ToBigInteger() ^ y.ToBigInteger()); + } + + /// Returns x | y. + public static StarlarkInt Or(StarlarkInt x, StarlarkInt y) + { + if (x.TryToLong(out long xl) && y.TryToLong(out long yl)) + { + return Of(xl | yl); + } + return Of(x.ToBigInteger() | y.ToBigInteger()); + } + + /// Returns x & y. + public static StarlarkInt And(StarlarkInt x, StarlarkInt y) + { + if (x.TryToLong(out long xl) && y.TryToLong(out long yl)) + { + return Of(xl & yl); + } + return Of(x.ToBigInteger() & y.ToBigInteger()); + } + + /// Returns ~x. + public static StarlarkInt Bitnot(StarlarkInt x) + { + if (x.TryToLong(out long xl)) + { + return Of(~xl); + } + return Of(-((BigValue)x).V - BigInteger.One); + } + + /// Returns -x. + public static StarlarkInt Uminus(StarlarkInt x) + { + switch (x) + { + case Int32Value i: + return Of(-(long)i.V); + case Int64Value l when l.V != long.MinValue: + return Of(-l.V); + default: + return Of(-x.ToBigInteger()); + } + } + + /// Reports whether int x exactly equals float y. + internal static bool IntEqualsFloat(StarlarkInt x, StarlarkFloat y) + { + double yf = y.ToDouble(); + return !double.IsNaN(yf) && CompareIntAndDouble(x, yf) == 0; + } + + /// Returns an exact three-valued comparison of int x with (non-NaN) double y. + internal static int CompareIntAndDouble(StarlarkInt x, double y) + { + if (double.IsInfinity(y)) + { + return y > 0 ? -1 : +1; + } + + if (x is Int32Value || (x is Int64Value l && LongHasExactDouble(l.V))) + { + double xf = x.ToDouble(); + if (xf > y) + { + return +1; + } + if (xf < y) + { + return -1; + } + return 0; + } + + int xsign = x.Signum(); + int ysign = Math.Sign(y); + if (xsign > ysign) + { + return +1; + } + if (xsign < ysign) + { + return -1; + } + + int shift = StarlarkFloat.GetShift(y); + BigInteger xbig = x.ToBigInteger(); + if (shift < 0) + { + xbig <<= -shift; + } + BigInteger ybig = StarlarkFloat.GetMantissa(y); + if (shift > 0) + { + ybig <<= shift; + } + return xbig.CompareTo(ybig); + } + + private static bool LongHasExactDouble(long x) => (long)(double)x == x; + + // Java Math.floorDiv/floorMod equivalents. + private static long FloorDiv(long x, long y) + { + long q = x / y; + if ((x ^ y) < 0 && q * y != x) + { + q--; + } + return q; + } + + private static long FloorMod(long x, long y) + { + long r = x % y; + if (r != 0 && (r ^ y) < 0) + { + r += y; + } + return r; + } +} diff --git a/src/Starlark/Eval/StarlarkSemantics.cs b/src/Starlark/Eval/StarlarkSemantics.cs new file mode 100644 index 000000000..7d1b6f14e --- /dev/null +++ b/src/Starlark/Eval/StarlarkSemantics.cs @@ -0,0 +1,222 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Text; + +namespace Starlark.Eval; + +/// +/// A StarlarkSemantics is an immutable set of optional name/value pairs that affect the dynamic +/// behavior of Starlark operators and built-in functions. Port of +/// net.starlark.java.eval.StarlarkSemantics. +/// +/// Boolean values are accessed using a string key prefixed with "+" (default true) or "-" +/// (default false). Other values are accessed using a . +/// +public class StarlarkSemantics +{ + /// The empty semantics, in which every option has its default value. + public static readonly StarlarkSemantics DEFAULT = + new(ImmutableSortedDictionary.Empty); + + // A map entry is accessed by Key iff its name has no [+-] prefix. + private readonly ImmutableSortedDictionary map; + private readonly int hashCode; + + private StarlarkSemantics(ImmutableSortedDictionary map) + { + this.map = map; + int h = 0; + foreach (var e in map) + { + h += e.Key.GetHashCode() ^ e.Value.GetHashCode(); + } + this.hashCode = h; + } + + protected StarlarkSemantics(StarlarkSemantics other) : this(other.map) { } + + /// Returns the value of a boolean option, which must have a [+-] prefix. + public bool GetBool(string name) + { + char prefix = name[0]; + if (prefix != '+' && prefix != '-') + { + throw new ArgumentException("boolean option name must start with + or -: " + name); + } + bool defaultValue = prefix == '+'; + return map.TryGetValue(name, out object? v) ? (bool)v : defaultValue; + } + + /// Returns the value of the option denoted by . + public T Get(Key key) where T : notnull => + map.TryGetValue(key.Name, out object? v) ? (T)v : key.DefaultValue; + + /// + /// Returns the value of the option with the given name, or the default value if not set. + /// + public object GetGeneric(string name, object defaultValue) + { + if (map.TryGetValue(name, out object? v) + || map.TryGetValue("+" + name, out v) + || map.TryGetValue("-" + name, out v)) + { + return v; + } + return defaultValue; + } + + /// A Key identifies an option, providing its name, type, and default value. + public sealed class Key where T : notnull + { + public string Name { get; } + public T DefaultValue { get; } + + public Key(string name, T defaultValue) + { + char prefix = name[0]; + if (prefix == '-' || prefix == '+') + { + throw new ArgumentException("Key name must not start with + or -: " + name); + } + Name = name; + DefaultValue = defaultValue ?? throw new ArgumentNullException(nameof(defaultValue)); + } + + public override string ToString() => Name; + } + + /// Returns a new builder initially holding the same key/value pairs as this. + public Builder ToBuilder() => new(map.ToBuilder()); + + /// Returns a new empty builder. + public static Builder NewBuilder() => + new(ImmutableSortedDictionary.CreateBuilder()); + + /// A mutable container used to construct an immutable StarlarkSemantics. + public sealed class Builder + { + private readonly ImmutableSortedDictionary.Builder map; + + internal Builder(ImmutableSortedDictionary.Builder map) => this.map = map; + + public Builder Set(Key key, T value) where T : notnull + { + if (!value.Equals(key.DefaultValue)) + { + map[key.Name] = value; + } + else + { + map.Remove(key.Name); + } + return this; + } + + public Builder SetBool(string name, bool value) + { + char prefix = name[0]; + if (prefix != '+' && prefix != '-') + { + throw new ArgumentException("boolean option name must start with + or -: " + name); + } + bool defaultValue = prefix == '+'; + if (value != defaultValue) + { + map[name] = value; + } + else + { + map.Remove(name); + } + return this; + } + + public StarlarkSemantics Build() => + map.Count == 0 ? DEFAULT : new StarlarkSemantics(map.ToImmutable()); + } + + /// Returns true if a feature attached to the given toggling flags should be enabled. + internal bool IsFeatureEnabledBasedOnTogglingFlags(string enablingFlag, string disablingFlag) + { + if (!string.IsNullOrEmpty(enablingFlag) && !string.IsNullOrEmpty(disablingFlag)) + { + throw new ArgumentException("at least one of enablingFlag or disablingFlag must be empty"); + } + if (!string.IsNullOrEmpty(enablingFlag)) + { + return GetBool(enablingFlag); + } + if (!string.IsNullOrEmpty(disablingFlag)) + { + return !GetBool(disablingFlag); + } + return true; + } + + /// Returns a possibly different equivalent instance for caching purposes. + public virtual StarlarkSemantics GetBuiltinManagerCacheKey() => this; + + public override int GetHashCode() => hashCode; + + public override bool Equals(object? that) => + ReferenceEquals(this, that) + || (that is StarlarkSemantics s && DictionaryEquals(map, s.map)); + + private static bool DictionaryEquals( + ImmutableSortedDictionary a, ImmutableSortedDictionary b) + { + if (a.Count != b.Count) + { + return false; + } + foreach (var e in a) + { + if (!b.TryGetValue(e.Key, out object? v) || !Equals(e.Value, v)) + { + return false; + } + } + return true; + } + + public override string ToString() + { + var buf = new StringBuilder("StarlarkSemantics{"); + string sep = ""; + foreach (var e in map) + { + buf.Append(sep); + sep = ", "; + string key = e.Key; + buf.Append(key[0] == '+' || key[0] == '-' ? key[1..] : key); + buf.Append('=').Append(e.Value); + } + return buf.Append('}').ToString(); + } + + // -- semantics options affecting the Starlark interpreter itself -- + + public const string PRINT_TEST_MARKER = "-print_test_marker"; + public const string ALLOW_RECURSION = "-allow_recursion"; + public const string EXPERIMENTAL_ENABLE_STARLARK_SET = "+experimental_enable_starlark_set"; + public const string INTERNAL_BAZEL_ONLY_UTF_8_BYTE_STRINGS = + "-internal_bazel_only_utf_8_byte_strings"; + public const string EXPERIMENTAL_STARLARK_STATIC_TYPE_CHECKING = + "-experimental_starlark_static_type_checking"; + public const string EXPERIMENTAL_STARLARK_DYNAMIC_TYPE_CHECKING = + "-experimental_starlark_dynamic_type_checking"; + public const string FORCE_STARLARK_STACK_TRACE = "-force_starlark_stack_trace"; +} diff --git a/src/Starlark/Eval/StarlarkValueInterfaces.cs b/src/Starlark/Eval/StarlarkValueInterfaces.cs new file mode 100644 index 000000000..0ae332922 --- /dev/null +++ b/src/Starlark/Eval/StarlarkValueInterfaces.cs @@ -0,0 +1,114 @@ +// Copyright 2015 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Starlark.Syntax; + +namespace Starlark.Eval; + +/// +/// The printing surface of net.starlark.java.eval.StarlarkValue (repr, str, +/// debugPrint). The base marker (with Truth/ +/// IsImmutable) lives in StarlarkCore.cs; this companion interface adds the interpreter +/// printing hooks so value types can be rendered by . +/// +public interface IStarlarkPrintableValue : IStarlarkValue +{ + /// Prints an official (parseable) representation of this value. + void Repr(Printer printer, StarlarkSemantics semantics) => + printer.Append(""); + + /// Prints an informal, human-readable representation of this value. + void Str(Printer printer, StarlarkSemantics semantics) => Repr(printer, semantics); + + /// Prints an informal debug representation of this value. + void DebugPrint(Printer printer, StarlarkThread thread) => Str(printer, thread.GetSemantics()); + + /// Returns normally if the value is hashable and thus suitable as a dict key. + void CheckHashable() + { + if (!IsImmutable()) + { + throw Starlark.Errorf("unhashable type: '{0}'", Starlark.Type(this)); + } + } +} + +/// +/// A StarlarkIterable value may be iterated by Starlark constructs such as for-loops and +/// comprehensions. Port of net.starlark.java.eval.StarlarkIterable. +/// +public interface IStarlarkIterable : IStarlarkValue, IEnumerable { } + +/// +/// A Starlark value that supports membership tests (key in object). Port of +/// net.starlark.java.eval.StarlarkMembershipTestable. +/// +public interface IStarlarkMembershipTestable : IStarlarkValue +{ + bool ContainsKey(StarlarkSemantics semantics, object key); +} + +/// +/// A Starlark value that supports indexed access (object[key]). Port of +/// net.starlark.java.eval.StarlarkIndexable. +/// +public interface IStarlarkIndexable : IStarlarkMembershipTestable +{ + object? GetIndex(StarlarkSemantics semantics, object key); + + /// Variant providing a StarlarkThread on method calls. + public interface IThreaded + { + object? GetIndex(StarlarkThread thread, StarlarkSemantics semantics, object key); + + bool ContainsKey(StarlarkThread thread, StarlarkSemantics semantics, object key); + } +} + +/// +/// A Starlark value that supports binary operators such as x + y. Port of +/// net.starlark.java.eval.HasBinary. The token kind is represented as a +/// from the syntax package. +/// +public interface IHasBinary : IStarlarkValue +{ + /// + /// Returns this op that if , else that op this. Returns + /// null if the operation is not supported. + /// + object? BinaryOp(TokenKind op, object that, bool thisLeft); +} + +/// +/// A Starlark value with fields accessed using x.field. Port of +/// net.starlark.java.eval.Structure. +/// +public interface IStructure : IStarlarkValue +{ + /// Returns the value of the named field, or null if the field does not exist. + object? GetValue(string name); + + /// Returns the value of the named field, with access to the semantics. + object? GetValue(StarlarkSemantics semantics, string name) => GetValue(name); + + /// Returns the names of this value's fields, in some stable order. + IReadOnlyCollection GetFieldNames(); + + /// Returns the error message for an attempt to access an undefined field, or null. + string? GetErrorMessageForUnknownField(string field); + + /// Updates the named field of this value as if by this.field = value. + void SetField(string field, object value) => + throw Starlark.Errorf("{0} value does not support field assignment", Starlark.Type(this)); +} diff --git a/src/Starlark/Syntax/Argument.cs b/src/Starlark/Syntax/Argument.cs new file mode 100644 index 000000000..4678f1d99 --- /dev/null +++ b/src/Starlark/Syntax/Argument.cs @@ -0,0 +1,102 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// Syntax node for an argument to a function. +/// +/// Arguments may be of four forms, as in f(expr, id=expr, *expr, **expr). These are +/// represented by the subclasses Positional, Keyword, Star, and StarStar. +/// +public abstract class Argument : Node +{ + protected readonly Expression value; + + internal Argument(FileLocations locs, Expression value) + : base(locs) + { + this.value = value ?? throw new ArgumentNullException(nameof(value)); + } + + public Expression GetValue() => value; + + public override int GetEndOffset() => value.GetEndOffset(); + + /// Return the name of this argument's parameter, or null if it is not a Keyword argument. + public virtual string? GetName() => null; + + /// Syntax node for a positional argument, f(expr). + public sealed class Positional : Argument + { + internal Positional(FileLocations locs, Expression value) + : base(locs, value) + { + } + + public override int GetStartOffset() => value.GetStartOffset(); + } + + /// Syntax node for a keyword argument, f(id=expr). + public sealed class Keyword : Argument + { + internal readonly Identifier id; + + internal Keyword(FileLocations locs, Identifier id, Expression value) + : base(locs, value) + { + this.id = id; + } + + public Identifier GetIdentifier() => id; + + public override string? GetName() => id.GetName(); + + public override int GetStartOffset() => id.GetStartOffset(); + } + + /// Syntax node for an argument of the form f(*expr). + public sealed class Star : Argument + { + private readonly int starOffset; + + internal Star(FileLocations locs, int starOffset, Expression value) + : base(locs, value) + { + this.starOffset = starOffset; + } + + public override int GetStartOffset() => starOffset; + } + + /// Syntax node for an argument of the form f(**expr). + public sealed class StarStar : Argument + { + private readonly int starStarOffset; + + internal StarStar(FileLocations locs, int starStarOffset, Expression value) + : base(locs, value) + { + this.starStarOffset = starStarOffset; + } + + public override int GetStartOffset() => starStarOffset; + } + + public override void Accept(NodeVisitor visitor) + { + // All Argument subclasses dispatch to NodeVisitor.Visit(Argument). + visitor.Visit(this); + } +} diff --git a/src/Starlark/Syntax/BinaryOperatorExpression.cs b/src/Starlark/Syntax/BinaryOperatorExpression.cs new file mode 100644 index 000000000..c33f3f24f --- /dev/null +++ b/src/Starlark/Syntax/BinaryOperatorExpression.cs @@ -0,0 +1,75 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// A BinaryExpression represents a binary operator expression 'x op y'. +public sealed class BinaryOperatorExpression : Expression +{ + private readonly Expression x; + private readonly TokenKind op; // one of 'Operators' + private readonly int opOffset; + private readonly Expression y; + + /// The set of valid binary operators. + public static readonly IReadOnlySet Operators = new HashSet + { + TokenKind.AND, + TokenKind.EQUALS_EQUALS, + TokenKind.GREATER, + TokenKind.GREATER_EQUALS, + TokenKind.IN, + TokenKind.LESS, + TokenKind.LESS_EQUALS, + TokenKind.MINUS, + TokenKind.NOT_EQUALS, + TokenKind.NOT_IN, + TokenKind.OR, + TokenKind.PERCENT, + TokenKind.SLASH, + TokenKind.SLASH_SLASH, + TokenKind.PLUS, + TokenKind.PIPE, + TokenKind.STAR, + }; + + internal BinaryOperatorExpression( + FileLocations locs, Expression x, TokenKind op, int opOffset, Expression y) + : base(locs, ExpressionKind.BINARY_OPERATOR) + { + this.x = x; + this.op = op; + this.opOffset = opOffset; + this.y = y; + } + + /// Returns the left operand. + public Expression GetX() => x; + + /// Returns the operator. + public TokenKind GetOperator() => op; + + public Location GetOperatorLocation() => Locs.GetLocation(opOffset); + + /// Returns the right operand. + public Expression GetY() => y; + + public override int GetStartOffset() => x.GetStartOffset(); + + public override int GetEndOffset() => y.GetEndOffset(); + + public override string ToString() => x + " " + op.ToDisplayString() + " " + y; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/CallExpression.cs b/src/Starlark/Syntax/CallExpression.cs new file mode 100644 index 000000000..5029fb9b1 --- /dev/null +++ b/src/Starlark/Syntax/CallExpression.cs @@ -0,0 +1,80 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Text; + +namespace Starlark.Syntax; + +/// Syntax node for a function call expression. +public sealed class CallExpression : Expression +{ + private readonly Expression function; + private readonly Location lparenLocation; + private readonly ImmutableArray arguments; + private readonly int rparenOffset; + + private readonly int numPositionalArgs; + + internal CallExpression( + FileLocations locs, + Expression function, + Location lparenLocation, + ImmutableArray arguments, + int rparenOffset) + : base(locs, ExpressionKind.CALL) + { + this.function = function ?? throw new ArgumentNullException(nameof(function)); + this.lparenLocation = lparenLocation; + this.arguments = arguments; + this.rparenOffset = rparenOffset; + + int n = 0; + foreach (Argument arg in arguments) + { + if (arg is Argument.Positional) + { + n++; + } + } + this.numPositionalArgs = n; + } + + /// Returns the function that is called. + public Expression GetFunction() => function; + + /// Returns the number of arguments of type . + public int GetNumPositionalArguments() => numPositionalArgs; + + /// Returns the function call's arguments. + public IReadOnlyList GetArguments() => arguments; + + public override int GetStartOffset() => function.GetStartOffset(); + + public override int GetEndOffset() => rparenOffset + 1; + + public Location GetLparenLocation() => lparenLocation; + + public override string ToString() + { + var buf = new StringBuilder(); + buf.Append(function); + buf.Append('('); + ListExpression.AppendNodes(buf, arguments); + buf.Append(')'); + return buf.ToString(); + } + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/Comment.cs b/src/Starlark/Syntax/Comment.cs new file mode 100644 index 000000000..7d5e80114 --- /dev/null +++ b/src/Starlark/Syntax/Comment.cs @@ -0,0 +1,56 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Syntax node for comments. +public sealed class Comment : Node +{ + private readonly int offset; + private readonly string text; + + internal Comment(FileLocations locs, int offset, string text) + : base(locs) + { + this.offset = offset; + this.text = text; + } + + /// Returns the text of the comment, including the leading '#' but not the trailing newline. + public string GetText() => text; + + /// Returns true if the comment starts with #:, like a Sphinx autodoc-style doc comment. + public bool HasDocCommentPrefix() => text.StartsWith("#:", StringComparison.Ordinal); + + /// + /// If the comment starts with a #: or #: prefix, returns the text following it; + /// otherwise, returns null. + /// + public string? GetDocCommentText() + { + if (HasDocCommentPrefix()) + { + return text.StartsWith("#: ", StringComparison.Ordinal) ? text.Substring(3) : text.Substring(2); + } + return null; + } + + public override int GetStartOffset() => offset; + + public override int GetEndOffset() => offset + text.Length; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + + public override string ToString() => text; +} diff --git a/src/Starlark/Syntax/ConditionalExpression.cs b/src/Starlark/Syntax/ConditionalExpression.cs new file mode 100644 index 000000000..6e3edf4e7 --- /dev/null +++ b/src/Starlark/Syntax/ConditionalExpression.cs @@ -0,0 +1,44 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Syntax node for an expression of the form t if cond else f. +public sealed class ConditionalExpression : Expression +{ + private readonly Expression t; + private readonly Expression cond; + private readonly Expression f; + + public Expression GetThenCase() => t; + + public Expression GetCondition() => cond; + + public Expression GetElseCase() => f; + + /// Constructor for a conditional expression. + internal ConditionalExpression(FileLocations locs, Expression t, Expression cond, Expression f) + : base(locs, ExpressionKind.CONDITIONAL) + { + this.t = t; + this.cond = cond; + this.f = f; + } + + public override int GetStartOffset() => t.GetStartOffset(); + + public override int GetEndOffset() => f.GetEndOffset(); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/DictExpression.cs b/src/Starlark/Syntax/DictExpression.cs new file mode 100644 index 000000000..4f5151faa --- /dev/null +++ b/src/Starlark/Syntax/DictExpression.cs @@ -0,0 +1,69 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// Syntax node for dict expressions. +public sealed class DictExpression : Expression +{ + /// A key/value pair in a dict expression or comprehension. + public sealed class Entry : Node + { + private readonly Expression key; + private readonly int colonOffset; + private readonly Expression value; + + internal Entry(FileLocations locs, Expression key, int colonOffset, Expression value) + : base(locs) + { + this.key = key; + this.colonOffset = colonOffset; + this.value = value; + } + + public Expression GetKey() => key; + + public Expression GetValue() => value; + + public override int GetStartOffset() => key.GetStartOffset(); + + public override int GetEndOffset() => value.GetEndOffset(); + + public Location GetColonLocation() => Locs.GetLocation(colonOffset); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + } + + private readonly int lbraceOffset; + private readonly ImmutableArray entries; + private readonly int rbraceOffset; + + internal DictExpression(FileLocations locs, int lbraceOffset, IReadOnlyList entries, int rbraceOffset) + : base(locs, ExpressionKind.DICT_EXPR) + { + this.lbraceOffset = lbraceOffset; + this.entries = entries.ToImmutableArray(); + this.rbraceOffset = rbraceOffset; + } + + public override int GetStartOffset() => lbraceOffset; + + public override int GetEndOffset() => rbraceOffset + 1; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + + public IReadOnlyList GetEntries() => entries; +} diff --git a/src/Starlark/Syntax/DotExpression.cs b/src/Starlark/Syntax/DotExpression.cs new file mode 100644 index 000000000..79832faf5 --- /dev/null +++ b/src/Starlark/Syntax/DotExpression.cs @@ -0,0 +1,44 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Syntax node for a dot expression. e.g. obj.field, but not obj.method(). +public sealed class DotExpression : Expression +{ + private readonly Expression obj; + private readonly int dotOffset; + // This Identifier's `binding` is left null by the resolver. + private readonly Identifier field; + + internal DotExpression(FileLocations locs, Expression obj, int dotOffset, Identifier field) + : base(locs, ExpressionKind.DOT) + { + this.obj = obj; + this.dotOffset = dotOffset; + this.field = field; + } + + public Expression GetObject() => obj; + + public Identifier GetField() => field; + + public override int GetStartOffset() => obj.GetStartOffset(); + + public override int GetEndOffset() => field.GetEndOffset(); + + public Location GetDotLocation() => Locs.GetLocation(dotOffset); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/Ellipsis.cs b/src/Starlark/Syntax/Ellipsis.cs new file mode 100644 index 000000000..beb384319 --- /dev/null +++ b/src/Starlark/Syntax/Ellipsis.cs @@ -0,0 +1,35 @@ +// Copyright 2023 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Syntax node for the singleton ellipsis expression. +public sealed class Ellipsis : Expression +{ + private readonly int startOffset; + + internal Ellipsis(FileLocations locs, int startOffset) + : base(locs, ExpressionKind.ELLIPSIS) + { + this.startOffset = startOffset; + } + + public override int GetStartOffset() => startOffset; + + public override int GetEndOffset() => startOffset + 3; + + public override string ToString() => "..."; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/Expression.cs b/src/Starlark/Syntax/Expression.cs new file mode 100644 index 000000000..c42ac3425 --- /dev/null +++ b/src/Starlark/Syntax/Expression.cs @@ -0,0 +1,78 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Base class for all expression nodes in the AST. +public abstract class Expression : Node +{ + /// Kind of the expression. + public enum ExpressionKind + { + BINARY_OPERATOR, + CALL, + CAST, + COMPREHENSION, + CONDITIONAL, + DICT_EXPR, + DOT, + ELLIPSIS, + FLOAT_LITERAL, + IDENTIFIER, + INDEX, + INT_LITERAL, + ISINSTANCE, + LAMBDA, + LIST_EXPR, + SLICE, + STRING_LITERAL, + UNARY_OPERATOR, + TYPE_APPLICATION, + } + + private readonly ExpressionKind kind; + + internal Expression(FileLocations locs, ExpressionKind kind) + : base(locs) + { + this.kind = kind; + } + + /// Kind of the expression. + public ExpressionKind Kind => kind; + + /// Parses an expression. + public static Expression Parse(ParserInput input, FileOptions options) + { + return Parser.ParseExpression(input, options); + } + + /// Parses an expression with default options. + public static Expression Parse(ParserInput input) + { + return Parse(input, FileOptions.DEFAULT); + } + + /// Parses a type expression. + public static Expression ParseTypeExpression(ParserInput input, FileOptions options) + { + return Parser.ParseTypeExpression(input, options); + } + + /// Parses a type expression with default options. + public static Expression ParseTypeExpression(ParserInput input) + { + return ParseTypeExpression(input, FileOptions.DEFAULT); + } +} diff --git a/src/Starlark/Syntax/FileLocations.cs b/src/Starlark/Syntax/FileLocations.cs new file mode 100644 index 000000000..62ca819c9 --- /dev/null +++ b/src/Starlark/Syntax/FileLocations.cs @@ -0,0 +1,129 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// FileLocations maps each source offset within a file to a Location. An offset is a (UTF-16) char +/// index such that 0 <= offset <= size. A Location is a (file, line, column) triple. +/// +internal sealed class FileLocations +{ + private readonly int[] linestart; // maps line number (line >= 1) to char offset + private readonly string file; + private readonly int size; // size of file in chars + + private FileLocations(int[] linestart, string file, int size) + { + this.linestart = linestart; + this.file = file; + this.size = size; + } + + internal static FileLocations Create(char[] buffer, string file) + { + return new FileLocations(ComputeLinestart(buffer), file, buffer.Length); + } + + internal string File => file; + + private int GetLineAt(int offset) + { + if (offset < 0 || offset > size) + { + throw new InvalidOperationException("Illegal position: " + offset); + } + int lowBoundary = 1; + int highBoundary = linestart.Length - 1; + while (true) + { + if ((highBoundary - lowBoundary) <= 1) + { + if (linestart[highBoundary] > offset) + { + return lowBoundary; + } + return highBoundary; + } + int medium = lowBoundary + ((highBoundary - lowBoundary) >> 1); + if (linestart[medium] > offset) + { + highBoundary = medium; + } + else + { + lowBoundary = medium; + } + } + } + + internal Location GetLocation(int offset) + { + int line = GetLineAt(offset); + int column = offset - linestart[line] + 1; + return new Location(file, line, column); + } + + internal int Size => size; + + public override int GetHashCode() + { + var hc = new HashCode(); + foreach (int x in linestart) + { + hc.Add(x); + } + hc.Add(file); + hc.Add(size); + return hc.ToHashCode(); + } + + public override bool Equals(object? other) + { + if (other is not FileLocations that) + { + return false; + } + return this.size == that.size + && this.linestart.AsSpan().SequenceEqual(that.linestart) + && this.file == that.file; + } + + private static int[] ComputeLinestart(char[] buffer) + { + // Compute the size. + int size = 2; + for (int i = 0; i < buffer.Length; i++) + { + if (buffer[i] == '\n') + { + size++; + } + } + int[] linestart = new int[size]; + + int index = 0; + linestart[index++] = 0; // The 0th line does not exist. + linestart[index++] = 0; // The first line ("line 1") starts at offset 0. + + for (int i = 0; i < buffer.Length; i++) + { + if (buffer[i] == '\n') + { + linestart[index++] = i + 1; + } + } + return linestart; + } +} diff --git a/src/Starlark/Syntax/FileOptions.cs b/src/Starlark/Syntax/FileOptions.cs new file mode 100644 index 000000000..f4062329e --- /dev/null +++ b/src/Starlark/Syntax/FileOptions.cs @@ -0,0 +1,162 @@ +// Copyright 2020 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// FileOptions is a set of options that affect the static processing---scanning, parsing, validation +/// (identifier resolution), and compilation---of a single Starlark file. +/// +/// The options represent the desired behavior for new uses of Starlark. +/// +public sealed class FileOptions +{ + private FileOptions( + bool allowLoadPrivateSymbols, + bool allowToplevelRebinding, + bool loadBindsGlobally, + bool requireLoadStatementsFirst, + bool stringLiteralsAreAsciiOnly, + bool allowTypeSyntax, + bool resolveTypeSyntax, + bool tolerateInvalidTypeExpressions, + bool staticTypeChecking) + { + AllowLoadPrivateSymbols = allowLoadPrivateSymbols; + AllowToplevelRebinding = allowToplevelRebinding; + LoadBindsGlobally = loadBindsGlobally; + RequireLoadStatementsFirst = requireLoadStatementsFirst; + StringLiteralsAreAsciiOnly = stringLiteralsAreAsciiOnly; + AllowTypeSyntax = allowTypeSyntax; + ResolveTypeSyntax = resolveTypeSyntax; + TolerateInvalidTypeExpressions = tolerateInvalidTypeExpressions; + StaticTypeChecking = staticTypeChecking; + } + + /// The default options for Starlark static processing. + public static readonly FileOptions DEFAULT = Builder().Build(); + + /// During resolution, permit load statements to access private names such as _x. + public bool AllowLoadPrivateSymbols { get; } + + /// + /// During resolution, permit multiple assignments to a given top-level binding. + /// + public bool AllowToplevelRebinding { get; } + + /// + /// During resolution, make load statements bind global variables of the module. + /// + public bool LoadBindsGlobally { get; } + + /// During resolution, require load statements to appear before other kinds of statements. + public bool RequireLoadStatementsFirst { get; } + + /// During lexing, whether to ban non-ASCII characters in string literals. + public bool StringLiteralsAreAsciiOnly { get; } + + /// Whether type annotations and related syntax are allowed in the source code. + public bool AllowTypeSyntax { get; } + + /// Whether type annotations are processed by the resolver. + public bool ResolveTypeSyntax { get; } + + /// + /// If true, type expressions in annotations and type declarations may be any valid + /// expression (except for unparenthesized tuples). + /// + public bool TolerateInvalidTypeExpressions { get; } + + /// Whether to perform static type checking. + public bool StaticTypeChecking { get; } + + public static FileOptionsBuilder Builder() + { + // These are the DEFAULT values. + return new FileOptionsBuilder() + .AllowLoadPrivateSymbols(false) + .AllowToplevelRebinding(false) + .LoadBindsGlobally(false) + .RequireLoadStatementsFirst(true) + .StringLiteralsAreAsciiOnly(false) + .AllowTypeSyntax(false) + .ResolveTypeSyntax(false) + .TolerateInvalidTypeExpressions(false) + .StaticTypeChecking(false); + } + + public FileOptionsBuilder ToBuilder() + { + return new FileOptionsBuilder() + .AllowLoadPrivateSymbols(AllowLoadPrivateSymbols) + .AllowToplevelRebinding(AllowToplevelRebinding) + .LoadBindsGlobally(LoadBindsGlobally) + .RequireLoadStatementsFirst(RequireLoadStatementsFirst) + .StringLiteralsAreAsciiOnly(StringLiteralsAreAsciiOnly) + .AllowTypeSyntax(AllowTypeSyntax) + .ResolveTypeSyntax(ResolveTypeSyntax) + .TolerateInvalidTypeExpressions(TolerateInvalidTypeExpressions) + .StaticTypeChecking(StaticTypeChecking); + } + + /// Builder for . + public sealed class FileOptionsBuilder + { + private bool allowLoadPrivateSymbols; + private bool allowToplevelRebinding; + private bool loadBindsGlobally; + private bool requireLoadStatementsFirst; + private bool stringLiteralsAreAsciiOnly; + private bool allowTypeSyntax; + private bool resolveTypeSyntax; + private bool tolerateInvalidTypeExpressions; + private bool staticTypeChecking; + + public FileOptionsBuilder AllowLoadPrivateSymbols(bool value) { allowLoadPrivateSymbols = value; return this; } + public FileOptionsBuilder AllowToplevelRebinding(bool value) { allowToplevelRebinding = value; return this; } + public FileOptionsBuilder LoadBindsGlobally(bool value) { loadBindsGlobally = value; return this; } + public FileOptionsBuilder RequireLoadStatementsFirst(bool value) { requireLoadStatementsFirst = value; return this; } + public FileOptionsBuilder StringLiteralsAreAsciiOnly(bool value) { stringLiteralsAreAsciiOnly = value; return this; } + public FileOptionsBuilder AllowTypeSyntax(bool value) { allowTypeSyntax = value; return this; } + public FileOptionsBuilder ResolveTypeSyntax(bool value) { resolveTypeSyntax = value; return this; } + public FileOptionsBuilder TolerateInvalidTypeExpressions(bool value) { tolerateInvalidTypeExpressions = value; return this; } + public FileOptionsBuilder StaticTypeChecking(bool value) { staticTypeChecking = value; return this; } + + public FileOptions Build() + { + var options = new FileOptions( + allowLoadPrivateSymbols, + allowToplevelRebinding, + loadBindsGlobally, + requireLoadStatementsFirst, + stringLiteralsAreAsciiOnly, + allowTypeSyntax, + resolveTypeSyntax, + tolerateInvalidTypeExpressions, + staticTypeChecking); + if (options.StaticTypeChecking) + { + if (!options.ResolveTypeSyntax) + { + throw new ArgumentException("staticTypeChecking requires that resolveTypeSyntax is set"); + } + if (options.TolerateInvalidTypeExpressions) + { + throw new ArgumentException("staticTypeChecking requires that tolerateInvalidTypeExpressions is not set"); + } + } + return options; + } + } +} diff --git a/src/Starlark/Syntax/FloatLiteral.cs b/src/Starlark/Syntax/FloatLiteral.cs new file mode 100644 index 000000000..06b0cc5b5 --- /dev/null +++ b/src/Starlark/Syntax/FloatLiteral.cs @@ -0,0 +1,46 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// Syntax node for a non-negative float literal. (Negative floats are parsed as a +/// operating on a positive argument.) +/// +public sealed class FloatLiteral : Expression +{ + private readonly string raw; + private readonly int tokenOffset; + private readonly double value; + + internal FloatLiteral(FileLocations locs, string raw, int tokenOffset, double value) + : base(locs, ExpressionKind.FLOAT_LITERAL) + { + this.raw = raw; + this.tokenOffset = tokenOffset; + this.value = value; + } + + /// Returns the value denoted by this literal. + public double GetValue() => value; + + /// Returns the raw source text of the literal. + public string GetRaw() => raw; + + public override int GetStartOffset() => tokenOffset; + + public override int GetEndOffset() => tokenOffset + raw.Length; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/Identifier.cs b/src/Starlark/Syntax/Identifier.cs new file mode 100644 index 000000000..889920c03 --- /dev/null +++ b/src/Starlark/Syntax/Identifier.cs @@ -0,0 +1,107 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// Syntax node for an identifier. +public sealed class Identifier : Expression +{ + private readonly string name; + private readonly int nameOffset; + + // Set by Resolver if applicable. + private Resolver.Binding? binding; + + internal Identifier(FileLocations locs, string name, int nameOffset) + : base(locs, ExpressionKind.IDENTIFIER) + { + this.name = name; + this.nameOffset = nameOffset; + } + + public override int GetStartOffset() => nameOffset; + + public override int GetEndOffset() => nameOffset + name.Length; + + /// Returns the name of the Identifier. + public string GetName() => name; + + public bool IsPrivate() => name.StartsWith("_", StringComparison.Ordinal); + + /// + /// Returns information about the binding (symbol) that the identifier refers to. Set by the + /// resolver. May be null. + /// + public Resolver.Binding? GetBinding() => binding; + + internal void SetBinding(Resolver.Binding bind) + { + if (this.binding != null) + { + throw new InvalidOperationException("binding already set"); + } + this.binding = bind; + } + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + + /// Reports whether the string is a valid identifier. + public static bool IsValid(string name) + { + // Keep consistent with Lexer.ScanIdentifier. + for (int i = 0; i < name.Length; i++) + { + char c = name[i]; + if (!(('a' <= c && c <= 'z') + || ('A' <= c && c <= 'Z') + || (i > 0 && '0' <= c && c <= '9') + || (c == '_'))) + { + return false; + } + } + return name.Length != 0; + } + + /// Returns all names bound by an LHS expression. + public static ImmutableHashSet BoundIdentifiers(Expression expr) + { + if (expr is Identifier id) + { + return ImmutableHashSet.Create(id); + } + var result = ImmutableHashSet.CreateBuilder(); + CollectBoundIdentifiers(expr, result); + return result.ToImmutable(); + } + + private static void CollectBoundIdentifiers( + Expression lhs, ImmutableHashSet.Builder result) + { + if (lhs is Identifier id) + { + result.Add(id); + return; + } + if (lhs is ListExpression variables) + { + foreach (Expression expression in variables.GetElements()) + { + CollectBoundIdentifiers(expression, result); + } + } + } +} diff --git a/src/Starlark/Syntax/IndexExpression.cs b/src/Starlark/Syntax/IndexExpression.cs new file mode 100644 index 000000000..b85fb65a9 --- /dev/null +++ b/src/Starlark/Syntax/IndexExpression.cs @@ -0,0 +1,49 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// An index expression (obj[field]). Not to be confused with a slice expression +/// (obj[from:to]). +/// +public sealed class IndexExpression : Expression +{ + private readonly Expression obj; + private readonly int lbracketOffset; + private readonly Expression key; + private readonly int rbracketOffset; + + internal IndexExpression( + FileLocations locs, Expression obj, int lbracketOffset, Expression key, int rbracketOffset) + : base(locs, ExpressionKind.INDEX) + { + this.obj = obj; + this.lbracketOffset = lbracketOffset; + this.key = key; + this.rbracketOffset = rbracketOffset; + } + + public Expression GetObject() => obj; + + public Expression GetKey() => key; + + public override int GetStartOffset() => obj.GetStartOffset(); + + public override int GetEndOffset() => rbracketOffset + 1; + + public Location GetLbracketLocation() => Locs.GetLocation(lbracketOffset); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/IntLiteral.cs b/src/Starlark/Syntax/IntLiteral.cs new file mode 100644 index 000000000..72a592970 --- /dev/null +++ b/src/Starlark/Syntax/IntLiteral.cs @@ -0,0 +1,181 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Globalization; +using System.Numerics; + +namespace Starlark.Syntax; + +/// +/// Syntax node for a non-negative int literal. (Negative integers are parsed as a +/// operating on a positive argument.) +/// +public sealed class IntLiteral : Expression +{ + private readonly string raw; + private readonly int tokenOffset; + private readonly object value; // = int | long | BigInteger + + /// + /// Constructs an IntLiteral. must be either an int or long or + /// BigInteger, and the smallest type capable of exactly representing the number must be used. + /// + internal IntLiteral(FileLocations locs, string raw, int tokenOffset, object value) + : base(locs, ExpressionKind.INT_LITERAL) + { + this.raw = raw; + this.tokenOffset = tokenOffset; + this.value = value; + } + + /// + /// Returns the value denoted by this literal as an int, long, or BigInteger, using the narrowest + /// type capable of exactly representing the value. + /// + public object GetValue() => value; + + /// + /// Returns the value denoted by this literal as an int, or null if it can't be represented + /// exactly. + /// + public int? GetIntValueExact() => value is int intValue ? intValue : null; + + /// Returns the raw source text of the literal. + public string GetRaw() => raw; + + public override int GetStartOffset() => tokenOffset; + + public override int GetEndOffset() => tokenOffset + raw.Length; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + + /// + /// Returns the value denoted by a non-negative integer literal with an optional base prefix (but + /// no +/- sign), using the narrowest type of int, long, or BigInteger capable of exactly + /// representing the value. + /// + /// if the string is not a valid literal. + public static object Scan(string str) + { + string orig = str; + int radix = 10; + if (str.Length > 1 && str[0] == '0') + { + switch (str[1]) + { + case 'x': + case 'X': + radix = 16; + str = str.Substring(2); + break; + case 'o': + case 'O': + radix = 8; + str = str.Substring(2); + break; + default: + throw new FormatException( + "invalid octal literal: " + str + " (use '0o" + str.Substring(1) + "')"); + } + } + + if (TryParseLong(str, radix, out long v)) + { + if (v == (int)v) + { + return (int)v; + } + return v; + } + if (TryParseBigInteger(str, radix, out BigInteger big)) + { + return big; + } + throw new FormatException("invalid base-" + radix + " integer literal: " + orig); + } + + private static bool TryParseLong(string str, int radix, out long result) + { + result = 0; + if (str.Length == 0) + { + return false; + } + if (radix == 10) + { + return long.TryParse(str, NumberStyles.None, CultureInfo.InvariantCulture, out result); + } + try + { + long acc = 0; + checked + { + foreach (char c in str) + { + int digit = DigitValue(c); + if (digit < 0 || digit >= radix) + { + return false; + } + acc = acc * radix + digit; + } + } + result = acc; + return true; + } + catch (OverflowException) + { + return false; + } + } + + private static bool TryParseBigInteger(string str, int radix, out BigInteger result) + { + result = BigInteger.Zero; + if (str.Length == 0) + { + return false; + } + BigInteger acc = BigInteger.Zero; + BigInteger b = radix; + foreach (char c in str) + { + int digit = DigitValue(c); + if (digit < 0 || digit >= radix) + { + return false; + } + acc = acc * b + digit; + } + result = acc; + return true; + } + + private static int DigitValue(char c) + { + if (c >= '0' && c <= '9') + { + return c - '0'; + } + if (c >= 'a' && c <= 'z') + { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'Z') + { + return c - 'A' + 10; + } + return -1; + } +} diff --git a/src/Starlark/Syntax/ListExpression.cs b/src/Starlark/Syntax/ListExpression.cs new file mode 100644 index 000000000..8868c8879 --- /dev/null +++ b/src/Starlark/Syntax/ListExpression.cs @@ -0,0 +1,96 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Text; + +namespace Starlark.Syntax; + +/// Syntax node for list and tuple expressions. +public sealed class ListExpression : Expression +{ + private readonly bool isTuple; + private readonly int lbracketOffset; // -1 => unparenthesized non-empty tuple + private readonly ImmutableArray elements; + private readonly int rbracketOffset; // -1 => unparenthesized non-empty tuple + + internal ListExpression( + FileLocations locs, + bool isTuple, + int lbracketOffset, + ImmutableArray elements, + int rbracketOffset) + : base(locs, ExpressionKind.LIST_EXPR) + { + // An unparenthesized tuple must be non-empty. + if (elements.IsEmpty && !(lbracketOffset >= 0 && rbracketOffset >= 0)) + { + throw new ArgumentException("empty unparenthesized tuple"); + } + this.lbracketOffset = lbracketOffset; + this.isTuple = isTuple; + this.elements = elements; + this.rbracketOffset = rbracketOffset; + } + + public IReadOnlyList GetElements() => elements; + + /// Reports whether this is a tuple expression. + public bool IsTuple() => isTuple; + + public override int GetStartOffset() => + lbracketOffset < 0 ? elements[0].GetStartOffset() : lbracketOffset; + + public override int GetEndOffset() => + rbracketOffset < 0 ? elements[elements.Length - 1].GetEndOffset() : rbracketOffset + 1; + + public override string ToString() + { + var buf = new StringBuilder(); + buf.Append(IsTuple() ? '(' : '['); + AppendNodes(buf, elements); + if (IsTuple() && elements.Length == 1) + { + buf.Append(','); + } + buf.Append(IsTuple() ? ')' : ']'); + return buf.ToString(); + } + + // Appends elements to buf, comma-separated, abbreviating if they are numerous or long. + // (Also used by CallExpression, TypeApplication, TypeAliasStatement.) + internal static void AppendNodes(StringBuilder buf, IReadOnlyList elements) where T : Node + { + int n = elements.Count; + for (int i = 0; i < n; i++) + { + if (i > 0) + { + buf.Append(", "); + } + int mark = buf.Length; + buf.Append(elements[i]); + // Abbreviate, dropping this element, if we exceed 32 chars, + // or 4 elements (with more elements following). + if (buf.Length >= 32 || (i == 4 && i + 1 < n)) + { + buf.Length = mark; + buf.Append(string.Format("+{0} more", n - i)); + break; + } + } + } + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/Location.cs b/src/Starlark/Syntax/Location.cs new file mode 100644 index 000000000..04add1491 --- /dev/null +++ b/src/Starlark/Syntax/Location.cs @@ -0,0 +1,115 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text; + +namespace Starlark.Syntax; + +/// +/// A Location denotes a position within a Starlark file. +/// +/// A location is a triple (file, line, column), where file is the apparent name +/// of the file, line is the optional 1-based line number, and column is the optional +/// 1-based column number measured in UTF-16 code units. If the column is zero it is not displayed. +/// If the line number is also zero, it too is not displayed; in this case, the location denotes the +/// file as a whole. +/// +public sealed class Location : IComparable +{ + private readonly string file; + private readonly int line; + private readonly int column; + + public Location(string file, int line, int column) + { + this.file = file ?? throw new ArgumentNullException(nameof(file)); + this.line = line; + this.column = column; + } + + /// Returns the name of the file containing this location. + public string File => file; + + /// Returns the line number of this location. + public int Line => line; + + /// Returns the column number of this location. + public int Column => column; + + /// + /// Returns a Location for the given file, line and column. If column is non-zero, + /// line too must be non-zero. + /// + public static Location FromFileLineColumn(string file, int line, int column) + { + if (line == 0 && column != 0) + { + throw new ArgumentException("non-zero column but no line number"); + } + return new Location(file, line, column); + } + + /// Returns a Location for the file as a whole. + public static Location FromFile(string file) => new(file, 0, 0); + + /// + /// Formats the location as "file:line:col". If the column is zero, it is omitted. If the + /// line is also zero, it too is omitted. + /// + public override string ToString() + { + var buf = new StringBuilder(); + buf.Append(file); + if (line != 0) + { + buf.Append(':').Append(line); + if (column != 0) + { + buf.Append(':').Append(column); + } + } + return buf.ToString(); + } + + /// Returns a three-valued lexicographical comparison of two Locations. + public int CompareTo(Location? that) + { + if (that is null) + { + return 1; + } + int cmp = string.CompareOrdinal(this.file, that.file); + if (cmp != 0) + { + return cmp; + } + long a = ((long)this.line << 32) | (uint)this.column; + long b = ((long)that.line << 32) | (uint)that.column; + return a.CompareTo(b); + } + + public override int GetHashCode() => 97 * file.GetHashCode() + 37 * line + column; + + public override bool Equals(object? that) + { + return ReferenceEquals(this, that) + || (that is Location other + && this.file == other.file + && this.line == other.line + && this.column == other.column); + } + + /// A location for built-in functions. + public static readonly Location BUILTIN = FromFile(""); +} diff --git a/src/Starlark/Syntax/Node.cs b/src/Starlark/Syntax/Node.cs new file mode 100644 index 000000000..fcaea7557 --- /dev/null +++ b/src/Starlark/Syntax/Node.cs @@ -0,0 +1,74 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text; + +namespace Starlark.Syntax; + +/// +/// A Node is a node in a Starlark syntax tree. +/// +/// Nodes may be constructed only by the parser. +/// +public abstract class Node +{ + // The FileLocations table holds the file name and a compressed + // mapping from token char offsets to Locations. + // It is shared by all nodes from the same file. + internal readonly FileLocations Locs; + + internal Node(FileLocations locs) + { + Locs = locs ?? throw new ArgumentNullException(nameof(locs)); + } + + /// + /// Returns the node's start offset, as a char index (zero-based count of UTF-16 codes) from the + /// start of the file. + /// + public abstract int GetStartOffset(); + + /// Returns the location of the start of this syntax node. + public Location GetStartLocation() => Locs.GetLocation(GetStartOffset()); + + /// Returns the char offset of the source position immediately after this syntax node. + public abstract int GetEndOffset(); + + /// Returns the location of the source position immediately after this syntax node. + public Location GetEndLocation() => Locs.GetLocation(GetEndOffset()); + + /// + /// Returns a pretty-printed representation of this syntax tree. + /// + public string PrettyPrint() + { + var buf = new StringBuilder(); + new NodePrinter(buf).PrintNode(this); + return buf.ToString(); + } + + /// + /// Print the syntax node in a form useful for debugging. + /// + public override string ToString() + { + return PrettyPrint(); // default behavior, overridden in several subclasses + } + + /// + /// Implements the double dispatch by calling into the node specific Visit method of the + /// . + /// + public abstract void Accept(NodeVisitor visitor); +} diff --git a/src/Starlark/Syntax/Parameter.cs b/src/Starlark/Syntax/Parameter.cs new file mode 100644 index 000000000..fa9051f65 --- /dev/null +++ b/src/Starlark/Syntax/Parameter.cs @@ -0,0 +1,128 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// Syntax node for a parameter in a function definition. +/// +/// Parameters may be of four forms, as in def f(a, b=c, *args, **kwargs). They are +/// represented by the subclasses Mandatory, Optional, Star, and StarStar. +/// +public abstract class Parameter : Node +{ + // Null in the case of a bare * parameter, non-null for any other case. + private readonly Identifier? id; + + private readonly Expression? type; + + private protected Parameter(FileLocations locs, Identifier? id, Expression? type) + : base(locs) + { + this.id = id; + this.type = type; + } + + public string? GetName() => id?.GetName(); + + public Identifier? GetIdentifier() => id; + + public virtual Expression? GetDefaultValue() => null; + + public Expression? GetType() => type; + + /// + /// Syntax node for a mandatory parameter, f(id). It may be positional or keyword-only + /// depending on its position. + /// + public sealed class Mandatory : Parameter + { + internal Mandatory(FileLocations locs, Identifier id, Expression? type) + : base(locs, id, type) + { + } + + public override int GetStartOffset() => GetIdentifier()!.GetStartOffset(); + + public override int GetEndOffset() => + GetType() != null ? GetType()!.GetEndOffset() : GetIdentifier()!.GetEndOffset(); + } + + /// + /// Syntax node for an optional parameter, f(id=expr). It may be positional or keyword-only + /// depending on its position. + /// + public sealed class Optional : Parameter + { + public readonly Expression defaultValue; + + internal Optional(FileLocations locs, Identifier id, Expression? type, Expression defaultValue) + : base(locs, id, type) + { + this.defaultValue = defaultValue; + } + + public override Expression? GetDefaultValue() => defaultValue; + + public override int GetStartOffset() => GetIdentifier()!.GetStartOffset(); + + public override int GetEndOffset() => GetDefaultValue()!.GetEndOffset(); + + public override string ToString() => GetName() + "=" + defaultValue; + } + + /// Syntax node for a star parameter, f(*id) or f(..., *, ...). + public sealed class Star : Parameter + { + private readonly int starOffset; + + internal Star(FileLocations locs, int starOffset, Identifier? id, Expression? type) + : base(locs, id, type) + { + if (id == null && type != null) + { + throw new ArgumentException("Star parameter without id cannot have a type"); + } + this.starOffset = starOffset; + } + + public override int GetStartOffset() => starOffset; + + public override int GetEndOffset() => + GetType() != null ? GetType()!.GetEndOffset() : GetIdentifier()!.GetEndOffset(); + } + + /// Syntax node for a parameter of the form f(**id). + public sealed class StarStar : Parameter + { + private readonly int starStarOffset; + + internal StarStar(FileLocations locs, int starStarOffset, Identifier id, Expression? type) + : base(locs, id, type) + { + this.starStarOffset = starStarOffset; + } + + public override int GetStartOffset() => starStarOffset; + + public override int GetEndOffset() => + GetType() != null ? GetType()!.GetEndOffset() : GetIdentifier()!.GetEndOffset(); + } + + public override void Accept(NodeVisitor visitor) + { + // All Parameter subclasses dispatch to NodeVisitor.Visit(Parameter). + visitor.Visit(this); + } +} diff --git a/src/Starlark/Syntax/ParserInput.cs b/src/Starlark/Syntax/ParserInput.cs new file mode 100644 index 000000000..2cb6ab751 --- /dev/null +++ b/src/Starlark/Syntax/ParserInput.cs @@ -0,0 +1,96 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text; + +namespace Starlark.Syntax; + +/// +/// The apparent name and contents of a source file, for consumption by the parser. The file name +/// appears in the location information in the syntax tree, and in error messages, but the Starlark +/// interpreter will not attempt to open the file. +/// +/// The parser consumes a stream of chars (UTF-16 codes), and the syntax positions reported by +/// Node.GetStartOffset and Location.Column are effectively indices into a char +/// array. +/// +public sealed class ParserInput +{ + private readonly string file; + private readonly char[] content; + + private ParserInput(char[] content, string file) + { + this.content = content; + this.file = file ?? throw new ArgumentNullException(nameof(file)); + } + + /// Returns the content of the input source. Callers must not modify the result. + internal char[] GetContent() => content; + + /// Returns the apparent file name of the input source. + public string GetFile() => file; + + /// + /// Returns an input source that uses the name and content of the specified UTF-8-encoded text + /// file. + /// + public static ParserInput ReadFile(string file) + { + byte[] utf8 = System.IO.File.ReadAllBytes(file); + return FromUTF8(utf8, file); + } + + /// Returns an unnamed input source that reads from a list of strings, joined by newlines. + public static ParserInput FromLines(params string[] lines) + { + return FromString(string.Join("\n", lines), ""); + } + + /// Returns an input source that reads from a UTF-8-encoded byte array. + public static ParserInput FromUTF8(byte[] bytes, string file) + { + string s = Encoding.UTF8.GetString(bytes); + return FromCharArray(s.ToCharArray(), file); + } + + /// + /// Returns an input source that reads from a Latin1-encoded byte array. + /// + [Obsolete("This function exists to support legacy uses of Latin1 in Bazel. Do not use Latin1 in new applications.")] + public static ParserInput FromLatin1(byte[] bytes, string file) + { + char[] chars = new char[bytes.Length]; + for (int i = 0; i < bytes.Length; i++) + { + chars[i] = (char)(0xff & bytes[i]); + } + return new ParserInput(chars, file); + } + + /// Returns an input source that reads from the given string. + public static ParserInput FromString(string content, string file) + { + return FromCharArray(content.ToCharArray(), file); + } + + /// + /// Returns an input source that reads from the given char array. The caller must not subsequently + /// modify the array. + /// + public static ParserInput FromCharArray(char[] content, string file) + { + return new ParserInput(content, file); + } +} diff --git a/src/Starlark/Syntax/SliceExpression.cs b/src/Starlark/Syntax/SliceExpression.cs new file mode 100644 index 000000000..24b694407 --- /dev/null +++ b/src/Starlark/Syntax/SliceExpression.cs @@ -0,0 +1,60 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Syntax node for a slice expression, object[start:stop:step]. +public sealed class SliceExpression : Expression +{ + private readonly Expression obj; + private readonly int lbracketOffset; + private readonly Expression? start; + private readonly Expression? stop; + private readonly Expression? step; + private readonly int rbracketOffset; + + internal SliceExpression( + FileLocations locs, + Expression obj, + int lbracketOffset, + Expression? start, + Expression? stop, + Expression? step, + int rbracketOffset) + : base(locs, ExpressionKind.SLICE) + { + this.obj = obj; + this.lbracketOffset = lbracketOffset; + this.start = start; + this.stop = stop; + this.step = step; + this.rbracketOffset = rbracketOffset; + } + + public Expression GetObject() => obj; + + public Expression? GetStart() => start; + + public Expression? GetStop() => stop; + + public Expression? GetStep() => step; + + public override int GetStartOffset() => obj.GetStartOffset(); + + public override int GetEndOffset() => rbracketOffset + 1; + + public Location GetLbracketLocation() => Locs.GetLocation(lbracketOffset); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/Statement.cs b/src/Starlark/Syntax/Statement.cs new file mode 100644 index 000000000..b15f38656 --- /dev/null +++ b/src/Starlark/Syntax/Statement.cs @@ -0,0 +1,45 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Base class for all statements nodes in the AST. +public abstract class Statement : Node +{ + /// Kind of the statement. + public enum StatementKind + { + ASSIGNMENT, + EXPRESSION, + FLOW, + FOR, + DEF, + IF, + LOAD, + RETURN, + TYPE_ALIAS, + VAR, + } + + private readonly StatementKind kind; + + internal Statement(FileLocations locs, StatementKind kind) + : base(locs) + { + this.kind = kind; + } + + /// Kind of the statement. + public StatementKind Kind => kind; +} diff --git a/src/Starlark/Syntax/StringLiteral.cs b/src/Starlark/Syntax/StringLiteral.cs new file mode 100644 index 000000000..61eb1e758 --- /dev/null +++ b/src/Starlark/Syntax/StringLiteral.cs @@ -0,0 +1,72 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Syntax node for a string literal. +public sealed class StringLiteral : Expression +{ + private readonly int startOffset; + private readonly string value; + private readonly int endOffset; + + internal StringLiteral(FileLocations locs, int startOffset, string value, int endOffset) + : base(locs, ExpressionKind.STRING_LITERAL) + { + this.startOffset = startOffset; + this.value = value; + this.endOffset = endOffset; + } + + /// Returns the value denoted by the string literal. + public string GetValue() => value; + + public Location GetLocation() => Locs.GetLocation(startOffset); + + public override int GetStartOffset() => startOffset; + + public override int GetEndOffset() => endOffset; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + + /// Returns an opaque serializable object that may be passed to . + public object GetFileLocations() => Locs; + + /// + /// Returns the value denoted by the Starlark string literal within . + /// + /// if s does not contain a valid string literal. + public static string Unquote(string s) + { + var errors = new List(); + var lexer = new Lexer(ParserInput.FromLines(s), errors, FileOptions.DEFAULT); + lexer.NextToken(); + if (errors.Count != 0) + { + throw new ArgumentException(errors[0].Message); + } + if (lexer.Start != 0 || lexer.End != s.Length || lexer.Kind != TokenKind.STRING) + { + throw new ArgumentException("invalid syntax"); + } + return (string)lexer.Value!; + } + + /// Constructs a StringLiteral from its serialized components. + public static StringLiteral FromSerialization( + object fileLocations, int startOffset, string value, int endOffset) + { + return new StringLiteral((FileLocations)fileLocations, startOffset, value, endOffset); + } +} diff --git a/src/Starlark/Syntax/SyntaxError.cs b/src/Starlark/Syntax/SyntaxError.cs new file mode 100644 index 000000000..c6ddf75d9 --- /dev/null +++ b/src/Starlark/Syntax/SyntaxError.cs @@ -0,0 +1,82 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// +/// A SyntaxError represents a static error associated with the syntax, such as a scanner or parse +/// error, a structural problem, or a failure of identifier resolution. It records a description of +/// the error and its location in the syntax. +/// +public sealed class SyntaxError +{ + private readonly Location location; + private readonly string message; + + public SyntaxError(Location location, string message) + { + this.location = location ?? throw new ArgumentNullException(nameof(location)); + this.message = message ?? throw new ArgumentNullException(nameof(message)); + } + + /// Returns the location of the error. + public Location Location => location; + + /// Returns a description of the error. + public string Message => message; + + /// Returns a string of the form "foo.star:1:2: oops". + public override string ToString() => location + ": " + message; + + /// + /// A SyntaxError.Exception is an exception holding one or more syntax errors. + /// + /// SyntaxError.Exception is thrown by operations such as Expression.Parse, which are + /// "all or nothing". By contrast, StarlarkFile.Parse does not throw an exception; instead, + /// it records the accumulated scanner, parser, and optionally validation errors within the syntax + /// tree, so that clients may obtain partial information from a damaged file. + /// + public sealed class Exception : System.Exception + { + private readonly ImmutableArray errors; + + /// Construct a SyntaxError from a non-empty list of errors. + public Exception(IReadOnlyList errors) + { + if (errors.Count == 0) + { + throw new ArgumentException("no errors"); + } + this.errors = errors.ToImmutableArray(); + } + + /// Returns an immutable non-empty list of errors. + public IReadOnlyList Errors => errors; + + public override string Message + { + get + { + string first = errors[0].Message; + if (errors.Length > 1) + { + return string.Format("{0} (+ {1} more)", first, errors.Length - 1); + } + return first; + } + } + } +} diff --git a/src/Starlark/Syntax/TokenKind.cs b/src/Starlark/Syntax/TokenKind.cs new file mode 100644 index 000000000..366f0b278 --- /dev/null +++ b/src/Starlark/Syntax/TokenKind.cs @@ -0,0 +1,223 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// A TokenKind represents the kind of a lexical token. Ported from the Java enum, which associated +/// a human-readable name with each value. The name is preserved via and +/// . +/// +public enum TokenKind +{ + AMPERSAND, + AMPERSAND_EQUALS, + AND, + AS, + ASSERT, + BREAK, + CARET, + CARET_EQUALS, + /// Emitted only if --experimental_starlark_type_syntax is enabled. + CAST, + CLASS, + COLON, + COMMA, + CONTINUE, + DEF, + DEL, + /// A multiline block of Sphinx autodoc-style doc comments. + DOC_COMMENT_BLOCK, + /// Inline trailing doc comment. + DOC_COMMENT_TRAILING, + DOT, + ELIF, + /// Valid only in type expressions. + ELLIPSIS, + ELSE, + EOF, + EQUALS, + EQUALS_EQUALS, + EXCEPT, + FINALLY, + FLOAT, + FOR, + FROM, + GLOBAL, + GREATER, + GREATER_EQUALS, + GREATER_GREATER, + GREATER_GREATER_EQUALS, + IDENTIFIER, + IF, + ILLEGAL, + IMPORT, + IN, + INDENT, + INT, + IS, + /// Emitted only if --experimental_starlark_type_syntax is enabled. + ISINSTANCE, + LAMBDA, + LBRACE, + LBRACKET, + LESS, + LESS_EQUALS, + LESS_LESS, + LESS_LESS_EQUALS, + LOAD, + LPAREN, + MINUS, + MINUS_EQUALS, + NEWLINE, + NONLOCAL, + NOT, + NOT_EQUALS, + NOT_IN, + OR, + OUTDENT, + PASS, + PERCENT, + PERCENT_EQUALS, + PIPE, + PIPE_EQUALS, + PLUS, + PLUS_EQUALS, + RAISE, + RARROW, + RBRACE, + RBRACKET, + RETURN, + RPAREN, + SEMI, + SLASH, + SLASH_EQUALS, + SLASH_SLASH, + SLASH_SLASH_EQUALS, + STAR, + STAR_EQUALS, + STAR_STAR, + STRING, + TILDE, + TRY, + WHILE, + WITH, + YIELD, +} + +/// Helpers for preserving the Java enum's display names. +public static class TokenKinds +{ + private static readonly string[] Names = BuildNames(); + + /// Returns the human-readable name of a token kind (equivalent to Java's toString()). + public static string ToDisplayString(this TokenKind kind) => Names[(int)kind]; + + /// Returns the human-readable name of a token kind. + public static string Name(TokenKind kind) => Names[(int)kind]; + + private static string[] BuildNames() + { + var names = new string[(int)TokenKind.YIELD + 1]; + names[(int)TokenKind.AMPERSAND] = "&"; + names[(int)TokenKind.AMPERSAND_EQUALS] = "&="; + names[(int)TokenKind.AND] = "and"; + names[(int)TokenKind.AS] = "as"; + names[(int)TokenKind.ASSERT] = "assert"; + names[(int)TokenKind.BREAK] = "break"; + names[(int)TokenKind.CARET] = "^"; + names[(int)TokenKind.CARET_EQUALS] = "^="; + names[(int)TokenKind.CAST] = "cast"; + names[(int)TokenKind.CLASS] = "class"; + names[(int)TokenKind.COLON] = ":"; + names[(int)TokenKind.COMMA] = ","; + names[(int)TokenKind.CONTINUE] = "continue"; + names[(int)TokenKind.DEF] = "def"; + names[(int)TokenKind.DEL] = "del"; + names[(int)TokenKind.DOC_COMMENT_BLOCK] = "#:"; + names[(int)TokenKind.DOC_COMMENT_TRAILING] = "trailing #: "; + names[(int)TokenKind.DOT] = "."; + names[(int)TokenKind.ELIF] = "elif"; + names[(int)TokenKind.ELLIPSIS] = "..."; + names[(int)TokenKind.ELSE] = "else"; + names[(int)TokenKind.EOF] = "EOF"; + names[(int)TokenKind.EQUALS] = "="; + names[(int)TokenKind.EQUALS_EQUALS] = "=="; + names[(int)TokenKind.EXCEPT] = "except"; + names[(int)TokenKind.FINALLY] = "finally"; + names[(int)TokenKind.FLOAT] = "float literal"; + names[(int)TokenKind.FOR] = "for"; + names[(int)TokenKind.FROM] = "from"; + names[(int)TokenKind.GLOBAL] = "global"; + names[(int)TokenKind.GREATER] = ">"; + names[(int)TokenKind.GREATER_EQUALS] = ">="; + names[(int)TokenKind.GREATER_GREATER] = ">>"; + names[(int)TokenKind.GREATER_GREATER_EQUALS] = ">>="; + names[(int)TokenKind.IDENTIFIER] = "identifier"; + names[(int)TokenKind.IF] = "if"; + names[(int)TokenKind.ILLEGAL] = "illegal character"; + names[(int)TokenKind.IMPORT] = "import"; + names[(int)TokenKind.IN] = "in"; + names[(int)TokenKind.INDENT] = "indent"; + names[(int)TokenKind.INT] = "integer literal"; + names[(int)TokenKind.IS] = "is"; + names[(int)TokenKind.ISINSTANCE] = "isinstance"; + names[(int)TokenKind.LAMBDA] = "lambda"; + names[(int)TokenKind.LBRACE] = "{"; + names[(int)TokenKind.LBRACKET] = "["; + names[(int)TokenKind.LESS] = "<"; + names[(int)TokenKind.LESS_EQUALS] = "<="; + names[(int)TokenKind.LESS_LESS] = "<<"; + names[(int)TokenKind.LESS_LESS_EQUALS] = "<<="; + names[(int)TokenKind.LOAD] = "load"; + names[(int)TokenKind.LPAREN] = "("; + names[(int)TokenKind.MINUS] = "-"; + names[(int)TokenKind.MINUS_EQUALS] = "-="; + names[(int)TokenKind.NEWLINE] = "newline"; + names[(int)TokenKind.NONLOCAL] = "nonlocal"; + names[(int)TokenKind.NOT] = "not"; + names[(int)TokenKind.NOT_EQUALS] = "!="; + names[(int)TokenKind.NOT_IN] = "not in"; + names[(int)TokenKind.OR] = "or"; + names[(int)TokenKind.OUTDENT] = "outdent"; + names[(int)TokenKind.PASS] = "pass"; + names[(int)TokenKind.PERCENT] = "%"; + names[(int)TokenKind.PERCENT_EQUALS] = "%="; + names[(int)TokenKind.PIPE] = "|"; + names[(int)TokenKind.PIPE_EQUALS] = "|="; + names[(int)TokenKind.PLUS] = "+"; + names[(int)TokenKind.PLUS_EQUALS] = "+="; + names[(int)TokenKind.RAISE] = "raise"; + names[(int)TokenKind.RARROW] = "->"; + names[(int)TokenKind.RBRACE] = "}"; + names[(int)TokenKind.RBRACKET] = "]"; + names[(int)TokenKind.RETURN] = "return"; + names[(int)TokenKind.RPAREN] = ")"; + names[(int)TokenKind.SEMI] = ";"; + names[(int)TokenKind.SLASH] = "/"; + names[(int)TokenKind.SLASH_EQUALS] = "/="; + names[(int)TokenKind.SLASH_SLASH] = "//"; + names[(int)TokenKind.SLASH_SLASH_EQUALS] = "//="; + names[(int)TokenKind.STAR] = "*"; + names[(int)TokenKind.STAR_EQUALS] = "*="; + names[(int)TokenKind.STAR_STAR] = "**"; + names[(int)TokenKind.STRING] = "string literal"; + names[(int)TokenKind.TILDE] = "~"; + names[(int)TokenKind.TRY] = "try"; + names[(int)TokenKind.WHILE] = "while"; + names[(int)TokenKind.WITH] = "with"; + names[(int)TokenKind.YIELD] = "yield"; + return names; + } +} diff --git a/src/Starlark/Syntax/UnaryOperatorExpression.cs b/src/Starlark/Syntax/UnaryOperatorExpression.cs new file mode 100644 index 000000000..77ba9a6e7 --- /dev/null +++ b/src/Starlark/Syntax/UnaryOperatorExpression.cs @@ -0,0 +1,46 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// A UnaryOperatorExpression represents a unary operator expression, 'op x'. +public sealed class UnaryOperatorExpression : Expression +{ + private readonly TokenKind op; // NOT, TILDE, MINUS or PLUS + private readonly int opOffset; + private readonly Expression x; + + internal UnaryOperatorExpression(FileLocations locs, TokenKind op, int opOffset, Expression x) + : base(locs, ExpressionKind.UNARY_OPERATOR) + { + this.op = op; + this.opOffset = opOffset; + this.x = x; + } + + /// Returns the operator. + public TokenKind GetOperator() => op; + + public override int GetStartOffset() => opOffset; + + public override int GetEndOffset() => x.GetEndOffset(); + + /// Returns the operand. + public Expression GetX() => x; + + public override string ToString() => + (op == TokenKind.NOT ? "not " : op.ToDisplayString()) + x; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} From af9efd91ec079ec80fd81a5122d5c610e0e592b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 06:16:16 +0000 Subject: [PATCH 05/15] WIP: port util core (Glob, FileUtil, command runner) to C# Adds Copybara.Util: ExitCode, Glob subsystem, FileUtil, DirFactory, Identity, TablePrinter, and the process/command runner stack. Still WIP: Starlark interpreter (syntax + eval) in progress, so Copybara.Core does not build yet. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- src/Copybara.Core/Util/FileUtil.cs | 522 +++++++++++++++ src/Copybara.Core/Util/Glob.cs | 503 +++++++++++++++ src/Copybara.Core/Util/SequenceGlob.cs | 85 +++ src/Starlark/Eval/Dict.cs | 547 ++++++++++++++++ src/Starlark/Eval/EvalUtils.cs | 524 +++++++++++++++ src/Starlark/Eval/GuardedValue.cs | 31 + src/Starlark/Eval/Module.cs | 183 ++++++ src/Starlark/Eval/StarlarkCallable.cs | 55 ++ src/Starlark/Eval/StarlarkList.cs | 388 ++++++++++++ src/Starlark/Eval/StarlarkThread.cs | 216 +++++++ src/Starlark/Eval/Tuple.cs | 164 +++++ src/Starlark/Syntax/AssignmentStatement.cs | 90 +++ src/Starlark/Syntax/CastExpression.cs | 68 ++ src/Starlark/Syntax/Comprehension.cs | 121 ++++ src/Starlark/Syntax/DefStatement.cs | 80 +++ src/Starlark/Syntax/DocComments.cs | 51 ++ src/Starlark/Syntax/ExpressionStatement.cs | 35 + src/Starlark/Syntax/FlowStatement.cs | 39 ++ src/Starlark/Syntax/ForStatement.cs | 58 ++ src/Starlark/Syntax/IfStatement.cs | 66 ++ src/Starlark/Syntax/IsInstanceExpression.cs | 57 ++ src/Starlark/Syntax/LambdaExpression.cs | 52 ++ src/Starlark/Syntax/LoadStatement.cs | 69 ++ src/Starlark/Syntax/NodePrinter.cs | 670 ++++++++++++++++++++ src/Starlark/Syntax/NodeVisitor.cs | 321 ++++++++++ src/Starlark/Syntax/ReturnStatement.cs | 44 ++ src/Starlark/Syntax/SyntaxUtils.cs | 67 ++ src/Starlark/Syntax/TypeAliasStatement.cs | 69 ++ src/Starlark/Syntax/TypeApplication.cs | 60 ++ src/Starlark/Syntax/VarStatement.cs | 55 ++ 30 files changed, 5290 insertions(+) create mode 100644 src/Copybara.Core/Util/FileUtil.cs create mode 100644 src/Copybara.Core/Util/Glob.cs create mode 100644 src/Copybara.Core/Util/SequenceGlob.cs create mode 100644 src/Starlark/Eval/Dict.cs create mode 100644 src/Starlark/Eval/EvalUtils.cs create mode 100644 src/Starlark/Eval/GuardedValue.cs create mode 100644 src/Starlark/Eval/Module.cs create mode 100644 src/Starlark/Eval/StarlarkCallable.cs create mode 100644 src/Starlark/Eval/StarlarkList.cs create mode 100644 src/Starlark/Eval/StarlarkThread.cs create mode 100644 src/Starlark/Eval/Tuple.cs create mode 100644 src/Starlark/Syntax/AssignmentStatement.cs create mode 100644 src/Starlark/Syntax/CastExpression.cs create mode 100644 src/Starlark/Syntax/Comprehension.cs create mode 100644 src/Starlark/Syntax/DefStatement.cs create mode 100644 src/Starlark/Syntax/DocComments.cs create mode 100644 src/Starlark/Syntax/ExpressionStatement.cs create mode 100644 src/Starlark/Syntax/FlowStatement.cs create mode 100644 src/Starlark/Syntax/ForStatement.cs create mode 100644 src/Starlark/Syntax/IfStatement.cs create mode 100644 src/Starlark/Syntax/IsInstanceExpression.cs create mode 100644 src/Starlark/Syntax/LambdaExpression.cs create mode 100644 src/Starlark/Syntax/LoadStatement.cs create mode 100644 src/Starlark/Syntax/NodePrinter.cs create mode 100644 src/Starlark/Syntax/NodeVisitor.cs create mode 100644 src/Starlark/Syntax/ReturnStatement.cs create mode 100644 src/Starlark/Syntax/SyntaxUtils.cs create mode 100644 src/Starlark/Syntax/TypeAliasStatement.cs create mode 100644 src/Starlark/Syntax/TypeApplication.cs create mode 100644 src/Starlark/Syntax/VarStatement.cs diff --git a/src/Copybara.Core/Util/FileUtil.cs b/src/Copybara.Core/Util/FileUtil.cs new file mode 100644 index 000000000..3d5f996c9 --- /dev/null +++ b/src/Copybara.Core/Util/FileUtil.cs @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Copybara.Util; + +/// +/// Utility methods for files. Port of com.google.copybara.util.FileUtil. +/// +/// Java's java.nio.file.Path is represented here as a string and matching uses +/// /-separated, normalized paths (see ). +/// +public static class FileUtil +{ + private static readonly ILogger Logger = NullLogger.Instance; + + private static readonly IPathMatcher AllFilesMatcher = new AllFilesPathMatcher(); + + // Anchored so that (like Java's Matcher.matches) the whole string must match. + private static readonly Regex Relativism = new(@"\A(.*/)?[.][.]?(/.*)?\z", RegexOptions.Singleline); + + /// + /// Checks that the given path is relative and does not contain any . or .. + /// components. + /// + /// the passed. + public static string CheckNormalizedRelative(string path) + { + string normalized = PathNormalizer.Normalize(path); + Preconditions.CheckArgument( + !Relativism.IsMatch(normalized), + "path has unexpected . or .. components: {0}", path); + Preconditions.CheckArgument( + !normalized.StartsWith('/'), + "path must be relative, but it starts with /: {0}", path); + return path; + } + + /// How to handle symlinks in Copybara. + public enum SymlinkMode + { + /// Copy the symlink as-is. + CopyAsIs, + + /// Materialize the symlink (copy the target's contents). + Materialize, + + /// Ignore the symlink. + Ignore, + + /// Fail the operation. + Fail, + } + + /// Strategy for handling symlinks. Port of FileUtil.CopySymlinkStrategy. + public sealed class CopySymlinkStrategy + { + public static readonly CopySymlinkStrategy FailOutsideSymlinks = + new(SymlinkMode.CopyAsIs, SymlinkMode.Fail, SymlinkMode.Fail); + + public static readonly CopySymlinkStrategy MaterializeOutsideSymlinks = + new(SymlinkMode.CopyAsIs, SymlinkMode.Materialize, SymlinkMode.Fail); + + public static readonly CopySymlinkStrategy IgnoreInvalidSymlinks = + new(SymlinkMode.CopyAsIs, SymlinkMode.CopyAsIs, SymlinkMode.Ignore); + + private readonly SymlinkMode _inside; + private readonly SymlinkMode _outside; + private readonly SymlinkMode _broken; + + public CopySymlinkStrategy(SymlinkMode inside, SymlinkMode outside, SymlinkMode broken) + { + _inside = inside; + _outside = outside; + _broken = broken; + Preconditions.CheckArgument( + broken != SymlinkMode.Materialize, + "MATERIALIZE is not a valid mode for broken symlinks"); + } + + public SymlinkMode GetSymlinkMode(ResolvedSymlink resolvedSymlink) => + resolvedSymlink.TargetLocationValue switch + { + ResolvedSymlink.TargetLocation.Inside => _inside, + ResolvedSymlink.TargetLocation.Outside => _outside, + ResolvedSymlink.TargetLocation.Broken => _broken, + _ => throw new ArgumentOutOfRangeException(), + }; + } + + /// Additional checks to run while copying files. + public interface ICopyVisitorValidator + { + void Validate(string from); + } + + /// + /// Represents the regular file/directory that a symlink points to. Port of + /// FileUtil.ResolvedSymlink. + /// + public sealed class ResolvedSymlink + { + public enum TargetLocation + { + Inside, + Outside, + Broken, + } + + public ResolvedSymlink(string regularFile, TargetLocation targetLocation) + { + RegularFile = Preconditions.CheckNotNull(regularFile); + TargetLocationValue = targetLocation; + } + + public string RegularFile { get; } + + public TargetLocation TargetLocationValue { get; } + + public string GetRegularFile() => RegularFile; + + public TargetLocation GetTargetLocation() => TargetLocationValue; + } + + public static void CopyFilesRecursively( + string from, string to, CopySymlinkStrategy symlinkStrategy) => + CopyFilesRecursively(from, to, symlinkStrategy, Glob.AllFiles); + + /// + /// Copies files from to . Fails if a destination + /// file already exists. File attributes are copied where the platform supports it. + /// + public static void CopyFilesRecursively( + string from, string to, CopySymlinkStrategy symlinkStrategy, Glob glob) => + CopyFilesRecursively(from, to, symlinkStrategy, glob, null); + + public static void CopyFilesRecursively( + string from, + string to, + CopySymlinkStrategy symlinkStrategy, + Glob glob, + ICopyVisitorValidator? validator) + { + Preconditions.CheckArgument(Directory.Exists(from), "{0} (from) is not a directory", from); + Preconditions.CheckArgument(Directory.Exists(to), "{0} (to) is not a directory", to); + + string fromFull = Path.GetFullPath(from); + string toFull = Path.GetFullPath(to); + IPathMatcher destMatcher = glob.RelativeTo(toFull); + + foreach (var root in glob.Roots()) + { + string rootElement = string.IsNullOrEmpty(root) ? from : Path.Combine(from, root); + if (!Directory.Exists(rootElement) && !File.Exists(rootElement)) + { + continue; + } + foreach (var file in EnumerateFilesAndSymlinks(rootElement)) + { + string relativeToFrom = GetRelative(fromFull, file); + string destFile = NormalizeFull(Path.Combine(toFull, relativeToFrom)); + if (!destMatcher.Matches(destFile)) + { + continue; + } + validator?.Validate(file); + string? parent = Path.GetDirectoryName(destFile); + if (parent != null) + { + Directory.CreateDirectory(parent); + } + + var info = new FileInfo(file); + bool symlink = (info.Attributes & FileAttributes.ReparsePoint) != 0; + if (symlink) + { + string? target = info.LinkTarget; + var mode = symlinkStrategy.GetSymlinkMode( + ResolveSymlink(glob.RelativeTo(fromFull), file)); + switch (mode) + { + case SymlinkMode.Fail: + throw new SymlinkException( + $"Symlink '{file}' points to '{target}'"); + case SymlinkMode.Ignore: + continue; + case SymlinkMode.CopyAsIs: + if (target != null) + { + File.CreateSymbolicLink(destFile, target); + } + continue; + case SymlinkMode.Materialize: + File.Copy(file, destFile, overwrite: false); + continue; + } + } + File.Copy(file, destFile, overwrite: false); + } + } + } + + /// + /// Adds the given permissions to the matching files under the given path. On non-POSIX platforms + /// this is a best-effort operation using file attributes. + /// + public static void AddPermissionsRecursively( + string path, UnixFileMode permissionsToAdd, IPathMatcher pathMatcher) + { + foreach (var file in EnumerateFilesAndSymlinks(path)) + { + var info = new FileInfo(file); + bool symlink = (info.Attributes & FileAttributes.ReparsePoint) != 0; + if (!symlink && pathMatcher.Matches(NormalizeFull(file))) + { + AddPermissions(file, permissionsToAdd); + } + } + } + + /// Adds the given permissions to all the files under the given path. + public static void AddPermissionsAllRecursively(string path, UnixFileMode permissionsToAdd) => + AddPermissionsRecursively(path, permissionsToAdd, AllFilesMatcher); + + /// + /// Deletes the files that match the . Directories themselves are not + /// removed, only the files inside them. + /// + public static int DeleteFilesRecursively(string path, IPathMatcher pathMatcher) + { + int counter = 0; + string root = NormalizeFull(path); + foreach (var file in EnumerateFilesAndSymlinks(root)) + { + if (pathMatcher.Matches(NormalizeFull(file))) + { + File.Delete(file); + counter++; + } + } + return counter; + } + + /// Deletes the files that match the . + public static int DeleteFilesRecursively(string path, Glob glob) + { + int counter = 0; + string full = Path.GetFullPath(path); + foreach (var root in glob.Roots()) + { + string rootPath = string.IsNullOrEmpty(root) ? path : Path.Combine(path, root); + if (Directory.Exists(rootPath) || File.Exists(rootPath)) + { + counter += DeleteFilesRecursively(rootPath, glob.RelativeTo(full)); + } + } + return counter; + } + + /// Delete all the contents of a path recursively. + public static void DeleteRecursively(string path) + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } + else if (File.Exists(path)) + { + File.Delete(path); + } + } + + /// + /// A that returns true if any of the delegate matchers returns true. + /// + public static IPathMatcher AnyPathMatcher(IReadOnlyList pathMatchers) => + new AnyPathMatcherImpl(pathMatchers.ToImmutableArray()); + + /// Returns a that negates . + public static IPathMatcher NotPathMatcher(IPathMatcher pathMatcher) => + new NotPathMatcherImpl(pathMatcher); + + /// + /// Resolves recursively until it finds a regular file or directory, + /// checking that all intermediate paths jumps are under . + /// + public static ResolvedSymlink ResolveSymlink(IPathMatcher matcher, string symlink) + { + string path = NormalizeFull(symlink); + Preconditions.CheckArgument(matcher.Matches(path), "{0} doesn't match {1}", path, matcher); + + var visited = new HashSet(); + while (IsSymbolicLink(path)) + { + if (!visited.Add(path)) + { + throw new IOException("Symlink cycle detected:\n " + string.Join("\n ", visited)); + } + if (visited.Count > 50) + { + throw new IOException("Symlink chain too long:\n " + string.Join("\n ", visited)); + } + string? target = new FileInfo(path).LinkTarget; + if (target == null) + { + break; + } + string newPath; + if (!Path.IsPathRooted(target)) + { + string? dir = Path.GetDirectoryName(path); + newPath = NormalizeFull(Path.Combine(dir ?? "", target)); + } + else + { + newPath = NormalizeFull(target); + } + if (!matcher.Matches(newPath)) + { + if (!Directory.Exists(newPath) + || !matcher.Matches(newPath + "/copybara_random_path.txt")) + { + bool broken = !File.Exists(newPath) && !Directory.Exists(newPath); + return new ResolvedSymlink( + newPath, + broken + ? ResolvedSymlink.TargetLocation.Broken + : ResolvedSymlink.TargetLocation.Outside); + } + } + path = newPath; + } + bool notExists = !File.Exists(path) && !Directory.Exists(path); + return new ResolvedSymlink( + path, + notExists ? ResolvedSymlink.TargetLocation.Broken : ResolvedSymlink.TargetLocation.Inside); + } + + /// + /// Tries to add the given permissions. On POSIX filesystems this augments the existing mode; on + /// others it best-effort sets read/write/execute for the owner. + /// + public static void AddPermissions(string path, UnixFileMode permissionsToAdd) + { + if (!OperatingSystem.IsWindows()) + { + UnixFileMode current = File.GetUnixFileMode(path); + File.SetUnixFileMode(path, current | permissionsToAdd); + } + else + { + // On Windows only the read-only attribute is meaningful. If write permission is requested, + // clear the read-only flag. + if ((permissionsToAdd & UnixFileMode.UserWrite) != 0) + { + var attrs = File.GetAttributes(path); + if ((attrs & FileAttributes.ReadOnly) != 0) + { + File.SetAttributes(path, attrs & ~FileAttributes.ReadOnly); + } + } + } + } + + private const int RepoFolderNameLimit = 100; + + public static string ResolveDirInCache(string url, string repoStorage) + { + string escapedUrl = PercentEscape(url); + + // Avoid "Filename too long" errors. + if (escapedUrl.Length > RepoFolderNameLimit + 40) + { + string tail = escapedUrl.Substring(RepoFolderNameLimit - 1); + byte[] hash = SHA1.HashData(Encoding.UTF8.GetBytes(tail)); + escapedUrl = + escapedUrl.Substring(0, RepoFolderNameLimit - 1) + + "_" + + Convert.ToHexStringLower(hash); + } + return Path.Combine(repoStorage, escapedUrl); + } + + // Mirrors Guava PercentEscaper with safeChars "-_" and plusForSpace=true. + private static string PercentEscape(string s) + { + var sb = new StringBuilder(); + foreach (char c in s) + { + if (char.IsLetterOrDigit(c) || c == '-' || c == '_') + { + sb.Append(c); + } + else if (c == ' ') + { + sb.Append('+'); + } + else + { + foreach (byte b in Encoding.UTF8.GetBytes(c.ToString())) + { + sb.Append('%'); + sb.Append("0123456789ABCDEF"[(b >> 4) & 0xF]); + sb.Append("0123456789ABCDEF"[b & 0xF]); + } + } + } + return sb.ToString(); + } + + private static bool IsSymbolicLink(string path) + { + try + { + var info = new FileInfo(path); + return (info.Attributes & FileAttributes.ReparsePoint) != 0; + } + catch (IOException) + { + return false; + } + } + + private static IEnumerable EnumerateFilesAndSymlinks(string root) + { + if (File.Exists(root) && + !Directory.Exists(root)) + { + yield return root; + yield break; + } + if (!Directory.Exists(root)) + { + yield break; + } + foreach (var f in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)) + { + yield return f; + } + } + + private static string NormalizeFull(string path) => + PathNormalizer.Normalize(Path.GetFullPath(path)); + + private static string GetRelative(string from, string file) => + PathNormalizer.Normalize(Path.GetRelativePath(from, file)); + + private sealed class AllFilesPathMatcher : IPathMatcher + { + public bool Matches(string path) => true; + + public override string ToString() => "**"; + } + + private sealed class AnyPathMatcherImpl : IPathMatcher, IEquatable + { + private readonly ImmutableArray _pathMatchers; + + public AnyPathMatcherImpl(ImmutableArray pathMatchers) => + _pathMatchers = pathMatchers; + + public bool Matches(string path) + { + foreach (var pathMatcher in _pathMatchers) + { + if (pathMatcher.Matches(path)) + { + return true; + } + } + return false; + } + + public bool Equals(AnyPathMatcherImpl? other) => + other is not null && _pathMatchers.SequenceEqual(other._pathMatchers); + + public override bool Equals(object? obj) => Equals(obj as AnyPathMatcherImpl); + + public override int GetHashCode() + { + var hc = new HashCode(); + foreach (var m in _pathMatchers) + { + hc.Add(m); + } + return hc.ToHashCode(); + } + + public override string ToString() => "anyOf[" + string.Join(", ", _pathMatchers) + "]"; + } + + private sealed class NotPathMatcherImpl : IPathMatcher + { + private readonly IPathMatcher _pathMatcher; + + public NotPathMatcherImpl(IPathMatcher pathMatcher) => _pathMatcher = pathMatcher; + + public bool Matches(string path) => !_pathMatcher.Matches(path); + + public override string ToString() => "not(" + _pathMatcher + ")"; + } +} diff --git a/src/Copybara.Core/Util/Glob.cs b/src/Copybara.Core/Util/Glob.cs new file mode 100644 index 000000000..9f299f38f --- /dev/null +++ b/src/Copybara.Core/Util/Glob.cs @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRuntime = Starlark.Eval.Starlark; + +namespace Copybara.Util; + +/// +/// A builder that creates a matcher relative to a root path. Port of +/// com.google.copybara.util.Glob. +/// +/// The returned returns true if any of the paths expressions +/// match. If paths is empty it will not match any file. +/// +[StarlarkBuiltin( + "glob", + Doc = "A glob represents a set of relative filepaths in the Copybara workdir. Most consumers " + + "will also accept a list of fully qualified (no wildcards) file names instead.")] +public class Glob : IStarlarkValue, IEquatable +{ + public static readonly Glob AllFiles = CreateGlob(ImmutableArray.Create("**")); + + protected readonly ImmutableArray Include; + private readonly ImmutableArray _globInclude; + private readonly Glob? _exclude; + + internal Glob(IEnumerable include, IEnumerable globInclude, Glob? exclude) + { + Include = Preconditions.CheckNotNull(include).ToImmutableArray(); + _globInclude = Preconditions.CheckNotNull(globInclude).ToImmutableArray(); + _exclude = exclude; + } + + /// + /// Implements the Starlark + (union / list concatenation) and - (difference) + /// operators. Port of Java's binaryOp. + /// + public Glob BinaryOp(string op, object that, bool thisLeft) + { + switch (op) + { + case "+": + if (that is Glob addGlob) + { + return Union(this, addGlob); + } + if (that is IEnumerable addList) + { + return new Glob( + ImmutableArray.Empty, + new[] { this, SequenceGlob.OfIterable(addList) }, + null); + } + throw StarlarkRuntime.Errorf( + "Cannot concatenate {0} with {1}. Only a glob can be concatenated to a glob", + this, that); + case "-": + if (that is Glob subGlob) + { + return Difference(this, subGlob); + } + if (that is IEnumerable subList) + { + Glob list = SequenceGlob.OfIterable(subList); + return new Glob( + Include, _globInclude, _exclude != null ? Union(_exclude, list) : list); + } + throw StarlarkRuntime.Errorf( + "Cannot subtract {0} from {1}. Only a glob can be subtracted from a glob", that, this); + default: + throw StarlarkRuntime.Errorf("Glob does not support {0}", op); + } + } + + /// + /// Compute the 'set union' of two Globs, which is a Glob that will match any Path matched by at + /// least one of those two Globs. + /// + public static Glob Union(Glob glob1, Glob glob2) + { + if (Equals(glob1._exclude, glob2._exclude)) + { + return new Glob( + glob1.Include.Concat(glob2.Include), + glob1._globInclude.Concat(glob2._globInclude), + glob1._exclude); + } + if (glob1._exclude == null) + { + return new Glob( + glob1.Include, glob1._globInclude.Concat(new[] { glob2 }), null); + } + if (glob2._exclude == null) + { + return new Glob( + glob2.Include, glob2._globInclude.Concat(new[] { glob1 }), null); + } + return new Glob(ImmutableArray.Empty, new[] { glob1, glob2 }, null); + } + + /// + /// Compute the 'set difference' of two Globs, which is a Glob that will match any Path which is + /// matched by the first Glob, but not matched by the second Glob. + /// + public static Glob Difference(Glob glob1, Glob glob2) + { + if (glob1._exclude == null) + { + return new Glob(glob1.Include, glob1._globInclude, glob2); + } + return new Glob(glob1.Include, glob1._globInclude, Union(glob1._exclude, glob2)); + } + + /// Checks if the given are or are descendants of the roots. + public static bool AffectsRoots( + ImmutableHashSet roots, IReadOnlyCollection? changedFiles) + { + if (changedFiles == null || IsEmptyRoot(roots)) + { + return true; + } + foreach (var file in changedFiles) + { + foreach (var root in roots) + { + if (file == root || file.StartsWith(root + "/")) + { + return true; + } + } + } + return false; + } + + public virtual IPathMatcher RelativeTo(string path) + { + var includeList = new List(); + foreach (var atom in Include) + { + includeList.Add(atom.Matcher(path)); + } + foreach (var g in _globInclude) + { + includeList.Add(g.RelativeTo(path)); + } + IPathMatcher excludeMatcher = + _exclude == null ? FileUtil.AnyPathMatcher(ImmutableArray.Empty) : _exclude.RelativeTo(path); + return new GlobMatcher(FileUtil.AnyPathMatcher(includeList), excludeMatcher, ToString()); + } + + private sealed class GlobMatcher : IPathMatcher, IEquatable + { + private readonly IPathMatcher _includeMatcher; + private readonly IPathMatcher _excludeMatcher; + private readonly string _toString; + + public GlobMatcher(IPathMatcher includeMatcher, IPathMatcher excludeMatcher, string toString) + { + _includeMatcher = includeMatcher; + _excludeMatcher = excludeMatcher; + _toString = toString; + } + + public bool Matches(string path) => + _includeMatcher.Matches(path) && !_excludeMatcher.Matches(path); + + public override string ToString() => _toString; + + public bool Equals(GlobMatcher? other) => + other is not null + && Equals(_includeMatcher, other._includeMatcher) + && Equals(_excludeMatcher, other._excludeMatcher); + + public override bool Equals(object? obj) => Equals(obj as GlobMatcher); + + public override int GetHashCode() => HashCode.Combine(_includeMatcher, _excludeMatcher); + } + + /// Creates a from include and exclude patterns. + public static Glob CreateGlob(IEnumerable include, IEnumerable exclude) + { + var excludeList = exclude as IReadOnlyCollection ?? exclude.ToList(); + return new Glob( + GlobAtom.OfIterable(include, GlobAtom.AtomType.JavaGlob).ToImmutableArray(), + ImmutableArray.Empty, + excludeList.Count == 0 ? null : CreateGlob(excludeList)); + } + + /// Creates a that matches the given single file paths. + public static Glob CreateSingleFilesGlob(IEnumerable singleFilePaths) => + SequenceGlob.OfIterable(singleFilePaths); + + /// Handles a Glob/Sequence parameter passed to a Glob-consuming function. + public static Glob? WrapGlob(object? globOrList, Glob? defaultValue) + { + if (globOrList is null || ReferenceEquals(globOrList, StarlarkRuntime.None)) + { + return defaultValue; + } + if (globOrList is Glob glob) + { + return glob; + } + if (globOrList is IEnumerable list) + { + return SequenceGlob.OfIterable(list); + } + throw new EvalException("Glob can only be created from a Glob or a list of strings"); + } + + /// Creates a from include patterns only. + public static Glob CreateGlob(IEnumerable include) => + CreateGlob(include, ImmutableArray.Empty); + + /// + /// Calculates a set of paths which recursively contain all files that could possibly match a file + /// in this glob. See the Java documentation for the exact semantics. + /// + public ImmutableHashSet Roots() => Roots(false); + + /// Similar to but returns the longest shared paths for git cones. + public ImmutableHashSet Tips() => ComputeTipsFromIncludes(GetIncludes()); + + /// + /// If is set, then paths containing no meta characters are retained + /// exactly as they are. + /// + public ImmutableHashSet Roots(bool allowFiles) => + ComputeRootsFromIncludes(GetIncludes(), allowFiles); + + /// If roots is empty or contains a single element that is not a subdirectory. + public static bool IsEmptyRoot(IEnumerable roots) + { + using var it = roots.GetEnumerator(); + return !it.MoveNext() || it.Current == ""; + } + + protected virtual IEnumerable GetIncludes() => + Include.Concat(_globInclude.SelectMany(g => g.GetIncludes())); + + private static ImmutableHashSet ComputeRootsFromIncludes( + IEnumerable includes, bool allowFiles) + { + var roots = new List(); + foreach (var atom in includes) + { + roots.Add(atom.Root(allowFiles)); + } + + roots.Sort(CompareRoots); + if (roots.Contains("")) + { + return ImmutableHashSet.Create(""); + } + int r = 0; + while (r < roots.Count - 1) + { + if (roots[r + 1].StartsWith(roots[r] + "/")) + { + roots.RemoveAt(r + 1); + } + else + { + r++; + } + } + + return roots + .Select(s => CollapseSlashes(s)) + .ToImmutableHashSet(); + } + + private static string CollapseSlashes(string s) + { + // Replace "//+" with "/" then remove a trailing "/". + var sb = new StringBuilder(s.Length); + bool prevSlash = false; + foreach (var c in s) + { + if (c == '/') + { + if (!prevSlash) + { + sb.Append('/'); + } + prevSlash = true; + } + else + { + sb.Append(c); + prevSlash = false; + } + } + string result = sb.ToString(); + if (result.EndsWith('/')) + { + result = result.Substring(0, result.Length - 1); + } + return result; + } + + private static ImmutableHashSet ComputeTipsFromIncludes(IEnumerable includes) + { + var wildcards = new List(); + var singleFiles = new List(); + foreach (var atom in includes) + { + Root root = atom.AnnotatedRoot(false); + if (!root.IsRecursive()) + { + singleFiles.Add(root.GetRoot()); + } + else + { + wildcards.Add(root.GetRoot()); + } + } + wildcards.Sort(CompareRoots); + if (wildcards.Contains("")) + { + return ImmutableHashSet.Create(""); + } + int r = 0; + while (r < wildcards.Count - 1) + { + if (wildcards[r + 1].StartsWith(wildcards[r] + "/")) + { + wildcards.RemoveAt(r + 1); + } + else + { + r++; + } + } + + singleFiles.Sort((a, b) => CompareRoots(b, a)); + int t = 0; + while (t < singleFiles.Count - 1) + { + if (singleFiles[t].StartsWith(singleFiles[t + 1] + "/")) + { + singleFiles.RemoveAt(t + 1); + } + else + { + t++; + } + } + var tips = new List(wildcards); + + foreach (var single in singleFiles) + { + if (!wildcards.Any(w => (single + "/").StartsWith(w + "/"))) + { + tips.Add(single); + } + } + tips.AddRange(wildcards); + return tips.ToImmutableHashSet(); + } + + /// A lexicographical string comparator that sorts the '/' char before any other char. + private static int CompareRoots(string s1, string s2) + { + int len1 = s1.Length; + int len2 = s2.Length; + int lim = Math.Min(len1, len2); + for (int k = 0; k < lim; k++) + { + int c1 = s1[k]; + c1 = c1 == '/' ? -1 : c1; + int c2 = s2[k]; + c2 = c2 == '/' ? -1 : c2; + if (c1 != c2) + { + return c1 - c2; + } + } + return len1 - len2; + } + + public override string ToString() => ToStringWithParentheses(true); + + internal virtual string ToStringWithParentheses(bool isRootGlob) + { + var builder = new StringBuilder(); + int numberOfTerms = 0; + bool inlineExclude = + _globInclude.IsEmpty + && _exclude != null + && _exclude.GetType() == GetType() + && _exclude._globInclude.IsEmpty + && _exclude._exclude == null; + if (!Include.IsEmpty || _globInclude.IsEmpty) + { + builder + .Append("glob(include = ") + .Append(ToStringList(Include)) + .Append(inlineExclude ? ", exclude = " + ToStringList(_exclude!.Include) : "") + .Append(')'); + numberOfTerms += 1; + } + foreach (var g in _globInclude) + { + if (numberOfTerms > 0) + { + builder.Append(" + "); + } + builder.Append(g.ToStringWithParentheses(false)); + numberOfTerms += 1; + } + if (_exclude != null && !inlineExclude) + { + builder.Append(" - ").Append(_exclude.ToStringWithParentheses(false)); + numberOfTerms += 1; + } + + if (!isRootGlob && numberOfTerms > 1) + { + return "(" + builder + ")"; + } + return builder.ToString(); + } + + internal string ToStringList(IEnumerable iterable) + { + var sb = new StringBuilder("["); + bool first = true; + foreach (var atom in iterable) + { + if (first) + { + first = false; + } + else + { + sb.Append(", "); + } + sb.Append('"').Append(Sanitize(atom.ToString())).Append('"'); + } + return sb.Append(']').ToString(); + } + + private static string Sanitize(string s) => + s.Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\n", "\\n") + .Replace("\r", "\\r") + .Replace("\t", "\\t") + .Replace("\f", "\\f") + .Replace("\b", "\\b") + .Replace("\0", "\\000"); + + public bool Equals(Glob? other) => + other is not null + && Include.SequenceEqual(other.Include) + && _globInclude.SequenceEqual(other._globInclude) + && Equals(_exclude, other._exclude); + + public override bool Equals(object? obj) => Equals(obj as Glob); + + public override int GetHashCode() + { + var hc = new HashCode(); + foreach (var a in Include) + { + hc.Add(a); + } + foreach (var g in _globInclude) + { + hc.Add(g); + } + hc.Add(_exclude); + return hc.ToHashCode(); + } + + public int HeightOfGlobTree() + { + int includeHeight = _globInclude.Length == 0 + ? -1 + : _globInclude.Max(g => g.HeightOfGlobTree()); + int excludeHeight = _exclude == null ? -1 : _exclude.HeightOfGlobTree(); + return 1 + Math.Max(includeHeight, excludeHeight); + } +} diff --git a/src/Copybara.Core/Util/SequenceGlob.cs b/src/Copybara.Core/Util/SequenceGlob.cs new file mode 100644 index 000000000..3982704c3 --- /dev/null +++ b/src/Copybara.Core/Util/SequenceGlob.cs @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Starlark.Eval; + +namespace Copybara.Util; + +/// +/// A "glob" that matches lists of fully qualified file names. Port of +/// com.google.copybara.util.SequenceGlob. +/// +public class SequenceGlob : Glob +{ + private SequenceGlob(IEnumerable include) + : base(include, ImmutableArray.Empty, null) + { + } + + internal override string ToStringWithParentheses(bool isRootGlob) => ToStringList(Include); + + public override IPathMatcher RelativeTo(string root) + { + var matchPaths = new HashSet(); + foreach (var atom in Include) + { + matchPaths.Add(GlobAtom.GetRelativePath(root, atom.Pattern())); + } + return new ReadablePathMatcher(new SetPathMatcher(matchPaths), ToString()); + } + + private sealed class SetPathMatcher : IPathMatcher + { + private readonly HashSet _matchPaths; + + public SetPathMatcher(HashSet matchPaths) => _matchPaths = matchPaths; + + public bool Matches(string path) => _matchPaths.Contains(NormalizePath(path)); + + private static string NormalizePath(string path) => GlobAtom.GetRelativePath("", path); + } + + public static SequenceGlob OfStarlarkList(IEnumerable patterns) + { + var atoms = ImmutableArray.CreateBuilder(); + foreach (var pattern in patterns) + { + string s = pattern.ToString() ?? ""; + atoms.Add(GlobAtom.Of(s, GlobAtom.AtomType.SingleFile)); + if (pattern is not string) + { + throw new EvalException("Only strings are supported in file lists."); + } + if (GlobAtom.IsMeta(s)) + { + throw new EvalException("Wildcards are not supported in file lists."); + } + } + return new SequenceGlob(atoms.ToImmutable()); + } + + /// Creates a from an iterable of paths. + public static SequenceGlob OfIterable(IEnumerable paths) + { + var atoms = ImmutableArray.CreateBuilder(); + foreach (var path in paths) + { + atoms.Add(GlobAtom.Of(path, GlobAtom.AtomType.SingleFile)); + } + return new SequenceGlob(atoms.ToImmutable()); + } +} diff --git a/src/Starlark/Eval/Dict.cs b/src/Starlark/Eval/Dict.cs new file mode 100644 index 000000000..f7c8e546b --- /dev/null +++ b/src/Starlark/Eval/Dict.cs @@ -0,0 +1,547 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// A Dict is a Starlark dictionary (dict), a mapping from keys to values. Port of +/// net.starlark.java.eval.Dict. +/// +/// Iteration yields keys in insertion order. Order is unaffected by updating an existing +/// key's value, but a removed-then-reinserted key moves to the end (as in Starlark/Python). +/// +[StarlarkBuiltin("dict", Category = "core", Doc = "The built-in dict type.")] +public sealed class Dict : IStarlarkPrintableValue, IFreezable, IStarlarkIndexable, + IStarlarkIterable, IEnumerable +{ + // Insertion-ordered contents (a small hand-rolled ordered map to guarantee Starlark iteration order). + private readonly OrderedMap contents; + private int iteratorCount; + private Mutability mutability; + + private static readonly Dict EmptyImmutable = new(Mutability.IMMUTABLE, new OrderedMap()); + + private Dict(Mutability mutability, OrderedMap contents) + { + this.mutability = mutability; + this.contents = contents; + } + + /// + /// Takes ownership of the map and returns a new Dict wrapping it. Null mutability means immutable. + /// + internal static Dict Wrap(Mutability? mu, OrderedMap contents) + { + mu ??= Mutability.IMMUTABLE; + if (mu == Mutability.IMMUTABLE && contents.Count == 0) + { + return Empty(); + } + return new Dict(mu, contents); + } + + public bool Truth() => contents.Count != 0; + + public bool IsImmutable() => mutability.IsFrozen; + + public Mutability Mutability => mutability; + + public bool UpdateIteratorCount(int delta) + { + if (mutability.IsFrozen) + { + return false; + } + if (delta > 0) + { + iteratorCount++; + } + else if (delta < 0) + { + iteratorCount--; + } + return iteratorCount > 0; + } + + public void UnsafeShallowFreeze() + { + Mutability.CheckUnsafeShallowFreezePrecondition(this); + mutability = Mutability.IMMUTABLE; + } + + public void CheckHashable() => throw Starlark.Errorf("unhashable type: 'dict'"); + + public override int GetHashCode() => contents.GetHashCode(); + + public override bool Equals(object? o) => o is Dict d && contents.Equals(d.contents); + + public IEnumerator GetEnumerator() => contents.Keys.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => contents.Keys.GetEnumerator(); + + /// The number of entries. + public int Count => contents.Count; + + /// Whether the dict is empty. + public bool IsEmpty => contents.Count == 0; + + /// Returns the value for the given key, or null if absent. + public object? Get(object key) => contents.TryGetValue(key, out object? v) ? v : null; + + public bool ContainsKeyJava(object key) => contents.ContainsKey(key); + + public IReadOnlyList Keys => contents.Keys; + + public IReadOnlyList Values => contents.Values; + + public IEnumerable> Entries => contents.Entries; + + // -- factories -- + + public static Dict Empty() => EmptyImmutable; + + /// Returns a new empty dict with the specified mutability (null means immutable). + public static Dict Of(Mutability? mu) + { + mu ??= Mutability.IMMUTABLE; + return mu == Mutability.IMMUTABLE ? Empty() : new Dict(mu, new OrderedMap()); + } + + /// Returns a new dict with the given mutability containing the entries of m. + public static Dict CopyOf(Mutability? mu, IEnumerable> m) + { + mu ??= Mutability.IMMUTABLE; + var map = new OrderedMap(); + foreach (var e in m) + { + map.Put(Starlark.CheckValid(e.Key), Starlark.CheckValid(e.Value)); + } + if (mu == Mutability.IMMUTABLE && map.Count == 0) + { + return Empty(); + } + return new Dict(mu, map); + } + + public static Dict ImmutableCopyOf(IEnumerable> m) => CopyOf(null, m); + + public static Builder NewBuilder() => new(); + + /// A reusable builder for Dicts. + public sealed class Builder + { + private readonly List items = new(); // [k, v, ... k, v] + + public Builder Put(object? k, object? v) + { + items.Add(Starlark.CheckValid(k)); + items.Add(Starlark.CheckValid(v)); + return this; + } + + public Builder PutAll(IEnumerable> map) + { + foreach (var e in map) + { + Put(e.Key, e.Value); + } + return this; + } + + public Dict BuildImmutable() => Build(null); + + public Dict Build(Mutability? mu) + { + mu ??= Mutability.IMMUTABLE; + if (mu == Mutability.IMMUTABLE && items.Count == 0) + { + return Empty(); + } + var map = new OrderedMap(); + for (int i = 0; i < items.Count; i += 2) + { + map.Put(items[i], items[i + 1]); + } + return new Dict(mu, map); + } + } + + // -- mutators (checked against freezing) -- + + /// Puts an entry into the dict, after validating that mutation is allowed. + public void PutEntry(object? key, object? value) + { + Starlark.CheckMutable(this); + Starlark.CheckHashable(key); + contents.Put(key, value); + } + + /// Puts all the entries from a given map into the dict. + public void PutEntries(IEnumerable> map) + { + Starlark.CheckMutable(this); + foreach (var e in map) + { + Starlark.CheckHashable(e.Key); + contents.Put(e.Key, e.Value); + } + } + + /// Deletes the entry associated with the given key; returns its value or null. + public object? RemoveEntry(object key) + { + Starlark.CheckMutable(this); + return contents.Remove(key, out object? v) ? v : null; + } + + [StarlarkMethod("clear", Doc = "Remove all items from the dictionary.")] + public void ClearEntries() + { + Starlark.CheckMutable(this); + contents.Clear(); + } + + [StarlarkMethod("get", Doc = "Returns the value for key if present, else default.", + UseStarlarkThread = true)] + public object? Get2( + [Param(Name = "key")] object key, + [Param(Name = "default", DefaultValue = "None", Named = true)] object? defaultValue, + StarlarkThread thread) + { + object? v = Get(key); + if (v != null) + { + return v; + } + ContainsKey(thread.GetSemantics(), key); // throws if unhashable + return defaultValue; + } + + [StarlarkMethod("pop", Doc = "Removes a key and returns the associated value.", + UseStarlarkThread = true)] + public object? Pop( + [Param(Name = "key")] object key, + [Param(Name = "default", DefaultValue = "unbound", Named = true)] object? defaultValue, + StarlarkThread thread) + { + Starlark.CheckMutable(this); + if (contents.Remove(key, out object? value)) + { + return value; + } + Starlark.CheckHashable(key); + if (!ReferenceEquals(defaultValue, Starlark.UNBOUND)) + { + return defaultValue; + } + throw Starlark.Errorf("KeyError: {0}", Starlark.Repr(key, thread.GetSemantics())); + } + + [StarlarkMethod("popitem", Doc = "Remove and return the first (key, value) pair.")] + public Tuple Popitem() + { + if (IsEmpty) + { + throw Starlark.Errorf("popitem: empty dictionary"); + } + Starlark.CheckMutable(this); + var e = contents.First(); + contents.Remove(e.Key!, out _); + return Tuple.Pair(e.Key, e.Value); + } + + [StarlarkMethod("setdefault", Doc = "Returns the value for key, inserting default if absent.")] + public object? Setdefault( + [Param(Name = "key")] object? key, + [Param(Name = "default", DefaultValue = "None", Named = true)] object? defaultValue) + { + Starlark.CheckMutable(this); + Starlark.CheckHashable(key); + if (contents.TryGetValue(key, out object? prev)) + { + return prev; + } + contents.Put(key, defaultValue); + return defaultValue; + } + + [StarlarkMethod("update", Doc = "Updates the dictionary with pairs then keyword arguments.", + UseStarlarkThread = true)] + public void Update( + [Param(Name = "pairs", DefaultValue = "[]")] object pairs, + Dict kwargs, + StarlarkThread thread) + { + Starlark.CheckMutable(this); + UpdateCommon("update", this, pairs, kwargs.Entries); + } + + // Common implementation of dict(pairs, **kwargs) and dict.update(pairs, **kwargs). + internal static void UpdateCommon( + string funcname, Dict dict, object pairs, IEnumerable> kwargs) + { + if (pairs is Dict pd) + { + dict.PutEntries(pd.Entries); + } + else + { + IEnumerable iterable; + try + { + iterable = Starlark.ToIterable(pairs); + } + catch (EvalException) + { + throw Starlark.Errorf("in {0}, got {1}, want iterable", funcname, Starlark.Type(pairs)); + } + int pos = 0; + foreach (object? item in iterable) + { + object?[] pair; + try + { + pair = Starlark.ToArray(item); + } + catch (EvalException) + { + throw Starlark.Errorf( + "in {0}, dictionary update sequence element #{1} is not iterable ({2})", + funcname, pos, Starlark.Type(item)); + } + if (pair.Length != 2) + { + throw Starlark.Errorf( + "in {0}, item #{1} has length {2}, but exactly two elements are required", + funcname, pos, pair.Length); + } + dict.PutEntry(pair[0], pair[1]); + pos++; + } + } + dict.PutEntries(kwargs); + } + + [StarlarkMethod("values", Doc = "Returns the list of values.", UseStarlarkThread = true)] + public StarlarkList Values0(StarlarkThread thread) => + StarlarkList.CopyOf(thread.Mutability, contents.Values); + + [StarlarkMethod("items", Doc = "Returns the list of key-value tuples.", UseStarlarkThread = true)] + public StarlarkList Items(StarlarkThread thread) + { + object?[] array = new object?[Count]; + int i = 0; + foreach (var e in contents.Entries) + { + array[i++] = Tuple.Pair(e.Key, e.Value); + } + return StarlarkList.Wrap(thread.Mutability, array); + } + + [StarlarkMethod("keys", Doc = "Returns the list of keys.", UseStarlarkThread = true)] + public StarlarkList Keys0(StarlarkThread thread) => + StarlarkList.Wrap(thread.Mutability, contents.Keys.ToArray()); + + public void Repr(Printer printer, StarlarkSemantics semantics) + { + printer.Append("{"); + string sep = ""; + foreach (var e in contents.Entries) + { + printer.Append(sep); + sep = ", "; + printer.Repr(e.Key, semantics).Append(": ").Repr(e.Value, semantics); + } + printer.Append("}"); + } + + public override string ToString() => Starlark.Repr(this, StarlarkSemantics.DEFAULT); + + // -- StarlarkIndexable -- + + public object? GetIndex(StarlarkSemantics semantics, object key) + { + object? v = Get(key); + if (v == null) + { + throw Starlark.Errorf("key {0} not found in dictionary", Starlark.Repr(key, semantics)); + } + return v; + } + + public bool ContainsKey(StarlarkSemantics semantics, object key) + { + Starlark.CheckHashable(key); + return contents.ContainsKey(key); + } + + /// + /// A small insertion-ordered map used to back Dict, keyed by Starlark value equality. Deleting + /// then reinserting a key moves it to the end, matching Starlark semantics. + /// + internal sealed class OrderedMap + { + private readonly Dictionary index = new(StarlarkKeyComparer.Instance); + private readonly List> order = new(); + private int liveCount; + + public int Count => liveCount; + + private static readonly object NullSentinel = new(); + + private static object Box(object? k) => k ?? NullSentinel; + private static object? Unbox(object k) => ReferenceEquals(k, NullSentinel) ? null : k; + + public bool ContainsKey(object? key) => index.ContainsKey(Box(key)); + + public bool TryGetValue(object? key, out object? value) + { + if (index.TryGetValue(Box(key), out int i)) + { + value = order[i].Value; + return true; + } + value = null; + return false; + } + + public void Put(object? key, object? value) + { + object bk = Box(key); + if (index.TryGetValue(bk, out int i)) + { + order[i] = new KeyValuePair(key, value); + } + else + { + index[bk] = order.Count; + order.Add(new KeyValuePair(key, value)); + liveCount++; + } + } + + public bool Remove(object? key, out object? value) + { + object bk = Box(key); + if (index.TryGetValue(bk, out int i)) + { + value = order[i].Value; + index.Remove(bk); + order[i] = new KeyValuePair(Tombstone, null); + liveCount--; + if (liveCount < order.Count / 2) + { + Compact(); + } + return true; + } + value = null; + return false; + } + + public void Clear() + { + index.Clear(); + order.Clear(); + liveCount = 0; + } + + public KeyValuePair First() + { + foreach (var e in order) + { + if (!ReferenceEquals(e.Key, Tombstone)) + { + return e; + } + } + throw new InvalidOperationException("empty"); + } + + public IReadOnlyList Keys => Entries.Select(e => e.Key).ToList(); + + public IReadOnlyList Values => Entries.Select(e => e.Value).ToList(); + + public IEnumerable> Entries + { + get + { + foreach (var e in order) + { + if (!ReferenceEquals(e.Key, Tombstone)) + { + yield return e; + } + } + } + } + + private static readonly object Tombstone = new(); + + private void Compact() + { + var live = new List>(liveCount); + index.Clear(); + foreach (var e in order) + { + if (!ReferenceEquals(e.Key, Tombstone)) + { + index[Box(e.Key)] = live.Count; + live.Add(e); + } + } + order.Clear(); + order.AddRange(live); + } + + public override int GetHashCode() + { + int h = 0; + foreach (var e in Entries) + { + h += (e.Key?.GetHashCode() ?? 0) ^ (e.Value?.GetHashCode() ?? 0); + } + return h; + } + + public override bool Equals(object? obj) + { + if (obj is not OrderedMap other || liveCount != other.liveCount) + { + return false; + } + foreach (var e in Entries) + { + if (!other.TryGetValue(e.Key, out object? v) || !object.Equals(e.Value, v)) + { + return false; + } + } + return true; + } + } + + // Equality comparer that uses Starlark value equality/hashing for keys. + private sealed class StarlarkKeyComparer : IEqualityComparer + { + public static readonly StarlarkKeyComparer Instance = new(); + + public new bool Equals(object? x, object? y) => object.Equals(x, y); + + public int GetHashCode(object obj) => obj.GetHashCode(); + } +} diff --git a/src/Starlark/Eval/EvalUtils.cs b/src/Starlark/Eval/EvalUtils.cs new file mode 100644 index 000000000..a85555517 --- /dev/null +++ b/src/Starlark/Eval/EvalUtils.cs @@ -0,0 +1,524 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Starlark.Syntax; + +namespace Starlark.Eval; + +/// +/// Internal declarations used by the evaluator. Port of net.starlark.java.eval.EvalUtils. +/// +/// Deferred vs. Java: the StarlarkSet operator branches (set union/intersection/etc.) are +/// omitted pending the StarlarkSet port; string % formatting delegates to +/// helpers. +/// +public static class EvalUtils +{ + internal static void AddIterator(object? x) + { + if (x is IFreezable f) + { + f.UpdateIteratorCount(+1); + } + } + + internal static void RemoveIterator(object? x) + { + if (x is IFreezable f) + { + f.UpdateIteratorCount(-1); + } + } + + /// + /// Resolves a positive or negative index into [0, length), or throws if out of range. Negative + /// indices count backward from length. + /// + public static int GetSequenceIndex(int index, int length) + { + int actualIndex = index; + if (actualIndex < 0) + { + actualIndex += length; + } + if (actualIndex < 0 || actualIndex >= length) + { + throw Starlark.Errorf( + "index out of range (index is {0}, but sequence has {1} elements)", index, length); + } + return actualIndex; + } + + /// Evaluates an eager binary operation, x op y. (Excludes AND and OR.) + public static object? BinaryOp(TokenKind op, object? x, object? y, StarlarkThread thread) + { + StarlarkSemantics semantics = thread.GetSemantics(); + Mutability mu = thread.Mutability; + switch (op) + { + case TokenKind.PLUS: + if (x is StarlarkInt xip) + { + if (y is StarlarkInt yip) + { + return StarlarkInt.Add(xip, yip); + } + if (y is StarlarkFloat yfp) + { + return StarlarkFloat.Of(xip.ToFiniteDouble() + yfp.ToDouble()); + } + } + else if (x is string xsp) + { + if (y is string ysp) + { + return xsp + ysp; + } + } + else if (x is Tuple xtp) + { + if (y is Tuple ytp) + { + return Tuple.Concat(xtp, ytp); + } + } + else if (x is StarlarkList xlp) + { + if (y is StarlarkList ylp) + { + return StarlarkList.Concat(xlp, ylp, mu); + } + } + else if (x is StarlarkFloat xfp) + { + if (y is StarlarkFloat yfp2) + { + return StarlarkFloat.Of(xfp.ToDouble() + yfp2.ToDouble()); + } + if (y is StarlarkInt yip2) + { + return StarlarkFloat.Of(xfp.ToDouble() + yip2.ToFiniteDouble()); + } + } + break; + + case TokenKind.PIPE: + if (x is StarlarkInt xio && y is StarlarkInt yio) + { + return StarlarkInt.Or(xio, yio); + } + if (x is Dict xd && y is Dict yd) + { + return Dict.NewBuilder().PutAll(xd.Entries).PutAll(yd.Entries).Build(mu); + } + break; + + case TokenKind.AMPERSAND: + if (x is StarlarkInt xia && y is StarlarkInt yia) + { + return StarlarkInt.And(xia, yia); + } + break; + + case TokenKind.CARET: + if (x is StarlarkInt xic && y is StarlarkInt yic) + { + return StarlarkInt.Xor(xic, yic); + } + break; + + case TokenKind.GREATER_GREATER: + if (x is StarlarkInt xigg && y is StarlarkInt yigg) + { + return StarlarkInt.ShiftRight(xigg, yigg); + } + break; + + case TokenKind.LESS_LESS: + if (x is StarlarkInt xill && y is StarlarkInt yill) + { + return StarlarkInt.ShiftLeft(xill, yill); + } + break; + + case TokenKind.MINUS: + if (x is StarlarkInt xim) + { + if (y is StarlarkInt yim) + { + return StarlarkInt.Subtract(xim, yim); + } + if (y is StarlarkFloat yfm) + { + return StarlarkFloat.Of(xim.ToFiniteDouble() - yfm.ToDouble()); + } + } + else if (x is StarlarkFloat xfm) + { + if (y is StarlarkFloat yfm2) + { + return StarlarkFloat.Of(xfm.ToDouble() - yfm2.ToDouble()); + } + if (y is StarlarkInt yim2) + { + return StarlarkFloat.Of(xfm.ToDouble() - yim2.ToFiniteDouble()); + } + } + break; + + case TokenKind.STAR: + if (x is StarlarkInt xis) + { + if (y is StarlarkInt yis) + { + return StarlarkInt.Multiply(xis, yis); + } + if (y is string yss) + { + return RepeatString(yss, xis); + } + if (y is Tuple yts) + { + return yts.Repeat(xis); + } + if (y is StarlarkList yls) + { + return yls.Repeat(xis, mu); + } + if (y is StarlarkFloat yfs) + { + return StarlarkFloat.Of(xis.ToFiniteDouble() * yfs.ToDouble()); + } + } + else if (x is string xss) + { + if (y is StarlarkInt yis2) + { + return RepeatString(xss, yis2); + } + } + else if (x is Tuple xts) + { + if (y is StarlarkInt yis3) + { + return xts.Repeat(yis3); + } + } + else if (x is StarlarkList xls) + { + if (y is StarlarkInt yis4) + { + return xls.Repeat(yis4, mu); + } + } + else if (x is StarlarkFloat xfs) + { + if (y is StarlarkFloat yfs2) + { + return StarlarkFloat.Of(xfs.ToDouble() * yfs2.ToDouble()); + } + if (y is StarlarkInt yis5) + { + return StarlarkFloat.Of(xfs.ToDouble() * yis5.ToFiniteDouble()); + } + } + break; + + case TokenKind.SLASH: // real division + if (x is StarlarkInt xisl) + { + double xf = xisl.ToFiniteDouble(); + if (y is StarlarkInt yisl) + { + return StarlarkFloat.Div(xf, yisl.ToFiniteDouble()); + } + if (y is StarlarkFloat yfsl) + { + return StarlarkFloat.Div(xf, yfsl.ToDouble()); + } + } + else if (x is StarlarkFloat xfsl) + { + double xf = xfsl.ToDouble(); + if (y is StarlarkFloat yfsl2) + { + return StarlarkFloat.Div(xf, yfsl2.ToDouble()); + } + if (y is StarlarkInt yisl2) + { + return StarlarkFloat.Div(xf, yisl2.ToFiniteDouble()); + } + } + break; + + case TokenKind.SLASH_SLASH: + if (x is StarlarkInt xiss) + { + if (y is StarlarkInt yiss) + { + return StarlarkInt.Floordiv(xiss, yiss); + } + if (y is StarlarkFloat yfss) + { + return StarlarkFloat.Floordiv(xiss.ToFiniteDouble(), yfss.ToDouble()); + } + } + else if (x is StarlarkFloat xfss) + { + double xf = xfss.ToDouble(); + if (y is StarlarkFloat yfss2) + { + return StarlarkFloat.Floordiv(xf, yfss2.ToDouble()); + } + if (y is StarlarkInt yiss2) + { + return StarlarkFloat.Floordiv(xf, yiss2.ToFiniteDouble()); + } + } + break; + + case TokenKind.PERCENT: + if (x is StarlarkInt xipc) + { + if (y is StarlarkInt yipc) + { + return StarlarkInt.Mod(xipc, yipc); + } + if (y is StarlarkFloat yfpc) + { + return StarlarkFloat.Mod(xipc.ToFiniteDouble(), yfpc.ToDouble()); + } + } + else if (x is string xspc) + { + try + { + if (y is Tuple ytpc) + { + return Starlark.FormatWithList(semantics, xspc, ytpc); + } + return Starlark.Format(semantics, xspc, y); + } + catch (FormatException ex) + { + throw new EvalException(ex.Message); + } + } + else if (x is StarlarkFloat xfpc) + { + double xf = xfpc.ToDouble(); + if (y is StarlarkFloat yfpc2) + { + return StarlarkFloat.Mod(xf, yfpc2.ToDouble()); + } + if (y is StarlarkInt yipc2) + { + return StarlarkFloat.Mod(xf, yipc2.ToFiniteDouble()); + } + } + break; + + case TokenKind.EQUALS_EQUALS: + return Equals(x, y); + + case TokenKind.NOT_EQUALS: + return !Equals(x, y); + + case TokenKind.LESS: + return Compare(x, y) < 0; + + case TokenKind.LESS_EQUALS: + return Compare(x, y) <= 0; + + case TokenKind.GREATER: + return Compare(x, y) > 0; + + case TokenKind.GREATER_EQUALS: + return Compare(x, y) >= 0; + + case TokenKind.IN: + if (y is IStarlarkMembershipTestable mt) + { + return mt.ContainsKey(semantics, x!); + } + if (y is IStarlarkIndexable.IThreaded th) + { + return th.ContainsKey(thread, semantics, x!); + } + if (y is string ys) + { + if (x is not string xs2) + { + throw Starlark.Errorf( + "'in ' requires string as left operand, not '{0}'", Starlark.Type(x)); + } + return ys.Contains(xs2, StringComparison.Ordinal); + } + break; + + case TokenKind.NOT_IN: + object? z = BinaryOp(TokenKind.IN, x, y, thread); + if (z != null) + { + return !Starlark.Truth(z); + } + break; + + default: + throw new InvalidOperationException("not a binary operator: " + op); + } + + // custom binary operator? + if (x is IHasBinary xhb) + { + object? z = xhb.BinaryOp(op, y!, true); + if (z != null) + { + return z; + } + } + if (y is IHasBinary yhb) + { + object? z = yhb.BinaryOp(op, x!, false); + if (z != null) + { + return z; + } + } + + throw Starlark.Errorf( + "unsupported binary operation: {0} {1} {2}", Starlark.Type(x), op, Starlark.Type(y)); + } + + private static int Compare(object? x, object? y) + { + try + { + return Starlark.CompareUnchecked(x, y); + } + catch (InvalidCastException ex) + { + throw new EvalException(ex.Message); + } + } + + private static string RepeatString(string s, StarlarkInt @in) + { + int n = @in.ToInt("repeat"); + if (n <= 0) + { + return ""; + } + if ((long)s.Length * n > int.MaxValue) + { + throw Starlark.Errorf("excessive repeat ({0} * {1} characters)", s.Length, n); + } + return string.Concat(Enumerable.Repeat(s, n)); + } + + /// Evaluates a unary operation. + public static object? UnaryOp(TokenKind op, object? x) + { + switch (op) + { + case TokenKind.NOT: + return !Starlark.Truth(x); + + case TokenKind.MINUS: + if (x is StarlarkInt xi) + { + return StarlarkInt.Uminus(xi); + } + if (x is StarlarkFloat xf) + { + return StarlarkFloat.Of(-xf.ToDouble()); + } + break; + + case TokenKind.PLUS: + if (x is StarlarkInt or StarlarkFloat) + { + return x; + } + break; + + case TokenKind.TILDE: + if (x is StarlarkInt xit) + { + return StarlarkInt.Bitnot(xit); + } + break; + } + throw Starlark.Errorf("unsupported unary operation: {0}{1}", op, Starlark.Type(x)); + } + + /// Returns the element of sequence or mapping indexed by key. + public static object? Index(StarlarkThread thread, object? obj, object key) + { + Mutability mu = thread.Mutability; + StarlarkSemantics semantics = thread.GetSemantics(); + + if (obj is IStarlarkIndexable.IThreaded th) + { + return th.GetIndex(thread, semantics, key); + } + if (obj is IStarlarkIndexable ix) + { + object? result = ix.GetIndex(semantics, key); + return result == null ? null : Starlark.FromJava(result, mu); + } + if (obj is string s) + { + int index = Starlark.ToInt(key, "string index"); + index = GetSequenceIndex(index, s.Length); + return s[index].ToString(); + } + throw Starlark.Errorf( + "type '{0}' has no operator []({1})", Starlark.Type(obj), Starlark.Type(key)); + } + + /// Updates an object as if by object[key] = value. + public static void SetIndex(object? obj, object? key, object? value) + { + if (obj is Dict dict) + { + dict.PutEntry(key, value); + } + else if (obj is StarlarkList list) + { + int index = Starlark.ToInt(key, "list index"); + index = GetSequenceIndex(index, list.Count); + list.SetElementAt(index, value); + } + else + { + throw Starlark.Errorf( + "can only assign an element in a dictionary or a list, not in a '{0}'", + Starlark.Type(obj)); + } + } + + /// Updates the named field of x as if by x.field = value. + public static void SetField(object? x, string field, object value) + { + if (x is IStructure structure) + { + structure.SetField(field, value); + } + else + { + throw Starlark.Errorf("cannot set .{0} field of {1} value", field, Starlark.Type(x)); + } + } +} diff --git a/src/Starlark/Eval/GuardedValue.cs b/src/Starlark/Eval/GuardedValue.cs new file mode 100644 index 000000000..5349d2706 --- /dev/null +++ b/src/Starlark/Eval/GuardedValue.cs @@ -0,0 +1,31 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Eval; + +/// +/// Wrapper on a predeclared value that controls its accessibility to Starlark based on a semantic +/// flag and/or the Module's client data. Port of net.starlark.java.eval.GuardedValue. +/// +public interface IGuardedValue +{ + /// Returns an error describing an attempt to access this guard's protected object. + string GetErrorFromAttemptingAccess(string name); + + /// Returns this guard's underlying object. + object GetObject(); + + /// Returns true if the underlying object is accessible under the given semantics. + bool IsObjectAccessibleUsingSemantics(StarlarkSemantics semantics, object? clientData); +} diff --git a/src/Starlark/Eval/Module.cs b/src/Starlark/Eval/Module.cs new file mode 100644 index 000000000..a87a00e14 --- /dev/null +++ b/src/Starlark/Eval/Module.cs @@ -0,0 +1,183 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Globalization; + +namespace Starlark.Eval; + +/// +/// A Module represents a Starlark module, a container of global variables populated by executing a +/// Starlark file. Port of net.starlark.java.eval.Module. +/// +/// Note: the resolver integration (Resolver.Module) and static type-checking hooks +/// (getTypeConstructor, get*FieldType) are deferred pending the syntax/type-checker +/// port; the global/predeclared environment functionality is complete. +/// +public sealed class Module +{ + // The predeclared environment. Excludes UNIVERSE bindings. Conditionally-present values are + // stored as IGuardedValue regardless of whether they are enabled. + private readonly ImmutableDictionary predeclared; + + // The module's global variables, in order of creation. + private readonly Dictionary globalIndex = new(); + private object?[] globals = new object?[8]; + + private readonly object? clientData; + private readonly StarlarkSemantics semantics; + private string? documentation; + + private Module( + ImmutableDictionary predeclared, object? clientData, StarlarkSemantics semantics) + { + this.predeclared = predeclared; + this.clientData = clientData; + this.semantics = semantics; + } + + /// Constructs a Module with the specified predeclared bindings. + public static Module WithPredeclared( + StarlarkSemantics semantics, IReadOnlyDictionary predeclared) => + WithPredeclaredAndData(semantics, predeclared, null); + + /// Constructs a Module with predeclared bindings and client data. + public static Module WithPredeclaredAndData( + StarlarkSemantics semantics, IReadOnlyDictionary predeclared, object? clientData) => + new(predeclared.ToImmutableDictionary(), clientData, semantics); + + /// Creates a module with no predeclared bindings and no client data. + public static Module Create() => + new(ImmutableDictionary.Empty, null, StarlarkSemantics.DEFAULT); + + /// Returns the client data associated with this module. + public object? ClientData => clientData; + + public StarlarkSemantics Semantics => semantics; + + /// Sets the module's doc string. + public void SetDocumentation(string doc) => documentation = doc; + + /// Returns the module's doc string, or null. + public string? GetDocumentation() => documentation; + + // Replaces an enabled IGuardedValue with the value it guards. + private object FilterGuardedValue(object v) + { + if (v is not IGuardedValue gv) + { + return v; + } + return gv.IsObjectAccessibleUsingSemantics(semantics, clientData) ? gv.GetObject() : gv; + } + + /// Returns the value of a predeclared (not universal) binding in this module. + public object? GetPredeclared(string name) => + predeclared.TryGetValue(name, out object? value) ? FilterGuardedValue(value) : null; + + /// Returns this module's additional predeclared bindings (excludes UNIVERSE). + public IReadOnlyDictionary GetPredeclaredBindings() + { + var result = new Dictionary(); + foreach (var e in predeclared) + { + result[e.Key] = FilterGuardedValue(e.Value); + } + return result; + } + + /// Returns an immutable mapping containing the global variables of this module. + public ImmutableDictionary GetGlobals() + { + var m = ImmutableDictionary.CreateBuilder(); + foreach (var e in globalIndex) + { + object? v = GetGlobalByIndex(e.Value); + if (v != null) + { + m[e.Key] = v; + } + } + return m.ToImmutable(); + } + + /// + /// Returns the value of the specified global variable, or null if not bound. Does not look in + /// the predeclared environment. + /// + public object? GetGlobal(string name) => + globalIndex.TryGetValue(name, out int i) ? globals[i] : null; + + internal void SetGlobalByIndex(int i, object value) + { + if (i >= globalIndex.Count) + { + throw new ArgumentException("index out of range"); + } + globals[i] = value; + } + + internal object? GetGlobalByIndex(int i) + { + if (i >= globalIndex.Count) + { + throw new ArgumentException("index out of range"); + } + return globals[i]; + } + + /// + /// Returns the index within this Module of a global variable, creating a slot if needed. + /// + internal int GetIndexOfGlobal(string name) + { + if (globalIndex.TryGetValue(name, out int prev)) + { + return prev; + } + int i = globalIndex.Count; + globalIndex[name] = i; + if (i == globals.Length) + { + Array.Resize(ref globals, globals.Length << 1); + } + return i; + } + + /// Returns a list of indices of a list of globals. + internal int[] GetIndicesOfGlobals(IReadOnlyList names) + { + int n = names.Count; + if (n == 0) + { + return Array.Empty(); + } + int[] array = new int[n]; + for (int i = 0; i < n; i++) + { + array[i] = GetIndexOfGlobal(names[i]); + } + return array; + } + + /// Updates a global binding in the module environment. + public void SetGlobal(string name, object value) + { + ArgumentNullException.ThrowIfNull(value); + SetGlobalByIndex(GetIndexOfGlobal(name), value); + } + + public override string ToString() => + string.Format(CultureInfo.InvariantCulture, "", clientData?.ToString() ?? "?"); +} diff --git a/src/Starlark/Eval/StarlarkCallable.cs b/src/Starlark/Eval/StarlarkCallable.cs new file mode 100644 index 000000000..834283778 --- /dev/null +++ b/src/Starlark/Eval/StarlarkCallable.cs @@ -0,0 +1,55 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Starlark.Syntax; + +namespace Starlark.Eval; + +/// +/// Implemented by all Starlark values that may be called like a function. Port of +/// net.starlark.java.eval.StarlarkCallable. +/// +public interface IStarlarkCallable : IStarlarkValue +{ + /// The "convenient" implementation of function calling. + object? Call(StarlarkThread thread, Tuple args, Dict kwargs) => + throw Starlark.Errorf("function {0} not implemented", Name); + + /// + /// The "fast" implementation of function calling. The default forwards to + /// after rejecting duplicate named arguments. is a flat array of + /// alternating name/value pairs. + /// + object? Fastcall(StarlarkThread thread, object?[] positional, object?[] named) + { + var kwargs = new Dict.Builder(); + var seen = new HashSet(); + for (int i = 0; i < named.Length; i += 2) + { + string key = (string)named[i]!; + if (!seen.Add(key)) + { + throw Starlark.Errorf("{0} got multiple values for parameter '{1}'", this, key); + } + kwargs.Put(key, named[i + 1]); + } + return Call(thread, Tuple.Of(positional), kwargs.Build(thread.Mutability)); + } + + /// Returns the form this callable value should take in a stack trace. + string Name { get; } + + /// Returns the location of the definition, or BUILTIN if not defined in Starlark. + Location Location => Location.BUILTIN; +} diff --git a/src/Starlark/Eval/StarlarkList.cs b/src/Starlark/Eval/StarlarkList.cs new file mode 100644 index 000000000..0f8664f59 --- /dev/null +++ b/src/Starlark/Eval/StarlarkList.cs @@ -0,0 +1,388 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// A StarlarkList is a mutable finite sequence of values. Port of +/// net.starlark.java.eval.StarlarkList. +/// +/// The Java implementation splits into several JVM-memory-optimized subclasses +/// (Regular/Immutable/Singleton/Lazy/Mutable). This .NET port collapses them into one class backed +/// by an object?[] plus a size, honoring the shared for freezing; +/// behavior is identical. +/// +[StarlarkBuiltin("list", Category = "core", Doc = "The built-in list type.")] +public sealed class StarlarkList : ISequence, IFreezable, IComparable +{ + internal const int MAX_ALLOC = 1 << 30; + private static readonly object?[] EMPTY_ARRAY = Array.Empty(); + + private static readonly StarlarkList EmptyImmutable = + new(Mutability.IMMUTABLE, EMPTY_ARRAY, 0); + + private object?[] elems; + private int size; + private int iteratorCount; + private Mutability mutability; + + private StarlarkList(Mutability mutability, object?[] elems, int size) + { + this.mutability = mutability; + this.elems = elems; + this.size = size; + } + + /// + /// Takes ownership of the array and returns a new StarlarkList wrapping it. If mutability is null + /// or frozen, the result is immutable. + /// + internal static StarlarkList Wrap(Mutability? mutability, object?[] elems) + { + if (mutability == null || mutability.IsFrozen) + { + if (elems.Length == 0) + { + return Empty(); + } + return new StarlarkList(Mutability.IMMUTABLE, elems, elems.Length); + } + return new StarlarkList(mutability, elems, elems.Length); + } + + /// Returns an empty frozen list. + public static StarlarkList Empty() => EmptyImmutable; + + /// Returns a new, empty list with the specified Mutability. + public static StarlarkList NewList(Mutability? mutability) => Wrap(mutability, EMPTY_ARRAY); + + /// Returns a StarlarkList with the given items and Mutability (null means immutable). + public static StarlarkList CopyOf(Mutability? mutability, IEnumerable elems) + { + if (mutability == null && elems is StarlarkList existing && existing.IsImmutable()) + { + return existing; + } + object?[] array = ToArray(elems); + foreach (object? e in array) + { + Starlark.CheckValid(e); + } + return Wrap(mutability, array); + } + + /// Returns an immutable list with the given elements. + public static StarlarkList ImmutableCopyOf(IEnumerable elems) => CopyOf(null, elems); + + /// Returns a StarlarkList with the given items and Mutability (null means immutable). + public static StarlarkList Of(Mutability? mutability, params object?[] elems) + { + if (elems.Length == 0) + { + return NewList(mutability); + } + foreach (object? e in elems) + { + Starlark.CheckValid(e); + } + return Wrap(mutability, (object?[])elems.Clone()); + } + + private static object?[] ToArray(IEnumerable elems) => + elems is ICollection c ? c.ToArray() : elems.ToArray(); + + // Returns the backing array (shared; caller must not modify beyond size). + internal object?[] Elems => elems; + + public Mutability Mutability => mutability; + + public int Count => size; + + public object? this[int index] + { + get + { + if (index < 0 || index >= size) + { + throw new IndexOutOfRangeException(index.ToString()); + } + return elems[index]; + } + } + + public void CheckHashable() => throw Starlark.Errorf("unhashable type: 'list'"); + + public bool IsImmutable() => mutability.IsFrozen; + + public bool UpdateIteratorCount(int delta) + { + if (mutability.IsFrozen) + { + return false; + } + if (delta > 0) + { + iteratorCount++; + } + else if (delta < 0) + { + iteratorCount--; + } + return iteratorCount > 0; + } + + public void UnsafeShallowFreeze() + { + Mutability.CheckUnsafeShallowFreezePrecondition(this); + mutability = Mutability.IMMUTABLE; + } + + /// Returns a new StarlarkList that is the concatenation of two lists. + public static StarlarkList Concat(StarlarkList x, StarlarkList y, Mutability? mutability) + { + int n = AddSizesAndFailIfExcessive(x.size, y.size); + object?[] res = new object?[n]; + Array.Copy(x.elems, 0, res, 0, x.size); + Array.Copy(y.elems, 0, res, x.size, y.size); + return Wrap(mutability, res); + } + + private static int AddSizesAndFailIfExcessive(int xsize, int ysize) + { + int sum = xsize + ysize; + if (sum < 0 || sum > MAX_ALLOC) + { + throw Starlark.Errorf("excessive capacity requested ({0} + {1} elements)", xsize, ysize); + } + return sum; + } + + public int CompareTo(StarlarkList? that) => Sequence.Compare(this, that!); + + public override bool Equals(object? that) => + ReferenceEquals(this, that) || (that is StarlarkList o && Sequence.SameElems(this, o)); + + public override int GetHashCode() + { + int result = 1; + for (int i = 0; i < size; i++) + { + result = unchecked(31 * result + (elems[i]?.GetHashCode() ?? 0)); + } + return unchecked(6047 + 4673 * result); + } + + public void Repr(Printer printer, StarlarkSemantics semantics) => + printer.PrintList(this, "[", ", ", "]", semantics); + + public override string ToString() => Starlark.Repr(this, StarlarkSemantics.DEFAULT); + + /// Returns a new StarlarkList containing n consecutive repeats of this list. + public StarlarkList Repeat(StarlarkInt n, Mutability? mutability) + { + if (n.Signum() <= 0) + { + return Wrap(mutability, EMPTY_ARRAY); + } + int ni = n.ToInt("repeat"); + long sz = (long)ni * size; + if (sz > MAX_ALLOC) + { + throw Starlark.Errorf("excessive repeat ({0} * {1} elements)", size, ni); + } + object?[] res = new object?[(int)sz]; + for (int i = 0; i < ni; i++) + { + Array.Copy(elems, 0, res, i * size, size); + } + return Wrap(mutability, res); + } + + public ISequence GetSlice(Mutability? mu, int start, int stop, int step) + { + var indices = new RangeList(start, stop, step); + int n = indices.Count; + object?[] res = new object?[n]; + if (step == 1) + { + Array.Copy(elems, indices.At(0), res, 0, n); + } + else + { + for (int i = 0; i < n; i++) + { + res[i] = elems[indices.At(i)]; + } + } + return Wrap(mu, res); + } + + // -- mutators (checked against freezing) -- + + private void CheckMutable() => Starlark.CheckMutable(this); + + private void EnsureCapacity(int min) + { + if (min > elems.Length) + { + int newCap = Math.Max(min, elems.Length == 0 ? 4 : elems.Length * 2); + Array.Resize(ref elems, newCap); + } + } + + /// Appends an element to the end of the list. + public void AddElement(object? element) + { + CheckMutable(); + EnsureCapacity(size + 1); + elems[size++] = element; + } + + /// Inserts an element at a given (already validated) position. + public void AddElementAt(int index, object? element) + { + CheckMutable(); + EnsureCapacity(size + 1); + Array.Copy(elems, index, elems, index + 1, size - index); + elems[index] = element; + size++; + } + + /// Appends all the elements to the end of the list. + public void AddElements(IEnumerable elements) + { + CheckMutable(); + if (elements is StarlarkList sl) + { + EnsureCapacity(size + sl.size); + Array.Copy(sl.elems, 0, elems, size, sl.size); + size += sl.size; + return; + } + foreach (object? e in elements) + { + AddElement(e); + } + } + + /// Removes the element at a given (already validated) index. + public void RemoveElementAt(int index) + { + CheckMutable(); + Array.Copy(elems, index + 1, elems, index, size - index - 1); + elems[--size] = null; + } + + /// Sets the element at the given index. Precondition: 0 <= index < Count. + public void SetElementAt(int index, object? value) + { + CheckMutable(); + elems[index] = value; + } + + [StarlarkMethod("remove", Doc = "Removes the first item whose value is x.")] + public void RemoveElement([Param(Name = "x")] object? x) + { + for (int i = 0; i < size; i++) + { + if (Equals(elems[i], x)) + { + RemoveElementAt(i); + return; + } + } + throw Starlark.Errorf("item {0} not found in list", Starlark.Repr(x, StarlarkSemantics.DEFAULT)); + } + + [StarlarkMethod("append", Doc = "Adds an item to the end of the list.")] + public void Append([Param(Name = "item")] object? item) => AddElement(item); + + [StarlarkMethod("clear", Doc = "Removes all the elements of the list.")] + public void ClearElements() + { + CheckMutable(); + Array.Clear(elems, 0, size); + size = 0; + } + + [StarlarkMethod("insert", Doc = "Inserts an item at a given position.")] + public void Insert( + [Param(Name = "index")] StarlarkInt index, [Param(Name = "item")] object? item) => + AddElementAt(ToSliceBound(index.ToInt("index"), size), item); + + [StarlarkMethod("extend", Doc = "Adds all items to the end of the list.")] + public void Extend([Param(Name = "items")] IEnumerable items) => AddElements(items); + + [StarlarkMethod("index", Doc = "Returns the index of the first item whose value is x.")] + public int Index( + [Param(Name = "x")] object? x, + [Param(Name = "start", DefaultValue = "unbound")] object start, + [Param(Name = "end", DefaultValue = "unbound")] object end) + { + int i = ReferenceEquals(start, Starlark.UNBOUND) + ? 0 + : ToSliceBound(Starlark.ToInt(start, "start"), size); + int j = ReferenceEquals(end, Starlark.UNBOUND) + ? size + : ToSliceBound(Starlark.ToInt(end, "end"), size); + for (; i < j; i++) + { + if (Equals(elems[i], x)) + { + return i; + } + } + throw Starlark.Errorf("item {0} not found in list", Starlark.Repr(x, StarlarkSemantics.DEFAULT)); + } + + [StarlarkMethod("pop", Doc = "Removes the item at the given position and returns it.")] + public object? Pop([Param(Name = "i", DefaultValue = "-1")] StarlarkInt arg) + { + int index = EvalUtils.GetSequenceIndex(arg.ToInt("i"), size); + object? result = elems[index]; + RemoveElementAt(index); + return result; + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < size; i++) + { + yield return elems[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + // Clamps an index into [0, length] as for a slice start bound (port of SyntaxUtils.toSliceBound). + private static int ToSliceBound(int index, int length) + { + if (index < 0) + { + index += length; + if (index < 0) + { + index = 0; + } + } + else if (index > length) + { + index = length; + } + return index; + } +} diff --git a/src/Starlark/Eval/StarlarkThread.cs b/src/Starlark/Eval/StarlarkThread.cs new file mode 100644 index 000000000..848acdb6c --- /dev/null +++ b/src/Starlark/Eval/StarlarkThread.cs @@ -0,0 +1,216 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using Starlark.Syntax; + +namespace Starlark.Eval; + +/// +/// A StarlarkThread represents a Starlark thread: the stack of active function calls plus per-thread +/// application state. Port of net.starlark.java.eval.StarlarkThread. +/// +/// Deferred vs. Java: CPU/wall profiler, debugger hooks, and the SymbolGenerator are omitted +/// pending the evaluator port; the mutability, semantics, thread-locals, call stack, and print/load +/// handlers are complete. +/// +public sealed class StarlarkThread +{ + private readonly Mutability mutability; + private readonly StarlarkSemantics semantics; + private readonly bool allowRecursion; + + private readonly Dictionary threadLocals = new(); + + // Stack of active function calls. + private readonly List callstack = new(); + + private PrintHandler printHandler = DefaultPrintHandler; + private Loader? loader; + private Func uncheckedExceptionContext = () => ""; + + internal long steps; // count of logical computation steps executed so far + internal long stepLimit = long.MaxValue; + + /// A hook for notifications of assignments at top level. + internal PostAssignHook? postAssignHook; + + private StarlarkThread(Mutability mu, StarlarkSemantics semantics, string contextDescription) + { + if (mu.IsFrozen) + { + throw new ArgumentException("mutability must not be frozen"); + } + mutability = mu; + this.semantics = semantics; + allowRecursion = semantics.GetBool(StarlarkSemantics.ALLOW_RECURSION); + if (!string.IsNullOrEmpty(contextDescription)) + { + uncheckedExceptionContext = () => contextDescription; + } + } + + /// Creates a StarlarkThread. + public static StarlarkThread Create( + Mutability mu, StarlarkSemantics semantics, string contextDescription = "") => + new(mu, semantics, contextDescription); + + /// Creates a StarlarkThread with an empty context description. + public static StarlarkThread CreateTransient(Mutability mu, StarlarkSemantics semantics) => + new(mu, semantics, ""); + + /// Returns the number of Starlark computation steps executed by this thread. + public long GetExecutedSteps() => steps; + + public void IncrementExecutedSteps(long delta) => steps += delta; + + public void SetMaxExecutionSteps(long steps) => stepLimit = steps; + + public long GetMaxExecutionSteps() => stepLimit; + + /// Saves a thread-local value keyed by type. + public void SetThreadLocal(T value) where T : notnull => threadLocals[typeof(T)] = value; + + /// Returns the most recently set thread-local for the key type, or default. + public T? GetThreadLocal() => + threadLocals.TryGetValue(typeof(T), out object? v) ? (T)v : default; + + /// Returns the mutability for values created by this thread. + public Mutability Mutability => mutability; + + public StarlarkSemantics GetSemantics() => semantics; + + /// Reports whether this thread is allowed to make recursive calls. + internal bool IsRecursionAllowed() => allowRecursion; + + /// A Frame records information about an active function call. + internal sealed class Frame + { + internal readonly IStarlarkCallable Fn; + internal Location Loc; + internal bool ErrorLocationSet; + internal object? Result = Starlark.None; + internal object?[]? Locals; + + internal Frame(IStarlarkCallable fn) + { + Fn = fn; + Loc = fn.Location; + } + + internal void SetLocation(Location loc) => Loc = loc; + + internal void SetErrorLocation(Location loc) + { + if (!ErrorLocationSet) + { + ErrorLocationSet = true; + Loc = loc; + } + } + + public override string ToString() => Fn.Name + "@" + Loc; + } + + /// Pushes a function onto the call stack. + internal void Push(IStarlarkCallable fn) => callstack.Add(new Frame(fn)); + + /// Pops a function off the call stack. + internal void Pop() => callstack.RemoveAt(callstack.Count - 1); + + internal Frame FrameAt(int depth) => callstack[callstack.Count - 1 - depth]; + + private bool Toplevel => callstack.Count < 2; + + /// Returns the location of the program counter in the enclosing call frame. + public Location GetCallerLocation() => Toplevel ? Location.BUILTIN : FrameAt(1).Loc; + + /// Returns the size of the callstack. + internal int GetCallStackSize() => callstack.Count; + + /// Determines how a Starlark thread deals with print statements. + public delegate void PrintHandler(StarlarkThread thread, string msg); + + internal PrintHandler GetPrintHandler() => printHandler; + + /// Sets the behavior of Starlark print statements executed by this thread. + public void SetPrintHandler(PrintHandler h) => + printHandler = h ?? throw new ArgumentNullException(nameof(h)); + + private static void DefaultPrintHandler(StarlarkThread thread, string msg) => + Console.Error.WriteLine(thread.GetCallerLocation() + ": " + msg); + + /// Determines the behavior of load statements. Returns the named module, or null. + public delegate Module? Loader(string module); + + internal Loader? GetLoader() => loader; + + public void SetLoader(Loader loader) => + this.loader = loader ?? throw new ArgumentNullException(nameof(loader)); + + public void SetUncheckedExceptionContext(Func context) => + uncheckedExceptionContext = context ?? throw new ArgumentNullException(nameof(context)); + + public string GetContextDescription() => uncheckedExceptionContext(); + + /// Specifies a hook function to be run after each assignment at top level. + public void SetPostAssignHook(PostAssignHook hook) => postAssignHook = hook; + + /// A hook for notifications of assignments at top level. + public delegate void PostAssignHook(string name, Location nameStartLocation, object value); + + /// The name for the implicit function that executes a file's top-level statements. + public const string TOP_LEVEL = ""; + + /// Creates a new CallStackEntry. + public static CallStackEntry NewCallStackEntry(string name, Location location) => + new(name, location); + + /// Describes the name and PC location of an active function call. + public sealed class CallStackEntry + { + public string Name { get; } + public Location Location { get; } + + internal CallStackEntry(string name, Location location) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Location = location ?? throw new ArgumentNullException(nameof(location)); + } + + public override string ToString() => Name + "@" + Location; + + public override int GetHashCode() => 31 * Name.GetHashCode() + Location.GetHashCode(); + + public override bool Equals(object? o) => + o is CallStackEntry that && Name == that.Name && Location.Equals(that.Location); + } + + /// Returns this thread's current stack of active function calls, outermost first. + public ImmutableArray GetCallStack() + { + var stack = ImmutableArray.CreateBuilder(callstack.Count); + foreach (Frame fr in callstack) + { + stack.Add(NewCallStackEntry(fr.Fn.Name, fr.Loc)); + } + return stack.ToImmutable(); + } + + public override int GetHashCode() => throw new NotSupportedException(); // avoid nondeterminism + + public override bool Equals(object? that) => throw new NotSupportedException(); + + public override string ToString() => $""; +} diff --git a/src/Starlark/Eval/Tuple.cs b/src/Starlark/Eval/Tuple.cs new file mode 100644 index 000000000..acc50101b --- /dev/null +++ b/src/Starlark/Eval/Tuple.cs @@ -0,0 +1,164 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// A Tuple is an immutable finite sequence of values. Port of net.starlark.java.eval.Tuple. +/// +/// The Java Regular/Singleton subclass split is collapsed to a single array-backed class. +/// +[StarlarkBuiltin("tuple", Category = "core", Doc = "The built-in tuple type.")] +public sealed class Tuple : ISequence, IComparable +{ + private static readonly Tuple EmptyTuple = new(Array.Empty()); + + private readonly object?[] elems; + + private Tuple(object?[] elems) => this.elems = elems; + + /// Returns the empty tuple. + public static Tuple Empty() => EmptyTuple; + + /// Returns a Tuple that wraps the array, which must not be subsequently modified. + internal static Tuple Wrap(object?[] array) => array.Length == 0 ? EmptyTuple : new Tuple(array); + + /// Returns a tuple containing the given elements. + public static Tuple CopyOf(IEnumerable seq) + { + if (seq is Tuple t) + { + return t; + } + object?[] array = seq is ICollection c ? c.ToArray() : seq.ToArray(); + return Wrap(array); + } + + /// Returns a tuple containing the given elements. + public static Tuple Of(params object?[] elems) => Wrap((object?[])elems.Clone()); + + /// Returns a two-element tuple. + public static Tuple Pair(object? a, object? b) => Wrap(new[] { a, b }); + + /// Returns a three-element tuple. + public static Tuple Triple(object? a, object? b, object? c) => Wrap(new[] { a, b, c }); + + /// Returns a tuple that is the concatenation of two tuples. + public static Tuple Concat(Tuple x, Tuple y) + { + if (x.elems.Length == 0) + { + return y; + } + if (y.elems.Length == 0) + { + return x; + } + object?[] res = new object?[x.elems.Length + y.elems.Length]; + Array.Copy(x.elems, 0, res, 0, x.elems.Length); + Array.Copy(y.elems, 0, res, x.elems.Length, y.elems.Length); + return Wrap(res); + } + + public int Count => elems.Length; + + public object? this[int index] => elems[index]; + + public bool IsImmutable() + { + foreach (object? e in elems) + { + if (!Starlark.IsImmutable(e)) + { + return false; + } + } + return true; + } + + public int CompareTo(Tuple? that) => Sequence.Compare(this, that!); + + public override bool Equals(object? that) => + ReferenceEquals(this, that) || (that is Tuple o && Sequence.SameElems(this, o)); + + public override int GetHashCode() + { + // Match the semantics of Java's AbstractList.hashCode for tuples. + int result = 1; + foreach (object? e in elems) + { + result = unchecked(31 * result + (e?.GetHashCode() ?? 0)); + } + return result; + } + + public void Repr(Printer printer, StarlarkSemantics semantics) + { + printer.Append("("); + string sep = ""; + foreach (object? e in elems) + { + printer.Append(sep); + sep = ", "; + printer.Repr(e, semantics); + } + if (elems.Length == 1) + { + printer.Append(","); + } + printer.Append(")"); + } + + public override string ToString() => Starlark.Repr(this, StarlarkSemantics.DEFAULT); + + public ISequence GetSlice(Mutability? mu, int start, int stop, int step) + { + var indices = new RangeList(start, stop, step); + int n = indices.Count; + object?[] res = new object?[n]; + for (int i = 0; i < n; i++) + { + res[i] = elems[indices.At(i)]; + } + return Wrap(res); + } + + /// Returns a Tuple containing n consecutive repeats of this tuple. + public Tuple Repeat(StarlarkInt n) + { + if (n.Signum() <= 0 || elems.Length == 0) + { + return EmptyTuple; + } + int ni = n.ToInt("repeat"); + long sz = (long)ni * elems.Length; + if (sz > StarlarkList.MAX_ALLOC) + { + throw Starlark.Errorf("excessive repeat ({0} * {1} elements)", elems.Length, ni); + } + object?[] res = new object?[(int)sz]; + for (int i = 0; i < ni; i++) + { + Array.Copy(elems, 0, res, i * elems.Length, elems.Length); + } + return Wrap(res); + } + + public IEnumerator GetEnumerator() => ((IEnumerable)elems).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => elems.GetEnumerator(); +} diff --git a/src/Starlark/Syntax/AssignmentStatement.cs b/src/Starlark/Syntax/AssignmentStatement.cs new file mode 100644 index 000000000..bdc537eff --- /dev/null +++ b/src/Starlark/Syntax/AssignmentStatement.cs @@ -0,0 +1,90 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// Syntax node for an assignment statement (lhs = rhs) or augmented assignment statement +/// (lhs op= rhs). +/// +public sealed class AssignmentStatement : Statement +{ + private readonly Expression lhs; // = IDENTIFIER | DOT | INDEX | LIST_EXPR + + // non-null only when lhs is an identifier and we're not augmented + private readonly Expression? type; + + private readonly TokenKind? op; + private readonly int opOffset; + + private readonly Expression rhs; + + private readonly DocComments? docComments; + + internal AssignmentStatement( + FileLocations locs, + Expression lhs, + Expression? type, + TokenKind? op, + int opOffset, + Expression rhs, + DocComments? docComments) + : base(locs, StatementKind.ASSIGNMENT) + { + this.lhs = lhs; + this.type = type; + this.op = op; + this.opOffset = opOffset; + this.rhs = rhs; + this.docComments = docComments; + if (type != null) + { + if (lhs.Kind != ExpressionKind.IDENTIFIER) + { + throw new InvalidOperationException("Can't have type annotation on complex LHS"); + } + if (op != null) + { + throw new InvalidOperationException("Can't have augmented assignment with type annotation"); + } + } + } + + /// Returns the LHS of the assignment. + public Expression GetLHS() => lhs; + + /// Returns the type expression (if present) of the variable on the LHS. + public Expression? GetType() => type; + + /// Returns the operator of an augmented assignment, or null for an ordinary assignment. + public TokenKind? GetOperator() => op; + + /// Returns the location of the assignment operator. + public Location GetOperatorLocation() => Locs.GetLocation(opOffset); + + public override int GetStartOffset() => lhs.GetStartOffset(); + + public override int GetEndOffset() => rhs.GetEndOffset(); + + /// Reports whether this is an augmented assignment (GetOperator() != null). + public bool IsAugmented() => op != null; + + /// Returns the RHS of the assignment. + public Expression GetRHS() => rhs; + + /// Returns the Sphinx autodoc-style doc comments attached to this statement, if any. + public DocComments? GetDocComments() => docComments; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/CastExpression.cs b/src/Starlark/Syntax/CastExpression.cs new file mode 100644 index 000000000..3a68e581b --- /dev/null +++ b/src/Starlark/Syntax/CastExpression.cs @@ -0,0 +1,68 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text; + +namespace Starlark.Syntax; + +/// Syntax node for cast() expressions. +public sealed class CastExpression : Expression +{ + private readonly int startOffset; + private readonly Expression type; + private readonly Expression value; + private readonly int rparenOffset; + // Set by type tagging. + private StarlarkType? starlarkType; + + internal CastExpression( + FileLocations locs, int startOffset, Expression type, Expression value, int rparenOffset) + : base(locs, ExpressionKind.CAST) + { + this.startOffset = startOffset; + this.type = type; + this.value = value; + this.rparenOffset = rparenOffset; + } + + public override int GetStartOffset() => startOffset; + + public override int GetEndOffset() => rparenOffset + 1; + + public Expression GetType() => type; + + /// + /// Returns the Starlark type extracted from the expression. Non-null after + /// type tagging. + /// + public StarlarkType? GetStarlarkType() => starlarkType; + + /// Intended for use by the type tagger. + internal void SetStarlarkType(StarlarkType starlarkType) => this.starlarkType = starlarkType; + + public Expression GetValue() => value; + + public override string ToString() + { + var buf = new StringBuilder(); + buf.Append("cast("); + buf.Append(type); + buf.Append(", "); + buf.Append(value); + buf.Append(')'); + return buf.ToString(); + } + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/Comprehension.cs b/src/Starlark/Syntax/Comprehension.cs new file mode 100644 index 000000000..26bc7072b --- /dev/null +++ b/src/Starlark/Syntax/Comprehension.cs @@ -0,0 +1,121 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// +/// Syntax node for list and dict comprehensions. +/// +/// A comprehension contains one or more clauses, e.g. [a+d for a in b if c for d in e] +/// contains three clauses: "for a in b", "if c", "for d in e". +/// +public sealed class Comprehension : Expression +{ + /// For or If. + public abstract class Clause : Node + { + private protected Clause(FileLocations locs) + : base(locs) + { + } + } + + /// A for clause in a comprehension, e.g. "for a in b" in the example above. + public sealed class For : Clause + { + private readonly int forOffset; + private readonly Expression vars; + private readonly Expression iterable; + + internal For(FileLocations locs, int forOffset, Expression vars, Expression iterable) + : base(locs) + { + this.forOffset = forOffset; + this.vars = vars; + this.iterable = iterable; + } + + public Expression GetVars() => vars; + + public Expression GetIterable() => iterable; + + public override int GetStartOffset() => forOffset; + + public override int GetEndOffset() => iterable.GetEndOffset(); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + } + + /// An if clause in a comprehension, e.g. "if c" in the example above. + public sealed class If : Clause + { + private readonly int ifOffset; + private readonly Expression condition; + + internal If(FileLocations locs, int ifOffset, Expression condition) + : base(locs) + { + this.ifOffset = ifOffset; + this.condition = condition; + } + + public Expression GetCondition() => condition; + + public override int GetStartOffset() => ifOffset; + + public override int GetEndOffset() => condition.GetEndOffset(); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + } + + private readonly bool isDict; // {k: v for vars in iterable} + private readonly int lbracketOffset; + private readonly Node body; // Expression or DictExpression.Entry + private readonly ImmutableArray clauses; + private readonly int rbracketOffset; + + internal Comprehension( + FileLocations locs, + bool isDict, + int lbracketOffset, + Node body, + ImmutableArray clauses, + int rbracketOffset) + : base(locs, ExpressionKind.COMPREHENSION) + { + this.isDict = isDict; + this.lbracketOffset = lbracketOffset; + this.body = body; + this.clauses = clauses; + this.rbracketOffset = rbracketOffset; + } + + public bool IsDict() => isDict; + + /// + /// Returns the loop body: an expression for a list comprehension, or a DictExpression.Entry for a + /// dict comprehension. + /// + public Node GetBody() => body; + + public IReadOnlyList GetClauses() => clauses; + + public override int GetStartOffset() => lbracketOffset; + + public override int GetEndOffset() => rbracketOffset + 1; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/DefStatement.cs b/src/Starlark/Syntax/DefStatement.cs new file mode 100644 index 000000000..ea3783172 --- /dev/null +++ b/src/Starlark/Syntax/DefStatement.cs @@ -0,0 +1,80 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Text; + +namespace Starlark.Syntax; + +/// Syntax node for a 'def' statement, which defines a function. +public sealed class DefStatement : Statement +{ + private readonly int defOffset; + private readonly Identifier identifier; + private readonly ImmutableArray typeParameters; // No type params => empty list + private readonly ImmutableArray body; // non-empty if well formed + private readonly ImmutableArray parameters; + private readonly Expression? returnType; // No return type => null + + // set by resolver + private Resolver.Function? resolved; + + internal DefStatement( + FileLocations locs, + int defOffset, + Identifier identifier, + ImmutableArray typeParameters, + ImmutableArray parameters, + Expression? returnType, + ImmutableArray body) + : base(locs, StatementKind.DEF) + { + this.defOffset = defOffset; + this.identifier = identifier; + this.typeParameters = typeParameters; + this.parameters = parameters; + this.returnType = returnType; + this.body = body; + } + + public override string ToString() + { + var buf = new StringBuilder(); + new NodePrinter(buf).PrintDefSignature(this); + buf.Append(" ...\n"); + return buf.ToString(); + } + + public Identifier GetIdentifier() => identifier; + + public IReadOnlyList GetBody() => body; + + public IReadOnlyList GetTypeParameters() => typeParameters; + + public IReadOnlyList GetParameters() => parameters; + + public Expression? GetReturnType() => returnType; + + internal void SetResolvedFunction(Resolver.Function resolved) => this.resolved = resolved; + + /// Returns information about the resolved function. Set by the resolver. + public Resolver.Function? GetResolvedFunction() => resolved; + + public override int GetStartOffset() => defOffset; + + public override int GetEndOffset() => + body.IsEmpty ? identifier.GetEndOffset() : body[body.Length - 1].GetEndOffset(); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/DocComments.cs b/src/Starlark/Syntax/DocComments.cs new file mode 100644 index 000000000..a321ad434 --- /dev/null +++ b/src/Starlark/Syntax/DocComments.cs @@ -0,0 +1,51 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Linq; + +namespace Starlark.Syntax; + +/// A block of Sphinx autodoc-style doc comments. +public sealed class DocComments +{ + private readonly ImmutableArray lines; + + public DocComments(IReadOnlyList lines) + { + if (lines.Count == 0) + { + throw new ArgumentException("no lines"); + } + if (!lines.All(c => c.HasDocCommentPrefix())) + { + throw new ArgumentException("all lines must have a doc comment prefix"); + } + this.lines = lines.ToImmutableArray(); + } + + public IReadOnlyList GetLines() => lines; + + public Location GetStartLocation() => lines[0].GetStartLocation(); + + public Location GetEndLocation() => lines[lines.Length - 1].GetEndLocation(); + + /// + /// Returns the text content (trimmed of the leading #: or #: prefixes, and joined + /// with newlines) of the doc comment block. + /// + public string GetText() => string.Join("\n", lines.Select(c => c.GetDocCommentText())); + + public override string ToString() => string.Join("\n", lines.Select(c => c.ToString())); +} diff --git a/src/Starlark/Syntax/ExpressionStatement.cs b/src/Starlark/Syntax/ExpressionStatement.cs new file mode 100644 index 000000000..86468296c --- /dev/null +++ b/src/Starlark/Syntax/ExpressionStatement.cs @@ -0,0 +1,35 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Syntax node for a statement consisting of an expression evaluated for effect. +public sealed class ExpressionStatement : Statement +{ + private readonly Expression expression; + + internal ExpressionStatement(FileLocations locs, Expression expression) + : base(locs, StatementKind.EXPRESSION) + { + this.expression = expression; + } + + public Expression GetExpression() => expression; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + + public override int GetStartOffset() => expression.GetStartOffset(); + + public override int GetEndOffset() => expression.GetEndOffset(); +} diff --git a/src/Starlark/Syntax/FlowStatement.cs b/src/Starlark/Syntax/FlowStatement.cs new file mode 100644 index 000000000..1e1580bab --- /dev/null +++ b/src/Starlark/Syntax/FlowStatement.cs @@ -0,0 +1,39 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// A class for flow statements (break, continue, and pass). +public sealed class FlowStatement : Statement +{ + private readonly TokenKind flowKind; // BREAK | CONTINUE | PASS + private readonly int offset; + + internal FlowStatement(FileLocations locs, TokenKind flowKind, int offset) + : base(locs, StatementKind.FLOW) + { + this.flowKind = flowKind; + this.offset = offset; + } + + public TokenKind GetFlowKind() => flowKind; + + public override string ToString() => flowKind.ToDisplayString() + "\n"; + + public override int GetStartOffset() => offset; + + public override int GetEndOffset() => offset + flowKind.ToDisplayString().Length; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/ForStatement.cs b/src/Starlark/Syntax/ForStatement.cs new file mode 100644 index 000000000..00c19278e --- /dev/null +++ b/src/Starlark/Syntax/ForStatement.cs @@ -0,0 +1,58 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// Syntax node for a for loop statement, for vars in iterable: .... +public sealed class ForStatement : Statement +{ + private readonly int forOffset; + private readonly Expression vars; + private readonly Expression iterable; + private readonly ImmutableArray body; // non-empty if well formed + + internal ForStatement( + FileLocations locs, + int forOffset, + Expression vars, + Expression iterable, + ImmutableArray body) + : base(locs, StatementKind.FOR) + { + this.forOffset = forOffset; + this.vars = vars; + this.iterable = iterable; + this.body = body; + } + + /// Returns variables assigned by each iteration. + public Expression GetVars() => vars; + + /// Returns the iterable value. + public Expression GetCollection() => iterable; + + /// Returns the statements of the loop body. Non-empty if parsing succeeded. + public IReadOnlyList GetBody() => body; + + public override int GetStartOffset() => forOffset; + + public override int GetEndOffset() => + body.IsEmpty ? iterable.GetEndOffset() : body[body.Length - 1].GetEndOffset(); + + public override string ToString() => "for " + vars + " in " + iterable + ": ...\n"; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/IfStatement.cs b/src/Starlark/Syntax/IfStatement.cs new file mode 100644 index 000000000..963690fef --- /dev/null +++ b/src/Starlark/Syntax/IfStatement.cs @@ -0,0 +1,66 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// Syntax node for an if or elif statement. +public sealed class IfStatement : Statement +{ + private readonly TokenKind token; // IF or ELIF + private readonly int ifOffset; + private readonly Expression condition; + // These blocks may be non-null but empty after a misparse: + private readonly ImmutableArray thenBlock; // non-empty + private ImmutableArray? elseBlock; // non-empty if non-null; set after construction + + internal IfStatement( + FileLocations locs, + TokenKind token, + int ifOffset, + Expression condition, + IReadOnlyList thenBlock) + : base(locs, StatementKind.IF) + { + this.token = token; + this.ifOffset = ifOffset; + this.condition = condition; + this.thenBlock = thenBlock.ToImmutableArray(); + } + + /// Reports whether this is an 'elif' statement. + public bool IsElif() => token == TokenKind.ELIF; + + public Expression GetCondition() => condition; + + public IReadOnlyList GetThenBlock() => thenBlock; + + public IReadOnlyList? GetElseBlock() => + elseBlock.HasValue ? elseBlock.Value : null; + + internal void SetElseBlock(ImmutableArray elseBlock) => this.elseBlock = elseBlock; + + public override int GetStartOffset() => ifOffset; + + public override int GetEndOffset() + { + ImmutableArray body = elseBlock ?? thenBlock; + return body.IsEmpty ? condition.GetEndOffset() : body[body.Length - 1].GetEndOffset(); + } + + public override string ToString() => string.Format("if {0}: ...\n", condition); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/IsInstanceExpression.cs b/src/Starlark/Syntax/IsInstanceExpression.cs new file mode 100644 index 000000000..bcdd51e1d --- /dev/null +++ b/src/Starlark/Syntax/IsInstanceExpression.cs @@ -0,0 +1,57 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text; + +namespace Starlark.Syntax; + +/// Syntax node for isinstance() expressions. +public sealed class IsInstanceExpression : Expression +{ + private readonly int startOffset; + private readonly Expression value; + private readonly Expression type; + private readonly int rparenOffset; + + internal IsInstanceExpression( + FileLocations locs, int startOffset, Expression value, Expression type, int rparenOffset) + : base(locs, ExpressionKind.ISINSTANCE) + { + this.startOffset = startOffset; + this.value = value; + this.type = type; + this.rparenOffset = rparenOffset; + } + + public override int GetStartOffset() => startOffset; + + public override int GetEndOffset() => rparenOffset + 1; + + public Expression GetValue() => value; + + public Expression GetType() => type; + + public override string ToString() + { + var buf = new StringBuilder(); + buf.Append("isinstance("); + buf.Append(value); + buf.Append(", "); + buf.Append(type); + buf.Append(')'); + return buf.ToString(); + } + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/LambdaExpression.cs b/src/Starlark/Syntax/LambdaExpression.cs new file mode 100644 index 000000000..21a7bda87 --- /dev/null +++ b/src/Starlark/Syntax/LambdaExpression.cs @@ -0,0 +1,52 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// A LambdaExpression (lambda params: body) denotes an anonymous function. +public sealed class LambdaExpression : Expression +{ + private readonly int lambdaOffset; // offset of 'lambda' token + private readonly ImmutableArray parameters; + private readonly Expression body; + + // set by resolver + private Resolver.Function? resolved; + + internal LambdaExpression( + FileLocations locs, int lambdaOffset, ImmutableArray parameters, Expression body) + : base(locs, ExpressionKind.LAMBDA) + { + this.lambdaOffset = lambdaOffset; + this.parameters = parameters; + this.body = body; + } + + public IReadOnlyList GetParameters() => parameters; + + public Expression GetBody() => body; + + /// Returns information about the resolved function. Set by the resolver. + public Resolver.Function? GetResolvedFunction() => resolved; + + internal void SetResolvedFunction(Resolver.Function resolved) => this.resolved = resolved; + + public override int GetStartOffset() => lambdaOffset; + + public override int GetEndOffset() => body.GetEndOffset(); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/LoadStatement.cs b/src/Starlark/Syntax/LoadStatement.cs new file mode 100644 index 000000000..07dcf9aa5 --- /dev/null +++ b/src/Starlark/Syntax/LoadStatement.cs @@ -0,0 +1,69 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// Syntax node for a load statement. +public sealed class LoadStatement : Statement +{ + /// + /// Binding represents a binding in a load statement. load("...", local = "orig"). + /// + public sealed class Binding + { + private readonly Identifier local; + private readonly Identifier orig; + + public Identifier GetLocalName() => local; + + public Identifier GetOriginalName() => orig; + + internal Binding(Identifier localName, Identifier originalName) + { + this.local = localName; + this.orig = originalName; + } + } + + private readonly int loadOffset; + private readonly StringLiteral module; + private readonly ImmutableArray bindings; + private readonly int rparenOffset; + + internal LoadStatement( + FileLocations locs, + int loadOffset, + StringLiteral module, + ImmutableArray bindings, + int rparenOffset) + : base(locs, StatementKind.LOAD) + { + this.loadOffset = loadOffset; + this.module = module; + this.bindings = bindings; + this.rparenOffset = rparenOffset; + } + + public IReadOnlyList GetBindings() => bindings; + + public StringLiteral GetImport() => module; + + public override int GetStartOffset() => loadOffset; + + public override int GetEndOffset() => rparenOffset + 1; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/NodePrinter.cs b/src/Starlark/Syntax/NodePrinter.cs new file mode 100644 index 000000000..fad72170d --- /dev/null +++ b/src/Starlark/Syntax/NodePrinter.cs @@ -0,0 +1,670 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Globalization; +using System.Text; + +namespace Starlark.Syntax; + +/// A pretty-printer for Starlark syntax trees. +internal sealed class NodePrinter +{ + private readonly StringBuilder buf; + private int indent; + + internal NodePrinter(StringBuilder buf) + { + this.buf = buf; + } + + internal NodePrinter(StringBuilder buf, int indent) + { + this.buf = buf; + this.indent = indent; + } + + // Main entry point for an arbitrary node. Called by Node.PrettyPrint. + internal void PrintNode(Node n) + { + if (n is Expression expr) + { + PrintExpr(expr); + } + else if (n is Statement stmt) + { + PrintStmt(stmt); + } + else if (n is StarlarkFile file) + { + foreach (Statement s in file.GetStatements()) + { + PrintStmt(s); + } + } + else if (n is Comment comment) + { + PrintIndent(); + buf.Append(comment.GetText()); + } + else if (n is Argument argument) + { + PrintArgument(argument); + } + else if (n is Parameter parameter) + { + PrintParameter(parameter); + } + else if (n is DictExpression.Entry entry) + { + PrintDictEntry(entry); + } + else + { + throw new ArgumentException("unexpected: " + n.GetType()); + } + } + + private void PrintSuite(IReadOnlyList statements) + { + indent++; + foreach (Statement stmt in statements) + { + PrintStmt(stmt); + } + indent--; + } + + private void PrintIndent() + { + for (int i = 0; i < indent; i++) + { + buf.Append(" "); + } + } + + private void PrintArgument(Argument arg) + { + if (arg is Argument.Positional) + { + // nop + } + else if (arg is Argument.Keyword keyword) + { + buf.Append(keyword.GetIdentifier().GetName()); + buf.Append(" = "); + } + else if (arg is Argument.Star) + { + buf.Append('*'); + } + else if (arg is Argument.StarStar) + { + buf.Append("**"); + } + PrintExpr(arg.GetValue(), true); + } + + private void PrintParameter(Parameter param) + { + if (param is Parameter.Mandatory) + { + buf.Append(param.GetName()); + } + else if (param is Parameter.Optional) + { + buf.Append(param.GetName()); + buf.Append('='); + PrintExpr(param.GetDefaultValue()!); + } + else if (param is Parameter.Star) + { + buf.Append('*'); + if (param.GetName() != null) + { + buf.Append(param.GetName()); + } + } + else if (param is Parameter.StarStar) + { + buf.Append("**"); + buf.Append(param.GetName()); + } + } + + private void PrintDictEntry(DictExpression.Entry e) + { + PrintExpr(e.GetKey()); + buf.Append(": "); + PrintExpr(e.GetValue()); + } + + // Appends "def f(a, ..., z):" to the buf. Also used by DefStatement.ToString. + internal void PrintDefSignature(DefStatement def) + { + buf.Append("def "); + PrintExpr(def.GetIdentifier()); + if (def.GetTypeParameters().Count != 0) + { + buf.Append('['); + string sep = ""; + foreach (Identifier typeParam in def.GetTypeParameters()) + { + buf.Append(sep); + PrintExpr(typeParam); + sep = ", "; + } + buf.Append(']'); + } + buf.Append('('); + string psep = ""; + foreach (Parameter param in def.GetParameters()) + { + buf.Append(psep); + PrintParameter(param); + if (param.GetType() != null) + { + buf.Append(": "); + PrintExpr(param.GetType()!, true); + } + psep = ", "; + } + buf.Append(')'); + if (def.GetReturnType() != null) + { + buf.Append(" -> "); + PrintExpr(def.GetReturnType()!, true); + } + buf.Append(':'); + } + + private void PrintStmt(Statement s) + { + PrintIndent(); + + switch (s.Kind) + { + case Statement.StatementKind.ASSIGNMENT: + { + var stmt = (AssignmentStatement)s; + PrintExpr(stmt.GetLHS()); + Expression? type = stmt.GetType(); + if (type != null) + { + buf.Append(" : "); + PrintExpr(type); + } + buf.Append(' '); + if (stmt.IsAugmented()) + { + buf.Append(stmt.GetOperator()!.Value.ToDisplayString()); + } + buf.Append("= "); + PrintExpr(stmt.GetRHS()); + buf.Append('\n'); + break; + } + + case Statement.StatementKind.EXPRESSION: + { + var stmt = (ExpressionStatement)s; + PrintExpr(stmt.GetExpression()); + buf.Append('\n'); + break; + } + + case Statement.StatementKind.FLOW: + { + var stmt = (FlowStatement)s; + buf.Append(stmt.GetFlowKind().ToDisplayString()).Append('\n'); + break; + } + + case Statement.StatementKind.FOR: + { + var stmt = (ForStatement)s; + buf.Append("for "); + PrintExpr(stmt.GetVars()); + buf.Append(" in "); + PrintExpr(stmt.GetCollection()); + buf.Append(":\n"); + PrintSuite(stmt.GetBody()); + break; + } + + case Statement.StatementKind.DEF: + { + var stmt = (DefStatement)s; + PrintDefSignature(stmt); + buf.Append('\n'); + PrintSuite(stmt.GetBody()); + break; + } + + case Statement.StatementKind.IF: + { + var stmt = (IfStatement)s; + buf.Append(stmt.IsElif() ? "elif " : "if "); + PrintExpr(stmt.GetCondition()); + buf.Append(":\n"); + PrintSuite(stmt.GetThenBlock()); + IReadOnlyList? elseBlock = stmt.GetElseBlock(); + if (elseBlock != null) + { + if (elseBlock.Count == 1 + && elseBlock[0] is IfStatement inner + && inner.IsElif()) + { + PrintStmt(elseBlock[0]); + } + else + { + PrintIndent(); + buf.Append("else:\n"); + PrintSuite(elseBlock); + } + } + break; + } + + case Statement.StatementKind.LOAD: + { + var stmt = (LoadStatement)s; + buf.Append("load("); + PrintExpr(stmt.GetImport()); + foreach (LoadStatement.Binding binding in stmt.GetBindings()) + { + buf.Append(", "); + Identifier local = binding.GetLocalName(); + string origName = binding.GetOriginalName().GetName(); + if (origName == local.GetName()) + { + buf.Append('"'); + PrintExpr(local); + buf.Append('"'); + } + else + { + PrintExpr(local); + buf.Append("=\""); + buf.Append(origName); + buf.Append('"'); + } + } + buf.Append(")\n"); + break; + } + + case Statement.StatementKind.RETURN: + { + var stmt = (ReturnStatement)s; + buf.Append("return"); + if (stmt.GetResult() != null) + { + buf.Append(' '); + PrintExpr(stmt.GetResult()!); + } + buf.Append('\n'); + break; + } + + case Statement.StatementKind.TYPE_ALIAS: + { + var stmt = (TypeAliasStatement)s; + buf.Append("type "); + PrintExpr(stmt.GetIdentifier()); + if (stmt.GetParameters().Count != 0) + { + buf.Append('['); + string sep = ""; + foreach (Identifier param in stmt.GetParameters()) + { + buf.Append(sep); + PrintExpr(param); + sep = ", "; + } + buf.Append(']'); + } + buf.Append(" = "); + PrintExpr(stmt.GetDefinition(), true); + buf.Append('\n'); + break; + } + + case Statement.StatementKind.VAR: + { + var stmt = (VarStatement)s; + PrintExpr(stmt.GetIdentifier()); + buf.Append(" : "); + PrintExpr(stmt.GetType()); + buf.Append('\n'); + break; + } + } + } + + private void PrintExpr(Expression expr) + { + PrintExpr(expr, false); + } + + private void PrintExpr(Expression expr, bool canSkipParenthesis) + { + switch (expr.Kind) + { + case Expression.ExpressionKind.BINARY_OPERATOR: + { + var binop = (BinaryOperatorExpression)expr; + if (!canSkipParenthesis) + { + buf.Append('('); + } + PrintExpr(binop.GetX()); + buf.Append(' '); + buf.Append(binop.GetOperator().ToDisplayString()); + buf.Append(' '); + PrintExpr(binop.GetY()); + if (!canSkipParenthesis) + { + buf.Append(')'); + } + break; + } + + case Expression.ExpressionKind.COMPREHENSION: + { + var comp = (Comprehension)expr; + buf.Append(comp.IsDict() ? '{' : '['); + PrintNode(comp.GetBody()); + foreach (Comprehension.Clause clause in comp.GetClauses()) + { + buf.Append(' '); + if (clause is Comprehension.For forClause) + { + buf.Append("for "); + PrintExpr(forClause.GetVars()); + buf.Append(" in "); + PrintExpr(forClause.GetIterable()); + } + else + { + var ifClause = (Comprehension.If)clause; + buf.Append("if "); + PrintExpr(ifClause.GetCondition()); + } + } + buf.Append(comp.IsDict() ? '}' : ']'); + break; + } + + case Expression.ExpressionKind.CONDITIONAL: + { + var cond = (ConditionalExpression)expr; + PrintExpr(cond.GetThenCase()); + buf.Append(" if "); + PrintExpr(cond.GetCondition()); + buf.Append(" else "); + PrintExpr(cond.GetElseCase()); + break; + } + + case Expression.ExpressionKind.DICT_EXPR: + { + var dictexpr = (DictExpression)expr; + buf.Append('{'); + string sep = ""; + foreach (DictExpression.Entry entry in dictexpr.GetEntries()) + { + buf.Append(sep); + PrintDictEntry(entry); + sep = ", "; + } + buf.Append('}'); + break; + } + + case Expression.ExpressionKind.DOT: + { + var dot = (DotExpression)expr; + PrintExpr(dot.GetObject()); + buf.Append('.'); + PrintExpr(dot.GetField()); + break; + } + + case Expression.ExpressionKind.CALL: + { + var call = (CallExpression)expr; + PrintExpr(call.GetFunction()); + buf.Append('('); + string sep = ""; + foreach (Argument arg in call.GetArguments()) + { + buf.Append(sep); + PrintArgument(arg); + sep = ", "; + } + buf.Append(')'); + break; + } + + case Expression.ExpressionKind.CAST: + { + var cast = (CastExpression)expr; + buf.Append("cast("); + PrintExpr(cast.GetType(), true); + buf.Append(", "); + PrintExpr(cast.GetValue(), true); + buf.Append(')'); + break; + } + + case Expression.ExpressionKind.ELLIPSIS: + { + buf.Append("..."); + break; + } + + case Expression.ExpressionKind.IDENTIFIER: + buf.Append(((Identifier)expr).GetName()); + break; + + case Expression.ExpressionKind.INDEX: + { + var index = (IndexExpression)expr; + PrintExpr(index.GetObject()); + buf.Append('['); + PrintExpr(index.GetKey()); + buf.Append(']'); + break; + } + + case Expression.ExpressionKind.INT_LITERAL: + { + buf.Append(((IntLiteral)expr).GetValue()); + break; + } + + case Expression.ExpressionKind.ISINSTANCE: + { + var isinstance = (IsInstanceExpression)expr; + buf.Append("isinstance("); + PrintExpr(isinstance.GetValue(), true); + buf.Append(", "); + PrintExpr(isinstance.GetType(), true); + buf.Append(')'); + break; + } + + case Expression.ExpressionKind.FLOAT_LITERAL: + { + buf.Append(FormatDouble(((FloatLiteral)expr).GetValue())); + break; + } + + case Expression.ExpressionKind.LAMBDA: + { + var lambda = (LambdaExpression)expr; + buf.Append("lambda"); + string sep = " "; + foreach (Parameter param in lambda.GetParameters()) + { + buf.Append(sep); + sep = ", "; + PrintParameter(param); + } + buf.Append(": "); + PrintExpr(lambda.GetBody()); + break; + } + + case Expression.ExpressionKind.LIST_EXPR: + { + var list = (ListExpression)expr; + buf.Append(list.IsTuple() ? '(' : '['); + string sep = ""; + foreach (Expression e in list.GetElements()) + { + buf.Append(sep); + PrintExpr(e, true); + sep = ", "; + } + if (list.IsTuple() && list.GetElements().Count == 1) + { + buf.Append(','); + } + buf.Append(list.IsTuple() ? ')' : ']'); + break; + } + + case Expression.ExpressionKind.SLICE: + { + var slice = (SliceExpression)expr; + PrintExpr(slice.GetObject()); + buf.Append('['); + if (slice.GetStart() != null) + { + PrintExpr(slice.GetStart()!); + } + buf.Append(':'); + if (slice.GetStop() != null) + { + PrintExpr(slice.GetStop()!); + } + if (slice.GetStep() != null) + { + buf.Append(':'); + PrintExpr(slice.GetStep()!); + } + buf.Append(']'); + break; + } + + case Expression.ExpressionKind.STRING_LITERAL: + { + var literal = (StringLiteral)expr; + string value = literal.GetValue(); + buf.Append('"'); + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + switch (c) + { + case '"': + buf.Append("\\\""); + break; + case '\\': + buf.Append("\\\\"); + break; + case '\r': + buf.Append("\\r"); + break; + case '\n': + buf.Append("\\n"); + break; + case '\t': + buf.Append("\\t"); + break; + default: + if (c < 32) + { + buf.Append(string.Format("\\x{0:x2}", (int)c)); + } + else + { + buf.Append(c); + } + break; + } + } + buf.Append('"'); + break; + } + + case Expression.ExpressionKind.UNARY_OPERATOR: + { + var unop = (UnaryOperatorExpression)expr; + buf.Append(unop.GetOperator() == TokenKind.NOT ? "not " : unop.GetOperator().ToDisplayString()); + if (!canSkipParenthesis) + { + buf.Append('('); + } + PrintExpr(unop.GetX()); + if (!canSkipParenthesis) + { + buf.Append(')'); + } + break; + } + + case Expression.ExpressionKind.TYPE_APPLICATION: + { + var typeApplication = (TypeApplication)expr; + PrintExpr(typeApplication.GetConstructor()); + buf.Append('['); + string sep = ""; + foreach (Expression arg in typeApplication.GetArguments()) + { + buf.Append(sep); + PrintExpr(arg, true); + sep = ", "; + } + buf.Append(']'); + break; + } + } + } + + // Approximates Java's Double.toString formatting (always includes a decimal point). + private static string FormatDouble(double d) + { + if (double.IsNaN(d)) + { + return "NaN"; + } + if (double.IsPositiveInfinity(d)) + { + return "Infinity"; + } + if (double.IsNegativeInfinity(d)) + { + return "-Infinity"; + } + string s = d.ToString("R", CultureInfo.InvariantCulture); + if (s.IndexOf('.') < 0 && s.IndexOf('E') < 0 && s.IndexOf('e') < 0) + { + s += ".0"; + } + return s; + } +} diff --git a/src/Starlark/Syntax/NodeVisitor.cs b/src/Starlark/Syntax/NodeVisitor.cs new file mode 100644 index 000000000..420c6e843 --- /dev/null +++ b/src/Starlark/Syntax/NodeVisitor.cs @@ -0,0 +1,321 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// A visitor for visiting the nodes of a syntax tree in lexical order (not evaluation order!). +/// +/// Comments are *not* visited. +/// +public class NodeVisitor +{ + /// + /// If set, we only visit s that correspond to a definition or use of a + /// symbol in the current file. + /// + protected bool skipNonSymbolIdentifiers = false; + + /// Entrypoint for visiting a node. Clients should avoid calling node-specific overloads. + public virtual void Visit(Node node) + { + // Double-dispatch pattern. + node.Accept(this); + } + + // ==== Miscellaneous node types ==== + + /// Handles all four Argument node types uniformly. + public virtual void Visit(Argument node) + { + if (!skipNonSymbolIdentifiers && node is Argument.Keyword keyword) + { + Visit(keyword.GetIdentifier()); + } + Visit(node.GetValue()); + } + + /// Not supported. + public virtual void Visit(Comment node) + { + throw new NotSupportedException("NodeVisitor does not support visiting comments"); + } + + /// Handles all four Parameter node types uniformly. + public virtual void Visit(Parameter node) + { + if (node.GetIdentifier() != null) + { + Visit(node.GetIdentifier()!); + } + if (node.GetType() != null) + { + Visit(node.GetType()!); + } + if (node.GetDefaultValue() != null) + { + Visit(node.GetDefaultValue()!); + } + } + + public virtual void Visit(StarlarkFile node) + { + VisitBlock(node.GetStatements()); + } + + // ==== Statement nodes ==== + + public virtual void Visit(AssignmentStatement node) + { + Visit(node.GetLHS()); + if (node.GetType() != null) + { + Visit(node.GetType()!); + } + Visit(node.GetRHS()); + } + + public virtual void Visit(ExpressionStatement node) + { + Visit(node.GetExpression()); + } + + public virtual void Visit(FlowStatement node) + { + } + + public virtual void Visit(ForStatement node) + { + Visit(node.GetVars()); + Visit(node.GetCollection()); + VisitBlock(node.GetBody()); + } + + public virtual void Visit(DefStatement node) + { + Visit(node.GetIdentifier()); + VisitAll(node.GetTypeParameters()); + VisitAll(node.GetParameters()); + if (node.GetReturnType() != null) + { + Visit(node.GetReturnType()!); + } + VisitBlock(node.GetBody()); + } + + public virtual void Visit(IfStatement node) + { + Visit(node.GetCondition()); + VisitBlock(node.GetThenBlock()); + if (node.GetElseBlock() != null) + { + VisitBlock(node.GetElseBlock()!); + } + } + + public virtual void Visit(LoadStatement node) + { + foreach (LoadStatement.Binding binding in node.GetBindings()) + { + Visit(binding.GetLocalName()); + // We don't visit the original name. + } + } + + public virtual void Visit(ReturnStatement node) + { + if (node.GetResult() != null) + { + Visit(node.GetResult()!); + } + } + + public virtual void Visit(TypeAliasStatement node) + { + Visit(node.GetIdentifier()); + VisitAll(node.GetParameters()); + Visit(node.GetDefinition()); + } + + public virtual void Visit(VarStatement node) + { + Visit(node.GetIdentifier()); + Visit(node.GetType()); + } + + // ==== Expression nodes ==== + + public virtual void Visit(BinaryOperatorExpression node) + { + Visit(node.GetX()); + Visit(node.GetY()); + } + + public virtual void Visit(CallExpression node) + { + Visit(node.GetFunction()); + VisitAll(node.GetArguments()); + } + + public virtual void Visit(CastExpression node) + { + Visit(node.GetType()); + Visit(node.GetValue()); + } + + public virtual void Visit(Comprehension node) + { + Visit(node.GetBody()); + foreach (Comprehension.Clause clause in node.GetClauses()) + { + if (clause is Comprehension.For f) + { + Visit(f); + } + else + { + Visit((Comprehension.If)clause); + } + } + } + + public virtual void Visit(Comprehension.For node) + { + Visit(node.GetVars()); + Visit(node.GetIterable()); + } + + public virtual void Visit(Comprehension.If node) + { + Visit(node.GetCondition()); + } + + public virtual void Visit(ConditionalExpression node) + { + Visit(node.GetThenCase()); + Visit(node.GetCondition()); + if (node.GetElseCase() != null) + { + Visit(node.GetElseCase()); + } + } + + public virtual void Visit(DictExpression node) + { + VisitAll(node.GetEntries()); + } + + public virtual void Visit(DictExpression.Entry node) + { + Visit(node.GetKey()); + Visit(node.GetValue()); + } + + public virtual void Visit(DotExpression node) + { + Visit(node.GetObject()); + if (!skipNonSymbolIdentifiers) + { + Visit(node.GetField()); + } + } + + public virtual void Visit(Ellipsis node) + { + } + + public virtual void Visit(FloatLiteral node) + { + } + + public virtual void Visit(Identifier node) + { + } + + public virtual void Visit(IndexExpression node) + { + Visit(node.GetObject()); + Visit(node.GetKey()); + } + + public virtual void Visit(IntLiteral node) + { + } + + public virtual void Visit(IsInstanceExpression node) + { + Visit(node.GetValue()); + Visit(node.GetType()); + } + + public virtual void Visit(LambdaExpression node) + { + VisitAll(node.GetParameters()); + Visit(node.GetBody()); + } + + public virtual void Visit(ListExpression node) + { + VisitAll(node.GetElements()); + } + + public virtual void Visit(SliceExpression node) + { + Visit(node.GetObject()); + if (node.GetStart() != null) + { + Visit(node.GetStart()!); + } + if (node.GetStop() != null) + { + Visit(node.GetStop()!); + } + if (node.GetStep() != null) + { + Visit(node.GetStep()!); + } + } + + public virtual void Visit(StringLiteral node) + { + } + + public virtual void Visit(UnaryOperatorExpression node) + { + Visit(node.GetX()); + } + + public virtual void Visit(TypeApplication node) + { + Visit(node.GetConstructor()); + VisitAll(node.GetArguments()); + } + + // ==== Helpers for sequences of nodes ==== + + /// Visits a sequence of nodes (e.g. a list of arguments). + public void VisitAll(IReadOnlyList nodes) where T : Node + { + foreach (Node node in nodes) + { + Visit(node); + } + } + + /// Convenience/readability method for visiting a block of statements. + public void VisitBlock(IReadOnlyList statements) + { + VisitAll(statements); + } +} diff --git a/src/Starlark/Syntax/ReturnStatement.cs b/src/Starlark/Syntax/ReturnStatement.cs new file mode 100644 index 000000000..036ecbf8c --- /dev/null +++ b/src/Starlark/Syntax/ReturnStatement.cs @@ -0,0 +1,44 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// A syntax node for return statements. +public sealed class ReturnStatement : Statement +{ + private readonly int returnOffset; + private readonly Expression? result; + + internal ReturnStatement(FileLocations locs, int returnOffset, Expression? result) + : base(locs, StatementKind.RETURN) + { + this.returnOffset = returnOffset; + this.result = result; + } + + /// + /// Returns a new return statement that returns expr. It is provided only for use by the evaluator. + /// + internal static ReturnStatement Make(Expression expr) => + new(expr.Locs, expr.GetStartOffset(), expr); + + public Expression? GetResult() => result; + + public override int GetStartOffset() => returnOffset; + + public override int GetEndOffset() => + result != null ? result.GetEndOffset() : returnOffset + "return".Length; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/SyntaxUtils.cs b/src/Starlark/Syntax/SyntaxUtils.cs new file mode 100644 index 000000000..2baf77b89 --- /dev/null +++ b/src/Starlark/Syntax/SyntaxUtils.cs @@ -0,0 +1,67 @@ +// Copyright 2023 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// Utility methods for Starlark syntax. +public static class SyntaxUtils +{ + /// + /// Returns the effective bound for a positive-stride slice operation from a user-supplied integer. + /// + public static int ToSliceBound(int index, int length) + { + if (index < 0) + { + index += length; + } + + if (index < 0) + { + return 0; + } + else if (index > length) + { + return length; + } + else + { + return index; + } + } + + /// + /// Returns the effective bound for a negative-stride slice operation from a user-supplied integer. + /// + public static int ToReverseSliceBound(int index, int length) + { + if (index < 0) + { + index += length; + } + + if (index < -1) + { + return -1; + } + else if (index >= length) + { + return length - 1; + } + else + { + return index; + } + } +} diff --git a/src/Starlark/Syntax/TypeAliasStatement.cs b/src/Starlark/Syntax/TypeAliasStatement.cs new file mode 100644 index 000000000..b16bf6474 --- /dev/null +++ b/src/Starlark/Syntax/TypeAliasStatement.cs @@ -0,0 +1,69 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Text; + +namespace Starlark.Syntax; + +/// Represents a type alias statement in the Starlark AST. +public sealed class TypeAliasStatement : Statement +{ + private readonly int startOffset; + private readonly Identifier identifier; + private readonly ImmutableArray parameters; + private readonly Expression definition; + + internal TypeAliasStatement( + FileLocations locs, + int startOffset, + Identifier identifier, + ImmutableArray parameters, + Expression definition) + : base(locs, StatementKind.TYPE_ALIAS) + { + this.startOffset = startOffset; + this.identifier = identifier; + this.parameters = parameters; + this.definition = definition; + } + + public override string ToString() + { + var buf = new StringBuilder(); + buf.Append("type "); + buf.Append(identifier.GetName()); + if (!parameters.IsEmpty) + { + buf.Append('['); + ListExpression.AppendNodes(buf, parameters); + buf.Append(']'); + } + buf.Append(" = ...\n"); + return buf.ToString(); + } + + public Identifier GetIdentifier() => identifier; + + public IReadOnlyList GetParameters() => parameters; + + public Expression GetDefinition() => definition; + + /// Note that this is the start offset of the statement's type keyword. + public override int GetStartOffset() => startOffset; + + public override int GetEndOffset() => definition.GetEndOffset(); + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/TypeApplication.cs b/src/Starlark/Syntax/TypeApplication.cs new file mode 100644 index 000000000..6d0790c80 --- /dev/null +++ b/src/Starlark/Syntax/TypeApplication.cs @@ -0,0 +1,60 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Text; + +namespace Starlark.Syntax; + +/// Syntax node for a type application expression. +public sealed class TypeApplication : Expression +{ + private readonly Identifier constructor; + private readonly ImmutableArray arguments; + private readonly int rbracketOffset; + + internal TypeApplication( + FileLocations locs, + Identifier constructor, + ImmutableArray arguments, + int rbracketOffset) + : base(locs, ExpressionKind.TYPE_APPLICATION) + { + this.constructor = constructor ?? throw new ArgumentNullException(nameof(constructor)); + this.arguments = arguments; + this.rbracketOffset = rbracketOffset; + } + + /// Returns the type constructor. + public Identifier GetConstructor() => constructor; + + /// Returns the type arguments. + public IReadOnlyList GetArguments() => arguments; + + public override int GetStartOffset() => constructor.GetStartOffset(); + + public override int GetEndOffset() => rbracketOffset + 1; + + public override string ToString() + { + var buf = new StringBuilder(); + buf.Append(constructor); + buf.Append('['); + ListExpression.AppendNodes(buf, arguments); + buf.Append(']'); + return buf.ToString(); + } + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Starlark/Syntax/VarStatement.cs b/src/Starlark/Syntax/VarStatement.cs new file mode 100644 index 000000000..28385898a --- /dev/null +++ b/src/Starlark/Syntax/VarStatement.cs @@ -0,0 +1,55 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// Syntax node for a variable type annotation appearing as its own statement (foo : int), as +/// opposed to in an assignment statement where there's an initializer on the right-hand side. +/// +public sealed class VarStatement : Statement +{ + private readonly Identifier identifier; + + private readonly Expression type; + + private readonly DocComments? docComments; + + internal VarStatement( + FileLocations locs, + Identifier identifier, + Expression type, + DocComments? docComments) + : base(locs, StatementKind.VAR) + { + this.identifier = identifier; + this.type = type; + this.docComments = docComments; + } + + public override int GetStartOffset() => identifier.GetStartOffset(); + + public override int GetEndOffset() => type.GetEndOffset(); + + /// Returns the variable being declared and annotated. + public Identifier GetIdentifier() => identifier; + + /// Returns the type expression associated with the variable. + public Expression GetType() => type; + + /// Returns the Sphinx autodoc-style doc comments attached to this statement, if any. + public DocComments? GetDocComments() => docComments; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); +} From c22c5d3eb5e1c47f2e0bfd07928a4709fbce918a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 06:18:26 +0000 Subject: [PATCH 06/15] WIP: port Starlark eval runtime and value types to C# Adds Starlark.Eval value types (StarlarkInt/Float/List/Tuple/Dict/RangeList), Mutability, StarlarkSemantics, Printer, Module, StarlarkThread, EvalUtils, and Starlark helper extensions. Evaluator, reflection dispatch, string methods and sets deferred. Syntax package still in progress. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- src/Starlark/Eval/StarlarkHelpers.cs | 503 +++++++++++++++++++++++++ src/Starlark/Eval/StarlarkInt.cs | 7 +- src/Starlark/Spelling/SpellChecker.cs | 124 ++++++ src/Starlark/Syntax/Program.cs | 162 ++++++++ src/Starlark/Syntax/StarlarkFile.cs | 112 ++++++ src/Starlark/Syntax/StarlarkType.cs | 136 +++++++ src/Starlark/Syntax/TypeConstructor.cs | 83 ++++ src/Starlark/Syntax/TypeContext.cs | 40 ++ 8 files changed, 1163 insertions(+), 4 deletions(-) create mode 100644 src/Starlark/Eval/StarlarkHelpers.cs create mode 100644 src/Starlark/Spelling/SpellChecker.cs create mode 100644 src/Starlark/Syntax/Program.cs create mode 100644 src/Starlark/Syntax/StarlarkFile.cs create mode 100644 src/Starlark/Syntax/StarlarkType.cs create mode 100644 src/Starlark/Syntax/TypeConstructor.cs create mode 100644 src/Starlark/Syntax/TypeContext.cs diff --git a/src/Starlark/Eval/StarlarkHelpers.cs b/src/Starlark/Eval/StarlarkHelpers.cs new file mode 100644 index 000000000..5f86d520e --- /dev/null +++ b/src/Starlark/Eval/StarlarkHelpers.cs @@ -0,0 +1,503 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; +using System.Collections.Immutable; +using System.Numerics; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// Static entry points and helpers of the Starlark interpreter. Port of the value/runtime helpers of +/// net.starlark.java.eval.Starlark. (The core surface — None, Errorf — lives in +/// StarlarkCore.cs; this file extends the partial class.) +/// +/// Deferred vs. Java: the reflective builtin registry (UNIVERSE/MethodLibrary/CallUtils), the +/// call/fastcall machinery, static type accessors (getStarlarkType), and +/// doc-string helpers are not ported here. +/// +public static partial class Starlark +{ + /// + /// A sentinel value passed to optional parameters of StarlarkMethod-annotated methods to + /// indicate that no argument value was supplied. + /// + public static readonly object UNBOUND = new UnboundMarker(); + + /// A type representing no argument passed to StarlarkMethods. + public sealed class UnboundMarker : IStarlarkPrintableValue + { + internal UnboundMarker() { } + + public override string ToString() => ""; + + public bool IsImmutable() => true; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(""); + } + + /// + /// The universal bindings predeclared in every Starlark file. Currently only the literal + /// constants; builtin functions are added once the reflective registry is ported. + /// + public static readonly ImmutableDictionary UNIVERSE = + new Dictionary { ["False"] = false, ["True"] = true, ["None"] = None } + .ToImmutableDictionary(); + + /// Thrown when a value is not a legal Starlark value. + public sealed class InvalidStarlarkValueException : ArgumentException + { + public Type? InvalidClass { get; } + + internal InvalidStarlarkValueException(Type? invalidClass) + : base("invalid Starlark value: " + (invalidClass == null ? "null" : invalidClass.Name)) + { + InvalidClass = invalidClass; + } + } + + /// Reports whether the argument is a legal Starlark value. + public static bool Valid(object? x) => x is string || x is bool || x is IStarlarkValue; + + /// Returns x if it is a valid Starlark value, else throws. + public static T CheckValid(T x) + { + if (!Valid(x)) + { + throw new InvalidStarlarkValueException(x?.GetType()); + } + return x; + } + + /// Reports whether x is null or Starlark None. + public static bool IsNullOrNone(object? x) => x == null || ReferenceEquals(x, None); + + /// Reports whether a Starlark value is assumed to be deeply immutable. + public static bool IsImmutable(object? x) + { + if (x is string || x is bool) + { + return true; + } + if (x is IStarlarkValue v) + { + return v.IsImmutable(); + } + throw new InvalidStarlarkValueException(x?.GetType()); + } + + /// Returns normally if the Starlark value is hashable and thus suitable as a dict key. + public static void CheckHashable(object? x) + { + if (x is string) + { + // Strings are the most common dict keys. + } + else if (x is IStarlarkPrintableValue pv) + { + pv.CheckHashable(); + } + else if (x is IStarlarkValue v) + { + if (!v.IsImmutable()) + { + throw Errorf("unhashable type: '{0}'", Type(x)); + } + } + else + { + CheckValid(x); + } + } + + /// Converts a Java/.NET value to a Starlark one, if not already valid. + public static object FromJava(object? x, Mutability? mutability) + { + switch (x) + { + case null: + return None; + case string: + case bool: + case IStarlarkValue: + return x; + case int i: + return StarlarkInt.Of(i); + case long l: + return StarlarkInt.Of(l); + case BigInteger b: + return StarlarkInt.Of(b); + case double d: + return StarlarkFloat.Of(d); + case IEnumerable> map: + return Dict.CopyOf(mutability, map); + case IEnumerable list: + return StarlarkList.CopyOf(mutability, list); + } + throw new InvalidStarlarkValueException(x.GetType()); + } + + /// Returns the truth value of a valid Starlark value. + public static bool Truth(object? x) + { + switch (x) + { + case bool b: + return b; + case IStarlarkValue v: + return v.Truth(); + case string s: + return s.Length != 0; + default: + throw new InvalidStarlarkValueException(x?.GetType()); + } + } + + /// Checks whether the Freezable value is mutable; throws if not. + public static void CheckMutable(IFreezable x) + { + if (x.Mutability.IsFrozen) + { + throw Errorf("trying to mutate a frozen {0} value", Type(x)); + } + if (x.UpdateIteratorCount(0)) + { + throw Errorf( + "{0} value is temporarily immutable due to active for-loop iteration", Type(x)); + } + } + + /// Returns an iterable view of x if it is an iterable Starlark value; throws otherwise. + public static IEnumerable ToIterable(object? x) + { + if (x is IStarlarkIterable it) + { + return it; + } + if (x is IEnumerable en && x is IStarlarkValue) + { + return en.Cast(); + } + throw Errorf("type '{0}' is not iterable", Type(x)); + } + + /// Returns a new array containing the elements of a Starlark iterable value. + public static object?[] ToArray(object? x) + { + switch (x) + { + case ISequence seq: + return seq.ToArray(); + case Dict dict: + return dict.Keys.ToArray(); + default: + return ToIterable(x).ToArray(); + } + } + + /// Returns the length of a value as if by len(x), or -1 if it has no length. + public static int Len(object? x) + { + switch (x) + { + case string s: + return s.Length; + case ISequence seq: + return seq.Count; + case Dict dict: + return dict.Count; + case IStarlarkIterable it: + return it.Count(); + default: + CheckValid(x); + return -1; + } + } + + /// Returns the name of the type of a value as if by type(x). + public static string Type(object? x) => ClassType(x?.GetType()); + + /// Returns the name of the type of instances of a class. + public static string ClassType(System.Type? c) + { + if (c == null) + { + return "NoneType"; + } + if (c == typeof(string)) + { + return "string"; + } + if (typeof(StarlarkInt).IsAssignableFrom(c)) + { + return "int"; + } + if (c == typeof(bool)) + { + return "bool"; + } + if (c == typeof(StarlarkFloat)) + { + return "float"; + } + if (typeof(StarlarkList).IsAssignableFrom(c)) + { + return "list"; + } + if (typeof(Tuple).IsAssignableFrom(c)) + { + return "tuple"; + } + if (c == typeof(Dict)) + { + return "dict"; + } + if (c == typeof(NoneType)) + { + return "NoneType"; + } + if (c == typeof(RangeList)) + { + return "range"; + } + if (c == typeof(UnboundMarker)) + { + return "unbound"; + } + if (c == typeof(int) || c == typeof(long) || c == typeof(BigInteger)) + { + return "int"; + } + if (c == typeof(double)) + { + return "float"; + } + + // Honor a [StarlarkBuiltin] annotation if present. + var attr = (StarlarkBuiltinAttribute?)Attribute.GetCustomAttribute(c, typeof(StarlarkBuiltinAttribute)); + if (attr != null) + { + return attr.Name; + } + + return c.Name; + } + + /// The strict weak ordering of Starlark values. Throws on failure. + public static int CompareUnchecked(object? x, object? y) + { + if (x == null || y == null) + { + throw new InvalidCastException( + string.Format("unsupported comparison: {0} <=> {1}", Type(x), Type(y))); + } + if (SameType(x, y)) + { + switch (x) + { + case string sx: + return string.CompareOrdinal(sx, (string)y); + case bool bx: + return bx.CompareTo((bool)y); + case StarlarkInt ix: + return StarlarkInt.Compare(ix, (StarlarkInt)y); + case StarlarkFloat fx: + return fx.CompareTo((StarlarkFloat)y); + case IComparable cx: + return cx.CompareTo(y); + } + } + else + { + if (x is StarlarkFloat xf && y is StarlarkInt yi) + { + double xd = xf.ToDouble(); + return double.IsNaN(xd) ? +1 : -StarlarkInt.CompareIntAndDouble(yi, xd); + } + if (x is StarlarkInt xi && y is StarlarkFloat yf) + { + double yd = yf.ToDouble(); + return double.IsNaN(yd) ? -1 : StarlarkInt.CompareIntAndDouble(xi, yd); + } + } + throw new InvalidCastException( + string.Format("unsupported comparison: {0} <=> {1}", Type(x), Type(y))); + } + + private static bool SameType(object x, object y) => + x.GetType() == y.GetType() || Type(x) == Type(y); + + /// Returns the string form of a value as if by str(x). + public static string Str(object? x, StarlarkSemantics semantics) => + new Printer().Str(x, semantics).ToString(); + + /// Returns the string form of a value as if by repr(x). + public static string Repr(object? x, StarlarkSemantics semantics) => + new Printer().Repr(x, semantics).ToString(); + + /// Returns a string formatted as if by pattern % arguments. + public static string Format(StarlarkSemantics semantics, string pattern, params object?[] arguments) + { + var pr = new Printer(); + Printer.Format(pr, semantics, pattern, arguments); + return pr.ToString(); + } + + /// Returns a string formatted as if by pattern % arguments. + public static string FormatWithList( + StarlarkSemantics semantics, string pattern, IReadOnlyList arguments) + { + var pr = new Printer(); + Printer.FormatWithList(pr, semantics, pattern, arguments); + return pr.ToString(); + } + + /// Returns the signed 32-bit value of a Starlark int, or throws with . + public static int ToInt(object? x, string what) + { + if (x is StarlarkInt si) + { + return si.ToInt(what); + } + if (x is int) + { + throw new ArgumentException("Integer is not a legal Starlark value"); + } + throw Errorf("got {0} for {1}, want int", Type(x), what); + } + + /// Returns a slice of a sequence as if by x[start:stop:step]. + public static object Slice( + Mutability? mu, object x, object startObj, object stopObj, object stepObj) + { + int n; + if (x is string s) + { + n = s.Length; + } + else if (x is ISequence seq) + { + n = seq.Count; + } + else + { + throw Errorf("invalid slice operand: {0}", Type(x)); + } + + int step; + if (ReferenceEquals(stepObj, None)) + { + step = 1; + } + else + { + step = ToInt(stepObj, "slice step"); + if (step == 0) + { + throw Errorf("slice step cannot be zero"); + } + } + + int start; + int stop; + if (step > 0) + { + start = ReferenceEquals(startObj, None) ? 0 : ToSliceBound(ToInt(startObj, "start index"), n); + stop = ReferenceEquals(stopObj, None) ? n : ToSliceBound(ToInt(stopObj, "stop index"), n); + if (stop < start) + { + stop = start; + } + } + else + { + start = ReferenceEquals(startObj, None) + ? n - 1 + : ToReverseSliceBound(ToInt(startObj, "start index"), n); + stop = ReferenceEquals(stopObj, None) + ? -1 + : ToReverseSliceBound(ToInt(stopObj, "stop index"), n); + if (start < stop) + { + start = stop; + } + } + + if (x is string str) + { + return SliceString(str, start, stop, step); + } + return ((ISequence)x).GetSlice(mu, start, stop, step); + } + + private static string SliceString(string s, int start, int stop, int step) + { + if (step == 1) + { + return start <= stop ? s.Substring(start, stop - start) : ""; + } + var sb = new System.Text.StringBuilder(); + if (step > 0) + { + for (int i = start; i < stop; i += step) + { + sb.Append(s[i]); + } + } + else + { + for (int i = start; i > stop; i += step) + { + sb.Append(s[i]); + } + } + return sb.ToString(); + } + + // Ports of SyntaxUtils.toSliceBound / toReverseSliceBound (clamping helpers). + internal static int ToSliceBound(int index, int length) + { + if (index < 0) + { + index += length; + if (index < 0) + { + index = 0; + } + } + else if (index > length) + { + index = length; + } + return index; + } + + internal static int ToReverseSliceBound(int index, int length) + { + if (index < 0) + { + index += length; + if (index < 0) + { + index = -1; + } + } + else if (index >= length) + { + index = length - 1; + } + return index; + } +} diff --git a/src/Starlark/Eval/StarlarkInt.cs b/src/Starlark/Eval/StarlarkInt.cs index 2a40be18f..860619c04 100644 --- a/src/Starlark/Eval/StarlarkInt.cs +++ b/src/Starlark/Eval/StarlarkInt.cs @@ -415,12 +415,11 @@ public static StarlarkInt Multiply(StarlarkInt x, StarlarkInt y) { if (x.TryToLong(out long xl) && y.TryToLong(out long yl)) { - long hi = Math.BigMul(xl, yl, out ulong lo); - long lows = unchecked((long)lo); + long hi = Math.BigMul(xl, yl, out long lo); // Check int128 result is within int64 range. - if (hi == (lows >> 63)) + if (hi == (lo >> 63)) { - return Of(lows); + return Of(lo); } } return Of(x.ToBigInteger() * y.ToBigInteger()); diff --git a/src/Starlark/Spelling/SpellChecker.cs b/src/Starlark/Spelling/SpellChecker.cs new file mode 100644 index 000000000..ea0ebec7e --- /dev/null +++ b/src/Starlark/Spelling/SpellChecker.cs @@ -0,0 +1,124 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Spelling; + +/// +/// Class that provides functions to do spell checking, i.e. detect typos and make suggestions. +/// +public static class SpellChecker +{ + /// + /// Computes the edit distance between two strings. The edit distance is the minimum number of + /// insertions, deletions and replacements to transform a string into the other string. + /// + /// is the maximum distance the function can return. If + /// it would be greater, the function returns -1. It is useful for speeding up the + /// computations. + /// + public static int EditDistance(string s1, string s2, int maxEditDistance) + { + // This is the Levenshtein distance, as described here: + // http://en.wikipedia.org/wiki/Levenshtein_distance + // + // We don't need to keep the full matrix. To update a cell, we only + // need top-left, top, and left values. Using a single array is + // sufficient. Top value is still in row[j] from the last iteration. + // Top-left value is stored in 'previous'. Left value is row[j - 1]. + + if (s1.Equals(s2, StringComparison.Ordinal)) + { + return 0; + } + + // Short-circuit based on string length. + if (Math.Abs(s1.Length - s2.Length) > maxEditDistance) + { + return -1; + } + + int[] row = new int[s2.Length + 1]; + for (int i = 0; i <= s2.Length; i++) + { + row[i] = i; + } + + for (int i = 1; i <= s1.Length; i++) + { + row[0] = i; + int bestInTheRow = row[0]; + int previous = i - 1; + + for (int j = 1; j <= s2.Length; j++) + { + int old = row[j]; + + row[j] = Math.Min( + previous + (s1[i - 1] == s2[j - 1] ? 0 : 1), + 1 + Math.Min(row[j - 1], row[j])); + previous = old; + bestInTheRow = Math.Min(bestInTheRow, row[j]); + } + + if (bestInTheRow > maxEditDistance) + { + return -1; + } + } + + int result = row[s2.Length]; + return result <= maxEditDistance ? result : -1; + } + + /// + /// Find in words which string is the most similar to input (according to the edit distance, + /// ignoring case) - or null if no string is similar enough. In case of equality, the first one + /// in words wins. + /// + public static string? Suggest(string input, IEnumerable words) + { + string? best = null; + // Heuristic: the expected number of typos depends on the length of the word. + int bestDistance = Math.Min(5, (input.Length + 1) / 2); + input = input.ToLowerInvariant(); + foreach (string candidate in words) + { + int d = EditDistance(input, candidate.ToLowerInvariant(), bestDistance); + if (d >= 0 && d < bestDistance) + { + bestDistance = d; + best = candidate; + } + } + + return best; + } + + /// + /// Return a string to be used at the end of an error message. It is either an empty string, or a + /// spelling suggestion, e.g. " (did you mean 'x'?)". + /// + public static string DidYouMean(string input, IEnumerable words) + { + string? suggestion = Suggest(input, words); + if (suggestion == null) + { + return ""; + } + else + { + return " (did you mean '" + suggestion + "'?)"; + } + } +} diff --git a/src/Starlark/Syntax/Program.cs b/src/Starlark/Syntax/Program.cs new file mode 100644 index 000000000..c266d7ec9 --- /dev/null +++ b/src/Starlark/Syntax/Program.cs @@ -0,0 +1,162 @@ +// Copyright 2020 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Linq; + +namespace Starlark.Syntax; + +/// +/// An opaque, executable representation of a valid Starlark program. +/// +public sealed class Program +{ + private readonly Resolver.Function body; + private readonly ImmutableArray loads; + private readonly ImmutableArray loadLocations; + private readonly ImmutableDictionary docCommentsMap; + private readonly ImmutableArray unusedDocCommentLines; + + private Program( + Resolver.Function body, + ImmutableArray loads, + ImmutableArray loadLocations, + ImmutableDictionary docCommentsMap, + ImmutableArray unusedDocCommentLines) + { + if (loads.Length != loadLocations.Length) + { + throw new ArgumentException("each load must have a corresponding location"); + } + this.body = body; + this.loads = loads; + this.loadLocations = loadLocations; + this.docCommentsMap = docCommentsMap; + this.unusedDocCommentLines = unusedDocCommentLines; + } + + public Resolver.Function GetResolvedFunction() => body; + + /// Returns the file name of this compiled program. + public string GetFilename() => body.GetLocation().File; + + /// Returns the list of load strings of this compiled program, in source order. + public IReadOnlyList GetLoads() => loads; + + /// Returns the location of the ith load (see ). + public Location GetLoadLocation(int i) => loadLocations[i]; + + /// + /// Returns a map from global variable names to Sphinx autodoc-style doc comments associated with + /// the variable's declarations. + /// + public IReadOnlyDictionary GetDocCommentsMap() => docCommentsMap; + + /// Returns the list of doc comments not associated with any global variable. + public IReadOnlyList GetUnusedDocCommentLines() => unusedDocCommentLines; + + /// + /// Resolves a file syntax tree in the specified environment and compiles it to a Program. This + /// operation mutates the syntax tree. + /// + /// in case of resolution error. + public static Program CompileFile(StarlarkFile file, Resolver.IModule env) + { + Resolver.ResolveFile(file, env); + if (!file.Ok()) + { + throw new SyntaxError.Exception(file.Errors()); + } + + if (file.GetOptions().ResolveTypeSyntax) + { + TypeTagger.TagFile(file, env); + if (!file.Ok()) + { + throw new SyntaxError.Exception(file.Errors()); + } + } + + if (file.GetOptions().StaticTypeChecking) + { + TypeChecker.CheckFile(file, env); + if (!file.Ok()) + { + throw new SyntaxError.Exception(file.Errors()); + } + } + + // Extract load statements. + var loads = ImmutableArray.CreateBuilder(); + var loadLocations = ImmutableArray.CreateBuilder(); + foreach (Statement stmt in file.GetStatements()) + { + if (stmt is LoadStatement load) + { + string module = load.GetImport().GetValue(); + loads.Add(module); + loadLocations.Add(load.GetImport().GetLocation()); + } + } + + // Find unused doc comments. + ImmutableDictionary docCommentsMap = file.docCommentsMap.ToImmutableDictionary(); + var usedDocCommentLines = new HashSet(); + foreach (DocComments docComments in docCommentsMap.Values) + { + foreach (Comment c in docComments.GetLines()) + { + usedDocCommentLines.Add(c); + } + } + ImmutableArray unusedDocCommentLines = + file.GetComments() + .Where(c => c.HasDocCommentPrefix() && !usedDocCommentLines.Contains(c)) + .ToImmutableArray(); + + return new Program( + file.GetResolvedFunction()!, + loads.ToImmutable(), + loadLocations.ToImmutable(), + docCommentsMap, + unusedDocCommentLines); + } + + /// + /// Resolves an expression syntax tree in the specified environment and compiles it to a Program. + /// + /// in case of resolution error. + public static Program CompileExpr(Expression expr, Resolver.IModule module, FileOptions options) + { + Resolver.Function body = Resolver.ResolveExpr(expr, module, options); + + if (options.ResolveTypeSyntax) + { + TypeTagger.TagExpr(expr, body, module); + } + + if (options.StaticTypeChecking) + { + StarlarkType exprType = TypeChecker.InferTypeOf(expr, module); + TypeTagger.TagExprFunction(body, exprType); + } + + return new Program( + body, + ImmutableArray.Empty, + ImmutableArray.Empty, + ImmutableDictionary.Empty, + ImmutableArray.Empty); + } +} diff --git a/src/Starlark/Syntax/StarlarkFile.cs b/src/Starlark/Syntax/StarlarkFile.cs new file mode 100644 index 000000000..9bc30e43b --- /dev/null +++ b/src/Starlark/Syntax/StarlarkFile.cs @@ -0,0 +1,112 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// +/// Syntax tree for a Starlark file, such as a Bazel BUILD or .bzl file. +/// +/// Call Parse to parse a file. Parser errors are recorded in the syntax tree (see +/// Errors), which may be incomplete. +/// +public sealed class StarlarkFile : Node +{ + private readonly ImmutableArray statements; + private readonly FileOptions options; + private readonly ImmutableArray comments; + internal readonly List errors; // appended to by Resolver + // Map from global variable name to doc comments. Added to by Resolver. + internal readonly Dictionary docCommentsMap = new(); + + // set by resolver + private Resolver.Function? resolved; + + public override int GetStartOffset() => 0; + + public override int GetEndOffset() => Locs.Size; + + private StarlarkFile( + FileLocations locs, + ImmutableArray statements, + FileOptions options, + ImmutableArray comments, + List errors) + : base(locs) + { + this.statements = statements; + this.options = options; + this.comments = comments; + this.errors = errors; + } + + /// + /// Returns an unmodifiable view of the list of scanner, parser, and (perhaps) resolver errors + /// accumulated in this Starlark file. + /// + public IReadOnlyList Errors() => errors.AsReadOnly(); + + /// Returns Errors().Count == 0. + public bool Ok() => errors.Count == 0; + + /// Returns an (immutable, ordered) list of statements in this BUILD file. + public IReadOnlyList GetStatements() => statements; + + /// Returns an (immutable, ordered) list of comments in this BUILD file. + public IReadOnlyList GetComments() => comments; + + public override string ToString() => ""; + + public override void Accept(NodeVisitor visitor) => visitor.Visit(this); + + internal void SetResolvedFunction(Resolver.Function resolved) => this.resolved = resolved; + + /// + /// Returns information about the implicit function containing the top-level statements of the + /// file. Set by the resolver. + /// + public Resolver.Function? GetResolvedFunction() => resolved; + + /// + /// Parse a Starlark file. A syntax tree is always returned, even in case of error. Errors are + /// recorded in the tree. + /// + public static StarlarkFile Parse(ParserInput input, FileOptions options) + { + Parser.ParseResult result = Parser.ParseFile(input, options); + return new StarlarkFile( + result.Locs, result.Statements, options, result.Comments, result.Errors); + } + + /// Parse a Starlark file with default options. + public static StarlarkFile Parse(ParserInput input) => Parse(input, FileOptions.DEFAULT); + + /// Returns the options specified when parsing this file. + public FileOptions GetOptions() => options; + + /// Returns the name of this file, as specified to the parser. + public string GetName() => Locs.File; + + /// A ParseProfiler records the start and end times of parse operations. + public interface IParseProfiler + { + long Start(); + + void End(long profileStartNanos, string filename); + } + + /// Installs a global hook that will be notified of parse operations. + public static void SetParseProfiler(IParseProfiler? p) => Parser.Profiler = p; +} diff --git a/src/Starlark/Syntax/StarlarkType.cs b/src/Starlark/Syntax/StarlarkType.cs new file mode 100644 index 000000000..230147821 --- /dev/null +++ b/src/Starlark/Syntax/StarlarkType.cs @@ -0,0 +1,136 @@ +// Copyright 2025 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Linq; + +namespace Starlark.Syntax; + +/// +/// Base class for all Starlark types. +/// +/// Starlark typing is an experimental feature under development. See the tracking issue: +/// https://github.com/bazelbuild/bazel/issues/27370 +/// +public abstract class StarlarkType : TypeConstructor.Arg +{ + /// + /// Returns the list of supertypes of this type. + /// + /// Preferred order is from the most specific to the least specific supertype. But if that + /// is not possible, the order can be arbitrary. + /// + public virtual IReadOnlyList GetSupertypes() + { + return System.Array.Empty(); + } + + /// + /// If this type has a field by the given name, returns the type of that field, or null otherwise. + /// + public virtual StarlarkType? GetField(string name, TypeContext context) + { + return null; + } + + /// + /// Returns whether a value of type can be assigned to a value of type + /// . + /// + /// In gradual typing terms, must be a "consistent subtype of" + /// . This means that there is a way to substitute zero or more occurrences + /// of Any in both terms, such that becomes a subtype of + /// in the ordinary sense. + /// + public static bool AssignableFrom(StarlarkType t1, StarlarkType t2) + { + if (t1.Equals(Types.ANY) || t2.Equals(Types.ANY)) + { + return true; + } + if (t1.Equals(Types.OBJECT)) + { + return true; + } + if (t1.Equals(t2)) + { + return true; + } + if (t2 is Types.UnionType union2) + { + return union2.GetTypes().All(sub2 => AssignableFrom(t1, sub2)); + } + if (t1 is Types.UnionType union1) + { + return union1.GetTypes().Any(sub1 => AssignableFrom(sub1, t2)); + } + if (t2.GetSupertypes().Any(super2 => AssignableFrom(t1, super2))) + { + return true; + } + return false; + } + + /// + /// Infers the return type of a binary operation having an operand of this type. Intended for use + /// by . + /// + /// Returns the inferred type of the operation, or null to indicate that we could not + /// infer a return type, in which case the caller would fall back to calling + /// InferBinaryOperator on the other operand's type, or to special-case handling for + /// certain operators on certain built-in types (e.g. tuple multiplication). + /// + internal virtual StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType that, bool thisLeft) + { + return null; + } + + /// + /// Returns true iff the values of the two arbitrary (possibly union) types can be ordering + /// compared. + /// + public static bool Comparable(StarlarkType x, StarlarkType y) + { + return x.IsComparable(y) || y.IsComparable(x); + } + + /// + /// Returns true if this type's values can be ordering compared with values of another type. A + /// return value of false is ambiguous on its own; two types are considered incomparable iff both + /// x.IsComparable(y) and y.IsComparable(x) are false. + /// + /// Do not call this method directly; instead, use . + /// + protected internal virtual bool IsComparable(StarlarkType that) + { + return false; + } + + /// + /// Returns true if an index expression on a value of this type can be used as the LHS of an + /// assignment. + /// + public virtual bool HasSetIndex() + { + return false; + } + + /// + /// Returns true if a dot expression on a value of this type can be used as the LHS of an + /// assignment. + /// + public virtual bool HasSetField() + { + return false; + } +} diff --git a/src/Starlark/Syntax/TypeConstructor.cs b/src/Starlark/Syntax/TypeConstructor.cs new file mode 100644 index 000000000..cfc255d61 --- /dev/null +++ b/src/Starlark/Syntax/TypeConstructor.cs @@ -0,0 +1,83 @@ +// Copyright 2026 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// +/// A factory for creating s, parameterized by zero or more type arguments. +/// +/// Conceptually, a type constructor corresponds to what the user informally thinks of as "a +/// type": a program symbol, like list, that can appear within a type expression. The usage of +/// a constructor in a type expression yields an actual type, like list[int]. In the case of +/// basic types like None that are not parameterized, there is both a trivial nullary type +/// constructor and an underlying singleton type, where the constructor just wraps the underlying +/// type. +/// +public interface TypeConstructor +{ + /// + /// Returns the result of applying this constructor to the given type arguments. + /// + /// + /// if the usage of this constructor is invalid (typically due to a mismatch in the number of + /// arguments). + /// + StarlarkType CreateStarlarkType(IReadOnlyList argsTuple); + + /// Exception thrown when a is called with invalid arguments. + public sealed class Failure : Exception + { + public Failure(string message) + : base(message) + { + } + } + + /// + /// An argument to a type constructor's method. + /// + /// Conceptually, a type argument is the result of evaluating a subexpression of a type + /// expression. Whereas the overall type expression must yield a , a + /// subexpression can also yield other objects such as an ellipsis or a list of other arguments. + /// These are needed for type expressions like tuple[Any, ...] and Callable[[int], + /// bool]. + /// + public interface Arg + { + public static readonly EllipsisArg ELLIPSIS = new EllipsisArg(); + public static readonly EmptyTupleArg EMPTY_TUPLE = new EmptyTupleArg(); + + /// An ellipsis type argument, .... + public sealed class EllipsisArg : Arg + { + internal EllipsisArg() + { + } + + public override string ToString() => "..."; + } + + /// An empty tuple type argument, (). + public sealed class EmptyTupleArg : Arg + { + internal EmptyTupleArg() + { + } + + public override string ToString() => "()"; + } + } +} diff --git a/src/Starlark/Syntax/TypeContext.cs b/src/Starlark/Syntax/TypeContext.cs new file mode 100644 index 000000000..9175d9299 --- /dev/null +++ b/src/Starlark/Syntax/TypeContext.cs @@ -0,0 +1,40 @@ +// Copyright 2026 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Starlark.Syntax; + +/// +/// A context for obtaining more detailed information about Starlark types. +/// +/// This is used to inject type information from the eval/ package into the syntax/ package, +/// e.g. the method APIs of StarlarkList. +/// +public interface TypeContext +{ + /// + /// Returns the type of the given field of a list[T] type, or null if no such field exists. + /// + StarlarkType? GetListFieldType(string name); + + /// + /// Returns the type of the given field of a dict[K, V] type, or null if no such field + /// exists. + /// + StarlarkType? GetDictFieldType(string name); + + /// + /// Returns the type of the given field of a set[T] type, or null if no such field exists. + /// + StarlarkType? GetSetFieldType(string name); +} From 313b1ff6792d5484eda63753efad2e3d17aae62e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 06:30:18 +0000 Subject: [PATCH 07/15] Port Starlark interpreter + foundation; solution builds green with tests Consolidates the parallel port work into a compiling, tested state: - Starlark project (annot + syntax + eval + resolver/type-system + spelling) builds clean. Value types, lexer/parser/AST, resolver and type checker ported. Tree-walking evaluator and reflective [StarlarkMethod] dispatch are deferred (next milestone, needed for config loading). - Copybara.Core foundation: exceptions, util (Glob/FileUtil/CommandRunner), console, revision, authoring, templatetoken. - Fix CommandRunner to qualify System.Console (namespace collision with Copybara.Util.Console). - Add xUnit smoke tests (glob matching, author parse/equality, printf-style CheckCondition, multimap ordering); all 8 pass. - Update TODO.md to reflect landed vs deferred work. Whole solution: dotnet build = 0 warnings/0 errors; dotnet test = 8 passed; dotnet pack produces the 'copybara' .NET tool package. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- TODO.md | 89 +- src/Copybara.Core/Util/CommandRunner.cs | 6 +- src/Starlark/Syntax/AssignmentStatement.cs | 4 +- src/Starlark/Syntax/CastExpression.cs | 2 +- src/Starlark/Syntax/IsInstanceExpression.cs | 2 +- src/Starlark/Syntax/Lexer.cs | 1092 +++++++++++++ src/Starlark/Syntax/Parameter.cs | 2 +- src/Starlark/Syntax/Parser.cs | 1626 +++++++++++++++++++ src/Starlark/Syntax/Resolver.cs | 1405 ++++++++++++++++ src/Starlark/Syntax/TypeChecker.cs | 1622 ++++++++++++++++++ src/Starlark/Syntax/TypeTagger.cs | 524 ++++++ src/Starlark/Syntax/Types.cs | 1289 +++++++++++++++ src/Starlark/Syntax/VarStatement.cs | 2 +- tests/Copybara.Tests/SmokeTests.cs | 82 + 14 files changed, 7693 insertions(+), 54 deletions(-) create mode 100644 src/Starlark/Syntax/Lexer.cs create mode 100644 src/Starlark/Syntax/Parser.cs create mode 100644 src/Starlark/Syntax/Resolver.cs create mode 100644 src/Starlark/Syntax/TypeChecker.cs create mode 100644 src/Starlark/Syntax/TypeTagger.cs create mode 100644 src/Starlark/Syntax/Types.cs create mode 100644 tests/Copybara.Tests/SmokeTests.cs diff --git a/TODO.md b/TODO.md index f32a20db2..121c20f2e 100644 --- a/TODO.md +++ b/TODO.md @@ -13,44 +13,39 @@ then the domain modules that depend on both. - ✅ Move original Java/Bazel tree into `java/` (reference only). - ✅ Author `CLAUDE.md` and `TODO.md`. -- 🚧 Create solution + project scaffold: - - `src/Copybara.Common` — helpers (Preconditions, ImmutableListMultimap, Glob-support types). - - `src/Starlark` — Starlark interpreter port. - - `src/Copybara.Core` — engine. - - `src/Copybara.Cli` — the `copybara` .NET tool (`PackAsTool=true`). - - `tests/Copybara.Tests` — xUnit. -- 🚧 Wire NuGet deps: `LibGit2Sharp`, `Microsoft.Extensions.Logging`, - `System.Text.Json`, `System.Collections.Immutable`, xUnit, FluentAssertions. -- ⬜ `.gitignore` for .NET (`bin/`, `obj/`, `*.user`). +- ✅ Create solution + project scaffold (`Copybara.slnx`): + - `src/Copybara.Common`, `src/Starlark`, `src/Copybara.Core`, + `src/Copybara.Cli` (`PackAsTool=true`, verified `dotnet pack` produces a tool + package), `tests/Copybara.Tests`. +- ✅ Wire NuGet deps: `LibGit2Sharp`, `Microsoft.Extensions.Logging`, xUnit, + FluentAssertions (`System.Collections.Immutable`/`System.Text.Json` are in-box). +- ✅ `.gitignore` for .NET (`bin/`, `obj/`, `*.user`, `nupkg/`). +- ✅ Whole solution builds clean (0 warnings/0 errors); 8 smoke tests pass. - ⬜ CI: `dotnet build` + `dotnet test` GitHub Action. ## Phase 1 — Foundation (`Copybara.Common` + `Copybara.Core` primitives) No Starlark dependency. Port these first; they unblock everything else. -- 🚧 `Copybara.Common` - - `Preconditions` (CheckNotNull, CheckArgument, CheckState). - - `ImmutableListMultimap` + builder. - - String helpers (Splitter/Joiner/CharMatcher equivalents as needed). -- 🚧 `exception/` → `Copybara.Exceptions` - - `ValidationException` (+ `CheckCondition`), `RepoException`, - `CannotResolveRevisionException`, `EmptyChangeException`, - `ChangeRejectedException`, `NonReversibleValidationException`, - `CommandLineException`, `RedundantChangeException`, `VoidOperationException`, - `NotADestinationFileException`, `AccessValidationException`, - `CannotResolveLabel`. -- ⬜ `util/` core (subset, no git): - - `ExitCode`, `Glob` + `GlobAtom` + `SequenceGlob` + `ReadablePathMatcher`, - `FileUtil`, `DirFactory`, `CommandOutput(WithStatus)`, `CommandRunner` - (process runner), `Identity`, `TablePrinter`, `RenameDetector`, `DiffUtil` - (backed by `git diff`/LibGit2Sharp), `OriginUtil`. -- ⬜ `util/console/` → `Copybara.Util.Console` - - `Console` interface, `AnsiConsole`, `LogConsole`, `FileConsole`, - `NoPromptConsole`, `CapturingConsole`, `Consoles`, `ProgressPrefixConsole`. -- ⬜ `revision/` → `Copybara.Revision`: `Revision`, `Change`, `Changes`, `OriginRef`. -- ⬜ `authoring/` → `Copybara.Authoring`: `Author`, `AuthorParser`, `Authoring`, - `InvalidAuthorException` (note: `Author`/`Authoring` are Starlark values — see Phase 3). -- ⬜ `templatetoken/` → `Copybara.TemplateToken`. +- ✅ `Copybara.Common`: `Preconditions`, `ImmutableListMultimap` + builder. + (String helpers added ad hoc where needed.) +- ✅ `exception/` → `Copybara.Exceptions` — all 13 classes ported with correct + hierarchy; `ValidationException.CheckCondition` handles printf-style `%s` + format strings via an internal `%`→`{}` translator. +- ✅ `util/` core (subset) → `Copybara.Util`: `ExitCode`, full `Glob` subsystem + (`Glob`/`GlobAtom`/`SequenceGlob`/`ReadablePathMatcher`/`GlobPathMatcher`/ + `IPathMatcher`), `FileUtil`, `DirFactory`, `Identity`, `TablePrinter`, + `CommandOutput(WithStatus)`, `CommandRunner` + shell stack. + - ⬜ Still TODO in util: `DiffUtil`, `CommandLineDiffUtil`, `MergeImportTool`, + `AutoPatchUtil`, `ConsistencyFile`, `ApplyDestinationPatch`, `RenameDetector`, + `ScpUtil`, `OriginUtil`, `RepositoryUtil`, `EnumMapConverter`. +- ✅ `util/console/` → `Copybara.Util.Console` — all 17 files (`Console`, + `AnsiConsole`, `LogConsole`, `FileConsole`, `NoPromptConsole`, + `CapturingConsole`, `MultiplexingConsole`, `PrefixConsole`, `Consoles`, …). +- ✅ `revision/` → `Copybara.Revision`: `IRevision`, `Change`, `Changes`, `OriginRef`. +- ✅ `authoring/` → `Copybara.Authoring`: `Author`, `AuthorParser`, `Authoring`, + `InvalidAuthorException` (as Starlark values). +- ✅ `templatetoken/` → `Copybara.TemplateToken`. - ⬜ `profiler/` → `Copybara.Profiler`. ## Phase 2 — Starlark interpreter (`src/Starlark`) @@ -60,20 +55,24 @@ self-contained interpreter and the critical dependency for config loading. Sub-packages: `annot`, `syntax` (lexer/parser/AST), `eval` (values, evaluator, builtins), `lib` (json, proto, etc.), `spelling`. -- ⬜ `annot/` — attributes: `[StarlarkBuiltin]`, `[StarlarkMethod]`, - `[Param]`, `[ParamType]`, `StarlarkDocumentationCategory`, etc. -- ⬜ `syntax/` — `Lexer`, `Parser`, AST node types, `Location`, `FileOptions`, - `SyntaxError`. -- ⬜ `eval/` — `StarlarkValue`, `Starlark` (entry/helpers), `StarlarkThread`, - `Module`, `Mutability`, `StarlarkInt/Float/List/Dict/Tuple/Function`, - `EvalException`, `MethodLibrary`, `StarlarkCallable`, `Structure`, - `CallUtils`/method-descriptor reflection (map `@StarlarkMethod` → dispatch). +- ✅ `annot/` — `[StarlarkBuiltin]`, `[StarlarkMethod]`, `[Param]`, `[ParamType]`. +- ✅ `syntax/` — `Lexer`, `Parser`, all AST node types, `Location`, `TokenKind`, + `FileOptions`, `SyntaxError`, `NodeVisitor`, `StarlarkFile`/`Program`, plus the + resolver/type-system subset (`Resolver`, `Types`, `StarlarkType`, `TypeChecker`, + `TypeTagger`, `TypeConstructor`, `TypeContext`). +- 🚧 `eval/` — DONE: value types (`StarlarkInt/Float/List/Tuple/Dict/RangeList`, + `Sequence`), `Mutability`, `StarlarkSemantics`, `Printer`, `Module`, + `StarlarkThread`, `EvalUtils`, `StarlarkValue`/`NoneType`/`EvalException`/ + `Starlark` helpers, callable interfaces. + - ⬜ DEFERRED (critical for config loading): `Eval` tree-walking evaluator; + reflection dispatch (`CallUtils`/`MethodDescriptor`/`ParamDescriptor`/ + `BuiltinFunction`/`StarlarkFunction`) mapping `[StarlarkMethod]` → calls; + `MethodLibrary`/`StringModule` (string methods); `StarlarkSet`. +- ✅ `spelling/` — `SpellChecker`. - ⬜ `lib/json` — `Json` module (interop with `System.Text.Json`). -- ⬜ `spelling/` — `SpellChecker`. -- ⬜ Decide reflection strategy: Java uses annotation processing + reflection. - In C#, use reflection over `[StarlarkMethod]` attributes at startup, cached - per type. (Source generators are a later optimization.) -- 🔬 Validate with a handful of upstream Starlark eval tests ported to xUnit. +- ⬜ Reflection strategy: reflect over `[StarlarkMethod]`/`[Param]` at startup, + cached per type (source generators a later optimization). +- 🔬 Validate with upstream Starlark eval tests once the evaluator lands. ## Phase 3 — Config model & core module (needs Phases 1–2) diff --git a/src/Copybara.Core/Util/CommandRunner.cs b/src/Copybara.Core/Util/CommandRunner.cs index 83e013cf5..6d6196268 100644 --- a/src/Copybara.Core/Util/CommandRunner.cs +++ b/src/Copybara.Core/Util/CommandRunner.cs @@ -116,7 +116,7 @@ public CommandOutputWithStatus Execute() Logger.LogInformation("{Msg}", validStartMsg); if (_verbose) { - Console.Error.WriteLine(validStartMsg); + System.Console.Error.WriteLine(validStartMsg); } var stdoutCollector = new MemoryStream(); @@ -276,7 +276,7 @@ public CommandOutputWithStatus Execute() } if (_verbose) { - Console.Error.WriteLine(finishMsg); + System.Console.Error.WriteLine(finishMsg); } } } @@ -339,7 +339,7 @@ private Stream CommandOutputStream(Stream outputStream, int maxOutLogLines) { return outputStream; } - var console = Console.OpenStandardError(); + var console = System.Console.OpenStandardError(); var limited = new LimitFilterOutputStream( console, // Assume a line has ~200 characters. If no limit, ~10k lines. diff --git a/src/Starlark/Syntax/AssignmentStatement.cs b/src/Starlark/Syntax/AssignmentStatement.cs index bdc537eff..a4fc97d3e 100644 --- a/src/Starlark/Syntax/AssignmentStatement.cs +++ b/src/Starlark/Syntax/AssignmentStatement.cs @@ -50,7 +50,7 @@ internal AssignmentStatement( this.docComments = docComments; if (type != null) { - if (lhs.Kind != ExpressionKind.IDENTIFIER) + if (lhs.Kind != Expression.ExpressionKind.IDENTIFIER) { throw new InvalidOperationException("Can't have type annotation on complex LHS"); } @@ -65,7 +65,7 @@ internal AssignmentStatement( public Expression GetLHS() => lhs; /// Returns the type expression (if present) of the variable on the LHS. - public Expression? GetType() => type; + public new Expression? GetType() => type; /// Returns the operator of an augmented assignment, or null for an ordinary assignment. public TokenKind? GetOperator() => op; diff --git a/src/Starlark/Syntax/CastExpression.cs b/src/Starlark/Syntax/CastExpression.cs index 3a68e581b..37a0c7ec0 100644 --- a/src/Starlark/Syntax/CastExpression.cs +++ b/src/Starlark/Syntax/CastExpression.cs @@ -40,7 +40,7 @@ internal CastExpression( public override int GetEndOffset() => rparenOffset + 1; - public Expression GetType() => type; + public new Expression GetType() => type; /// /// Returns the Starlark type extracted from the expression. Non-null after diff --git a/src/Starlark/Syntax/IsInstanceExpression.cs b/src/Starlark/Syntax/IsInstanceExpression.cs index bcdd51e1d..5d658fe8f 100644 --- a/src/Starlark/Syntax/IsInstanceExpression.cs +++ b/src/Starlark/Syntax/IsInstanceExpression.cs @@ -40,7 +40,7 @@ internal IsInstanceExpression( public Expression GetValue() => value; - public Expression GetType() => type; + public new Expression GetType() => type; public override string ToString() { diff --git a/src/Starlark/Syntax/Lexer.cs b/src/Starlark/Syntax/Lexer.cs new file mode 100644 index 000000000..2fb9c331d --- /dev/null +++ b/src/Starlark/Syntax/Lexer.cs @@ -0,0 +1,1092 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Globalization; +using System.Text; + +namespace Starlark.Syntax; + +/// A scanner for Starlark. +internal sealed class Lexer +{ + // --- These fields are accessed directly by the parser: --- + + // Mapping from file offsets to Locations. + internal readonly FileLocations Locs; + + // Information about current token. Updated by NextToken. + internal TokenKind Kind; + internal int Start; // start offset + internal int End; // end offset + internal object? Value; // String, Integer/Long/BigInteger, or Double value of token + + private bool kindSet; // whether Kind is meaningful (mirrors Java's null check) + + // --- end of parser-visible fields --- + + private readonly List errors; + + private readonly FileOptions options; + + // Input buffer and position + private readonly char[] buffer; + private int pos; + + // The stack of enclosing indentation levels in spaces. + private readonly List indentStack = new(); + + private readonly ImmutableArray.Builder comments = ImmutableArray.CreateBuilder(); + + private int openParenStackDepth = 0; + + private bool checkIndentation; + + private int dents; + + private bool lineOnlyWhitespaceOrComments; + + private static readonly Dictionary EQUAL_TOKENS = new() + { + { '=', TokenKind.EQUALS_EQUALS }, + { '!', TokenKind.NOT_EQUALS }, + { '>', TokenKind.GREATER_EQUALS }, + { '<', TokenKind.LESS_EQUALS }, + { '+', TokenKind.PLUS_EQUALS }, + { '-', TokenKind.MINUS_EQUALS }, + { '*', TokenKind.STAR_EQUALS }, + { '/', TokenKind.SLASH_EQUALS }, + { '%', TokenKind.PERCENT_EQUALS }, + { '^', TokenKind.CARET_EQUALS }, + { '&', TokenKind.AMPERSAND_EQUALS }, + { '|', TokenKind.PIPE_EQUALS }, + }; + + internal Lexer(ParserInput input, List errors, FileOptions options) + { + this.Locs = FileLocations.Create(input.GetContent(), input.GetFile()); + this.buffer = input.GetContent(); + this.pos = 0; + this.errors = errors; + this.options = options; + this.checkIndentation = true; + this.dents = 0; + this.lineOnlyWhitespaceOrComments = true; + + indentStack.Add(0); + } + + internal IReadOnlyList GetComments() => comments.ToImmutable(); + + /// + /// Reads the next token, updating the Lexer's token fields. The end state is EOF, after which any + /// further calls to NextToken() will produce only EOF. + /// + internal void NextToken() + { + bool afterNewline = + kindSet && (Kind == TokenKind.NEWLINE || Kind == TokenKind.DOC_COMMENT_BLOCK); + Tokenize(); + if (!kindSet) + { + throw new InvalidOperationException("kind not set"); + } + + if (Kind == TokenKind.EOF && !afterNewline) + { + Kind = TokenKind.NEWLINE; + } + if (Kind != TokenKind.NEWLINE + && Kind != TokenKind.INDENT + && Kind != TokenKind.OUTDENT + && Kind != TokenKind.DOC_COMMENT_BLOCK + && Kind != TokenKind.DOC_COMMENT_TRAILING) + { + lineOnlyWhitespaceOrComments = false; + } + } + + private void PopParen() + { + if (openParenStackDepth == 0) + { + Error("indentation error", pos - 1); + } + else + { + openParenStackDepth--; + } + } + + private void Error(string message, int errPos) + { + errors.Add(new SyntaxError(Locs.GetLocation(errPos), message)); + } + + private void SetToken(TokenKind kind, int start, int end) + { + this.Kind = kind; + this.kindSet = true; + this.Start = start; + this.End = end; + this.Value = null; + } + + private void SetValue(object value) + { + this.Value = value; + } + + /// Returns the raw input text associated with the current token. + internal string GetRaw() => BufferSlice(Start, End); + + private void Newline() + { + lineOnlyWhitespaceOrComments = true; + if (openParenStackDepth > 0) + { + NewlineInsideExpression(); + } + else + { + checkIndentation = true; + SetToken(TokenKind.NEWLINE, pos - 1, pos); + } + } + + private void NewlineInsideExpression() + { + while (pos < buffer.Length) + { + switch (buffer[pos]) + { + case ' ': + case '\t': + case '\r': + pos++; + break; + default: + return; + } + } + } + + private void ComputeIndentation() + { + int indentLen = 0; + while (pos < buffer.Length) + { + char c = buffer[pos]; + if (c == ' ') + { + indentLen++; + pos++; + } + else if (c == '\r') + { + pos++; + } + else if (c == '\t') + { + indentLen++; + pos++; + Error("Tab characters are not allowed for indentation. Use spaces instead.", pos); + } + else if (c == '\n') + { + indentLen = 0; + pos++; + } + else if (c == '#') + { + if (Peek(1) == ':' && openParenStackDepth == 0) + { + return; + } + int oldPos = pos; + ScanToNewline(); + AddComment(oldPos, pos); + indentLen = 0; + } + else + { + break; + } + } + + if (pos == buffer.Length) + { + indentLen = 0; + } + + int peekedIndent = IndentPeek(); + if (peekedIndent < indentLen) + { + indentStack.Add(indentLen); + dents++; + } + else if (peekedIndent > indentLen) + { + while (peekedIndent > indentLen) + { + indentStack.RemoveAt(indentStack.Count - 1); + dents--; + peekedIndent = IndentPeek(); + } + + if (peekedIndent < indentLen) + { + Error("indentation error", pos - 1); + } + } + } + + private int IndentPeek() => indentStack[indentStack.Count - 1]; + + private bool SkipTripleQuote(char quot) + { + if (Peek(0) == quot && Peek(1) == quot) + { + pos += 2; + return true; + } + return false; + } + + private void EscapedStringLiteral(char quot, bool isRaw) + { + int literalStartPos = isRaw ? pos - 2 : pos - 1; + bool inTriplequote = SkipTripleQuote(quot); + var literal = new StringBuilder(); + while (pos < buffer.Length) + { + char c = buffer[pos]; + pos++; + switch (c) + { + case '\n': + if (inTriplequote) + { + literal.Append(c); + break; + } + else + { + Error("unclosed string literal", literalStartPos); + SetToken(TokenKind.STRING, literalStartPos, pos); + SetValue(literal.ToString()); + return; + } + case '\\': + if (pos == buffer.Length) + { + Error("unclosed string literal", literalStartPos); + SetToken(TokenKind.STRING, literalStartPos, pos); + SetValue(literal.ToString()); + return; + } + if (isRaw) + { + literal.Append('\\'); + if (Peek(0) == '\r' && Peek(1) == '\n') + { + literal.Append('\n'); + pos += 2; + } + else if (buffer[pos] == '\r' || buffer[pos] == '\n') + { + literal.Append('\n'); + pos += 1; + } + else + { + literal.Append(buffer[pos]); + pos += 1; + } + break; + } + c = buffer[pos]; + pos++; + switch (c) + { + case '\r': + if (Peek(0) == '\n') + { + pos += 1; + } + break; + case '\n': + break; + case 'a': + literal.Append('\u0007'); + break; + case 'b': + literal.Append('\b'); + break; + case 'f': + literal.Append('\f'); + break; + case 'n': + literal.Append('\n'); + break; + case 'r': + literal.Append('\r'); + break; + case 't': + literal.Append('\t'); + break; + case 'v': + literal.Append('\u000b'); + break; + case '\\': + literal.Append('\\'); + break; + case '\'': + literal.Append('\''); + break; + case '"': + literal.Append('"'); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + { + int octal = c - '0'; + if (pos < buffer.Length) + { + c = buffer[pos]; + if (c >= '0' && c <= '7') + { + pos++; + octal = (octal << 3) | (c - '0'); + if (pos < buffer.Length) + { + c = buffer[pos]; + if (c >= '0' && c <= '7') + { + pos++; + octal = (octal << 3) | (c - '0'); + } + } + } + } + if (octal > 0xff) + { + Error("octal escape sequence out of range (maximum is \\377)", pos - 1); + } + else if (options.StringLiteralsAreAsciiOnly && octal >= 0x80) + { + Error("octal escape sequence denotes non-ASCII character", pos - 1); + } + literal.Append((char)(octal & 0xff)); + break; + } + case 'N': + case 'u': + case 'U': + default: + Error("invalid escape sequence: \\" + c + ". Use '\\\\' to insert '\\'.", pos - 1); + literal.Append('\\'); + literal.Append(c); + break; + } + break; + case '\'': + case '"': + if (c != quot || (inTriplequote && !SkipTripleQuote(quot))) + { + literal.Append(c); + } + else + { + SetToken(TokenKind.STRING, literalStartPos, pos); + SetValue(literal.ToString()); + return; + } + break; + default: + literal.Append(c); + if (options.StringLiteralsAreAsciiOnly && c >= 0x80) + { + Error("string literal contains non-ASCII character", pos - 1); + } + break; + } + } + Error("unclosed string literal", literalStartPos); + SetToken(TokenKind.STRING, literalStartPos, pos); + SetValue(literal.ToString()); + } + + private void StringLiteralScan(char quot, bool isRaw) + { + int literalStartPos = isRaw ? pos - 2 : pos - 1; + int contentStartPos = pos; + + if (SkipTripleQuote(quot)) + { + pos -= 2; + EscapedStringLiteral(quot, isRaw); + return; + } + + while (pos < buffer.Length) + { + char c = buffer[pos++]; + switch (c) + { + case '\n': + Error("unclosed string literal", literalStartPos); + SetToken(TokenKind.STRING, literalStartPos, pos); + SetValue(BufferSlice(contentStartPos, pos - 1)); + return; + case '\\': + if (isRaw) + { + if (Peek(0) == '\r' && Peek(1) == '\n') + { + pos = contentStartPos; + EscapedStringLiteral(quot, true); + return; + } + else + { + pos++; + break; + } + } + pos = contentStartPos; + EscapedStringLiteral(quot, false); + return; + case '\'': + case '"': + if (c == quot) + { + SetToken(TokenKind.STRING, literalStartPos, pos); + SetValue(BufferSlice(contentStartPos, pos - 1)); + if (options.StringLiteralsAreAsciiOnly) + { + for (int i = contentStartPos; i < pos - 1; i++) + { + if (buffer[i] >= 0x80) + { + Error("string literal contains non-ASCII character", i); + } + } + } + return; + } + break; + default: + break; + } + } + + if (pos > buffer.Length) + { + pos = buffer.Length; + } + + Error("unclosed string literal", literalStartPos); + SetToken(TokenKind.STRING, literalStartPos, pos); + SetValue(BufferSlice(contentStartPos, pos)); + } + + private void DocCommentsScan(Comment first, int firstStartPos, bool isBlock) + { + int lastEndPos = pos; + var docComments = new List { first }; + if (isBlock) + { + int prevLine = first.GetStartLocation().Line; + while (Peek(0) == '\n') + { + checkIndentation = false; + ComputeIndentation(); + if (Peek(0) != '#' || Peek(1) != ':') + { + break; + } + int line = Locs.GetLocation(pos).Line; + if (line != prevLine + 1) + { + break; + } + prevLine = line; + int startPos = pos; + ScanToNewline(); + Comment comment = AddComment(startPos, pos); + lastEndPos = pos; + docComments.Add(comment); + } + SetToken(TokenKind.DOC_COMMENT_BLOCK, firstStartPos, lastEndPos); + } + else + { + SetToken(TokenKind.DOC_COMMENT_TRAILING, firstStartPos, lastEndPos); + } + SetValue(new DocComments(docComments)); + } + + private void ScanToNewline() + { + for (; pos < buffer.Length; pos++) + { + if (buffer[pos] == '\n') + { + break; + } + } + } + + private static readonly Dictionary keywordMap = new() + { + { "and", TokenKind.AND }, + { "as", TokenKind.AS }, + { "assert", TokenKind.ASSERT }, + { "break", TokenKind.BREAK }, + { "class", TokenKind.CLASS }, + { "continue", TokenKind.CONTINUE }, + { "def", TokenKind.DEF }, + { "del", TokenKind.DEL }, + { "elif", TokenKind.ELIF }, + { "else", TokenKind.ELSE }, + { "except", TokenKind.EXCEPT }, + { "finally", TokenKind.FINALLY }, + { "for", TokenKind.FOR }, + { "from", TokenKind.FROM }, + { "global", TokenKind.GLOBAL }, + { "if", TokenKind.IF }, + { "import", TokenKind.IMPORT }, + { "in", TokenKind.IN }, + { "is", TokenKind.IS }, + { "lambda", TokenKind.LAMBDA }, + { "load", TokenKind.LOAD }, + { "nonlocal", TokenKind.NONLOCAL }, + { "not", TokenKind.NOT }, + { "or", TokenKind.OR }, + { "pass", TokenKind.PASS }, + { "raise", TokenKind.RAISE }, + { "return", TokenKind.RETURN }, + { "try", TokenKind.TRY }, + { "while", TokenKind.WHILE }, + { "with", TokenKind.WITH }, + { "yield", TokenKind.YIELD }, + }; + + private static readonly Dictionary typeSyntaxExtraKeywordMap = new() + { + { "cast", TokenKind.CAST }, + { "isinstance", TokenKind.ISINSTANCE }, + }; + + private void IdentifierOrKeyword() + { + int oldPos = pos - 1; + string id = string.Intern(ScanIdentifier()); + if (!keywordMap.TryGetValue(id, out TokenKind kind)) + { + if (options.AllowTypeSyntax && typeSyntaxExtraKeywordMap.TryGetValue(id, out TokenKind tk)) + { + SetToken(tk, oldPos, pos); + return; + } + SetToken(TokenKind.IDENTIFIER, oldPos, pos); + SetValue(id); + } + else + { + SetToken(kind, oldPos, pos); + } + } + + private string ScanIdentifier() + { + int oldPos = pos - 1; + while (pos < buffer.Length) + { + char c = buffer[pos]; + if (c == '_' + || (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9')) + { + pos++; + } + else + { + return BufferSlice(oldPos, pos); + } + } + return BufferSlice(oldPos, pos); + } + + private bool TokenizeTwoChars() + { + if (pos + 2 >= buffer.Length) + { + return false; + } + char c1 = buffer[pos]; + char c2 = buffer[pos + 1]; + TokenKind? tok = null; + if (c2 == '=') + { + if (EQUAL_TOKENS.TryGetValue(c1, out TokenKind t)) + { + tok = t; + } + } + else if (c2 == '*' && c1 == '*') + { + tok = TokenKind.STAR_STAR; + } + if (tok == null) + { + return false; + } + SetToken(tok.Value, pos, pos + 2); + return true; + } + + // Returns the ith unconsumed char, or -1 for EOF. + private int Peek(int i) => pos + i < buffer.Length ? buffer[pos + i] : -1; + + // Consumes a char and returns the next unconsumed char, or -1 for EOF. + private int Next() + { + pos++; + return Peek(0); + } + + private void Tokenize() + { + if (checkIndentation) + { + checkIndentation = false; + ComputeIndentation(); + } + + if (dents != 0) + { + if (dents < 0) + { + dents++; + SetToken(TokenKind.OUTDENT, pos - 1, pos); + } + else + { + dents--; + SetToken(TokenKind.INDENT, pos - 1, pos); + } + return; + } + + kindSet = false; + while (pos < buffer.Length) + { + if (TokenizeTwoChars()) + { + pos += 2; + return; + } + char c = buffer[pos]; + pos++; + switch (c) + { + case '{': + SetToken(TokenKind.LBRACE, pos - 1, pos); + openParenStackDepth++; + break; + case '}': + SetToken(TokenKind.RBRACE, pos - 1, pos); + PopParen(); + break; + case '(': + SetToken(TokenKind.LPAREN, pos - 1, pos); + openParenStackDepth++; + break; + case ')': + SetToken(TokenKind.RPAREN, pos - 1, pos); + PopParen(); + break; + case '[': + SetToken(TokenKind.LBRACKET, pos - 1, pos); + openParenStackDepth++; + break; + case ']': + SetToken(TokenKind.RBRACKET, pos - 1, pos); + PopParen(); + break; + case '>': + if (Peek(0) == '>' && Peek(1) == '=') + { + SetToken(TokenKind.GREATER_GREATER_EQUALS, pos - 1, pos + 2); + pos += 2; + } + else if (Peek(0) == '>') + { + SetToken(TokenKind.GREATER_GREATER, pos - 1, pos + 1); + pos += 1; + } + else + { + SetToken(TokenKind.GREATER, pos - 1, pos); + } + break; + case '<': + if (Peek(0) == '<' && Peek(1) == '=') + { + SetToken(TokenKind.LESS_LESS_EQUALS, pos - 1, pos + 2); + pos += 2; + } + else if (Peek(0) == '<') + { + SetToken(TokenKind.LESS_LESS, pos - 1, pos + 1); + pos += 1; + } + else + { + SetToken(TokenKind.LESS, pos - 1, pos); + } + break; + case ':': + SetToken(TokenKind.COLON, pos - 1, pos); + break; + case ',': + SetToken(TokenKind.COMMA, pos - 1, pos); + break; + case '+': + SetToken(TokenKind.PLUS, pos - 1, pos); + break; + case '-': + if (Peek(0) == '>') + { + SetToken(TokenKind.RARROW, pos - 1, pos + 1); + pos += 1; + } + else + { + SetToken(TokenKind.MINUS, pos - 1, pos); + } + break; + case '|': + SetToken(TokenKind.PIPE, pos - 1, pos); + break; + case '=': + SetToken(TokenKind.EQUALS, pos - 1, pos); + break; + case '%': + SetToken(TokenKind.PERCENT, pos - 1, pos); + break; + case '~': + SetToken(TokenKind.TILDE, pos - 1, pos); + break; + case '&': + SetToken(TokenKind.AMPERSAND, pos - 1, pos); + break; + case '^': + SetToken(TokenKind.CARET, pos - 1, pos); + break; + case '/': + if (Peek(0) == '/' && Peek(1) == '=') + { + SetToken(TokenKind.SLASH_SLASH_EQUALS, pos - 1, pos + 2); + pos += 2; + } + else if (Peek(0) == '/') + { + SetToken(TokenKind.SLASH_SLASH, pos - 1, pos + 1); + pos += 1; + } + else + { + SetToken(TokenKind.SLASH, pos - 1, pos); + } + break; + case ';': + SetToken(TokenKind.SEMI, pos - 1, pos); + break; + case '*': + SetToken(TokenKind.STAR, pos - 1, pos); + break; + case ' ': + case '\t': + case '\r': + break; + case '\\': + if (Peek(0) == '\n') + { + pos += 1; + } + else if (Peek(0) == '\r' && Peek(1) == '\n') + { + pos += 2; + } + else + { + SetToken(TokenKind.ILLEGAL, pos - 1, pos); + SetValue(c.ToString()); + } + break; + case '\n': + Newline(); + break; + case '#': + { + int oldPos = pos - 1; + ScanToNewline(); + Comment comment = AddComment(oldPos, pos); + if (comment.HasDocCommentPrefix() && openParenStackDepth == 0) + { + DocCommentsScan(comment, oldPos, lineOnlyWhitespaceOrComments); + } + break; + } + case '\'': + case '"': + StringLiteralScan(c, false); + break; + default: + // detect raw strings, e.g. r"str" + if (c == 'r') + { + int c0 = Peek(0); + if (c0 == '\'' || c0 == '"') + { + pos++; + StringLiteralScan((char)c0, true); + break; + } + } + + if (c == '.' || IsDigit(c)) + { + pos--; // unconsume + ScanNumberOrDotOrEllipsis(c); + break; + } + + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_') + { + IdentifierOrKeyword(); + } + else + { + Error("invalid character: '" + c + "'", pos - 1); + } + break; + } + if (kindSet) + { + return; + } + } + + if (indentStack.Count > 1) + { + SetToken(TokenKind.NEWLINE, pos - 1, pos); + while (indentStack.Count > 1) + { + indentStack.RemoveAt(indentStack.Count - 1); + dents--; + } + return; + } + + SetToken(TokenKind.EOF, pos, pos); + } + + private void ScanNumberOrDotOrEllipsis(int c) + { + int start = this.pos; + bool fraction = false; + bool exponent = false; + + if (c == '.') + { + if (!IsDigit(Peek(1))) + { + if (Peek(1) == '.' && Peek(2) == '.') + { + pos += 3; + SetToken(TokenKind.ELLIPSIS, start, pos); + return; + } + else + { + pos++; + SetToken(TokenKind.DOT, start, pos); + return; + } + } + fraction = true; + } + else if (c == '0') + { + c = Next(); + if (c == '.') + { + fraction = true; + } + else if (c == 'x' || c == 'X') + { + c = Next(); + if (!IsXDigit(c)) + { + Error("invalid hex literal", start); + } + while (IsXDigit(c)) + { + c = Next(); + } + } + else if (c == 'o' || c == 'O') + { + c = Next(); + while (IsDigit(c)) + { + c = Next(); + } + } + else if (c == 'b' || c == 'B') + { + c = Next(); + if (!IsBDigit(c)) + { + Error("invalid binary literal", start); + } + while (IsBDigit(c)) + { + c = Next(); + } + } + else + { + while (IsDigit(c)) + { + c = Next(); + } + if (c == '.') + { + fraction = true; + } + else if (c == 'e' || c == 'E') + { + exponent = true; + } + } + } + else + { + while (IsDigit(c)) + { + c = Next(); + } + if (c == '.') + { + fraction = true; + } + else if (c == 'e' || c == 'E') + { + exponent = true; + } + } + + if (fraction) + { + c = Next(); // consume '.' + while (IsDigit(c)) + { + c = Next(); + } + + if (c == 'e' || c == 'E') + { + exponent = true; + } + } + + if (exponent) + { + c = Next(); // consume [eE] + if (c == '+' || c == '-') + { + c = Next(); + } + while (IsDigit(c)) + { + c = Next(); + } + } + + if (fraction || exponent) + { + SetToken(TokenKind.FLOAT, start, pos); + double value = 0.0; + if (double.TryParse(BufferSlice(start, pos), NumberStyles.Float, CultureInfo.InvariantCulture, out value)) + { + if (!double.IsFinite(value)) + { + Error("floating-point literal too large", start); + } + } + else + { + Error("invalid float literal", start); + } + SetValue(value); + return; + } + + SetToken(TokenKind.INT, start, pos); + string literal = BufferSlice(start, pos); + object value2 = 0; + try + { + value2 = IntLiteral.Scan(literal); + } + catch (FormatException ex) + { + Error(ex.Message, start); + } + SetValue(value2); + } + + private static bool IsDigit(int c) => '0' <= c && c <= '9'; + + private static bool IsXDigit(int c) => + IsDigit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'); + + private static bool IsBDigit(int c) => c == '0' || c == '1'; + + /// + /// Returns a string containing the part of the source buffer beginning at offset start and + /// ending immediately before offset end. + /// + internal string BufferSlice(int start, int end) => new string(this.buffer, start, end - start); + + private Comment AddComment(int start, int end) + { + string content = BufferSlice(start, end); + Comment comment = new Comment(Locs, start, content); + comments.Add(comment); + return comment; + } +} diff --git a/src/Starlark/Syntax/Parameter.cs b/src/Starlark/Syntax/Parameter.cs index fa9051f65..6838a0822 100644 --- a/src/Starlark/Syntax/Parameter.cs +++ b/src/Starlark/Syntax/Parameter.cs @@ -40,7 +40,7 @@ private protected Parameter(FileLocations locs, Identifier? id, Expression? type public virtual Expression? GetDefaultValue() => null; - public Expression? GetType() => type; + public new Expression? GetType() => type; /// /// Syntax node for a mandatory parameter, f(id). It may be positional or keyword-only diff --git a/src/Starlark/Syntax/Parser.cs b/src/Starlark/Syntax/Parser.cs new file mode 100644 index 000000000..988e07f3f --- /dev/null +++ b/src/Starlark/Syntax/Parser.cs @@ -0,0 +1,1626 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; + +namespace Starlark.Syntax; + +/// Parser is a recursive-descent parser for Starlark. +internal sealed class Parser +{ + /// Combines the parser result into a single value object. + internal sealed class ParseResult + { + internal readonly FileLocations Locs; + internal readonly ImmutableArray Statements; + internal readonly ImmutableArray Comments; + internal readonly List Errors; + + internal ParseResult( + FileLocations locs, + ImmutableArray statements, + ImmutableArray comments, + List errors) + { + this.Locs = locs; + this.Statements = statements; + this.Comments = comments; + this.Errors = errors; + } + } + + private static readonly HashSet STATEMENT_TERMINATOR_SET = new() + { + TokenKind.EOF, TokenKind.NEWLINE, TokenKind.DOC_COMMENT_TRAILING, TokenKind.SEMI, + }; + + private static readonly HashSet LIST_TERMINATOR_SET = new() + { + TokenKind.EOF, TokenKind.RBRACKET, TokenKind.SEMI, + }; + + private static readonly HashSet DICT_TERMINATOR_SET = new() + { + TokenKind.EOF, TokenKind.RBRACE, TokenKind.SEMI, + }; + + private static readonly HashSet EXPR_LIST_TERMINATOR_SET = new() + { + TokenKind.EOF, + TokenKind.NEWLINE, + TokenKind.DOC_COMMENT_TRAILING, + TokenKind.EQUALS, + TokenKind.RBRACE, + TokenKind.RBRACKET, + TokenKind.RPAREN, + TokenKind.SEMI, + }; + + private static readonly HashSet EXPR_TERMINATOR_SET = new() + { + TokenKind.COLON, + TokenKind.COMMA, + TokenKind.EOF, + TokenKind.FOR, + TokenKind.MINUS, + TokenKind.PERCENT, + TokenKind.PLUS, + TokenKind.RBRACKET, + TokenKind.RPAREN, + TokenKind.SLASH, + }; + + private const string TYPE_SOFT_KEYWORD = "type"; + + private const bool DEBUGGING = false; + + private readonly FileOptions options; + + private readonly Lexer lexer; + private readonly FileLocations locs; + private readonly List errors; + + private DocComments? mostRecentDocCommentBlock = null; + + private bool insideTypeExpr = false; + + private static readonly Dictionary augmentedAssignments = new() + { + { TokenKind.PLUS_EQUALS, TokenKind.PLUS }, + { TokenKind.MINUS_EQUALS, TokenKind.MINUS }, + { TokenKind.STAR_EQUALS, TokenKind.STAR }, + { TokenKind.SLASH_EQUALS, TokenKind.SLASH }, + { TokenKind.SLASH_SLASH_EQUALS, TokenKind.SLASH_SLASH }, + { TokenKind.PERCENT_EQUALS, TokenKind.PERCENT }, + { TokenKind.AMPERSAND_EQUALS, TokenKind.AMPERSAND }, + { TokenKind.CARET_EQUALS, TokenKind.CARET }, + { TokenKind.PIPE_EQUALS, TokenKind.PIPE }, + { TokenKind.GREATER_GREATER_EQUALS, TokenKind.GREATER_GREATER }, + { TokenKind.LESS_LESS_EQUALS, TokenKind.LESS_LESS }, + }; + + private static readonly List> operatorPrecedence = new() + { + new HashSet { TokenKind.OR }, + new HashSet { TokenKind.AND }, + new HashSet { TokenKind.NOT }, + new HashSet + { + TokenKind.EQUALS_EQUALS, + TokenKind.NOT_EQUALS, + TokenKind.LESS, + TokenKind.LESS_EQUALS, + TokenKind.GREATER, + TokenKind.GREATER_EQUALS, + TokenKind.IN, + TokenKind.NOT_IN, + }, + new HashSet { TokenKind.PIPE }, + new HashSet { TokenKind.CARET }, + new HashSet { TokenKind.AMPERSAND }, + new HashSet { TokenKind.GREATER_GREATER, TokenKind.LESS_LESS }, + new HashSet { TokenKind.MINUS, TokenKind.PLUS }, + new HashSet { TokenKind.SLASH, TokenKind.SLASH_SLASH, TokenKind.STAR, TokenKind.PERCENT }, + }; + + private int errorsCount; + private bool recoveryMode; + + private readonly Dictionary stringInterner = new(); + + private Parser(Lexer lexer, List errors, FileOptions options) + { + this.lexer = lexer; + this.locs = lexer.Locs; + this.errors = errors; + this.options = options; + NextToken(); + } + + // token.kind is a prettier alias for lexer.Kind, etc. + private TokenKind TokenKindCur => lexer.Kind; + private int TokenStart => lexer.Start; + private int TokenEnd => lexer.End; + private object? TokenValue => lexer.Value; + + private string Intern(string s) + { + if (stringInterner.TryGetValue(s, out string? prev)) + { + return prev; + } + stringInterner[s] = s; + return s; + } + + private static string TokenString(TokenKind kind, object? value) + { + return kind == TokenKind.STRING + ? "\"" + value + "\"" + : value == null ? kind.ToDisplayString() : value.ToString()!; + } + + // Main entry point for parsing a file. + internal static ParseResult ParseFile(ParserInput input, FileOptions options) + { + var errors = new List(); + var lexer = new Lexer(input, errors, options); + var parser = new Parser(lexer, errors, options); + + StarlarkFile.IParseProfiler? profiler = Parser.Profiler; + long profileStartNanos = profiler != null ? profiler.Start() : -1; + try + { + ImmutableArray statements = parser.ParseFileInput(); + return new ParseResult(lexer.Locs, statements, lexer.GetComments().ToImmutableArray(), errors); + } + finally + { + if (profileStartNanos != -1) + { + profiler!.End(profileStartNanos, input.GetFile()); + } + } + } + + internal static StarlarkFile.IParseProfiler? Profiler; + + private void ParseStatement(ImmutableArray.Builder list) + { + if (TokenKindCur == TokenKind.DEF) + { + list.Add(ParseDefStatement()); + } + else if (TokenKindCur == TokenKind.IF) + { + list.Add(ParseIfStatement()); + } + else if (TokenKindCur == TokenKind.FOR) + { + list.Add(ParseForStatement()); + } + else + { + ParseSimpleStatement(list); + } + } + + private void MaybeParseDocCommentBlock() + { + while (TokenKindCur == TokenKind.DOC_COMMENT_BLOCK) + { + mostRecentDocCommentBlock = (DocComments)TokenValue!; + NextToken(); + } + } + + private DocComments? GetDocCommentBlockOnPreviousLine(int line) + { + if (mostRecentDocCommentBlock != null + && mostRecentDocCommentBlock.GetEndLocation().Line + 1 == line) + { + return mostRecentDocCommentBlock; + } + return null; + } + + /// Parses an expression, possibly preceded or followed by comments or whitespace. + internal static Expression ParseExpression(ParserInput input, FileOptions options) + { + return ParseValueOrTypeExpr(input, options, false); + } + + /// Parses a type expression, possibly preceded or followed by comments or whitespace. + internal static Expression ParseTypeExpression(ParserInput input, FileOptions options) + { + return ParseValueOrTypeExpr(input, options, true); + } + + private static Expression ParseValueOrTypeExpr(ParserInput input, FileOptions options, bool isTypeExpr) + { + var errors = new List(); + var lexer = new Lexer(input, errors, options); + var parser = new Parser(lexer, errors, options); + Expression? result = null; + while (parser.TokenKindCur == TokenKind.DOC_COMMENT_BLOCK) + { + parser.NextToken(); + } + result = isTypeExpr ? parser.ParseTypeExprWithFallback() : parser.ParseExpr(); + while (parser.TokenKindCur == TokenKind.NEWLINE + || parser.TokenKindCur == TokenKind.DOC_COMMENT_BLOCK + || parser.TokenKindCur == TokenKind.DOC_COMMENT_TRAILING) + { + parser.NextToken(); + } + parser.Expect(TokenKind.EOF); + if (errors.Count != 0) + { + throw new SyntaxError.Exception(errors); + } + return result; + } + + private Expression ParseExpr() + { + Expression e = ParseTest(); + if (TokenKindCur != TokenKind.COMMA) + { + return e; + } + + var elems = ImmutableArray.CreateBuilder(); + elems.Add(e); + ParseExprList(elems, false); + return new ListExpression(locs, true, -1, elems.ToImmutable(), -1); + } + + private void ReportError(int offset, string format, params object?[] args) + { + errorsCount++; + if (errorsCount <= 5) + { + Location location = locs.GetLocation(offset); + errors.Add(new SyntaxError(location, args.Length == 0 ? format : string.Format(format, args))); + } + } + + private void SyntaxErrorMsg(string message) + { + SyntaxErrorAt(TokenStart, TokenKindCur, TokenValue, message); + } + + private void SyntaxErrorAt(int offset, TokenKind tokenKind, object? tokenValue, string message) + { + if (!recoveryMode) + { + if (tokenKind == TokenKind.INDENT) + { + ReportError(offset, "indentation error"); + } + else + { + ReportError(offset, "syntax error at '{0}': {1}", TokenString(tokenKind, tokenValue), message); + } + recoveryMode = true; + } + } + + private int Expect(TokenKind kind) + { + if (TokenKindCur != kind) + { + SyntaxErrorMsg("expected " + kind.ToDisplayString()); + } + return NextToken(); + } + + private int ExpectAndRecover(TokenKind kind) + { + if (TokenKindCur != kind) + { + SyntaxErrorMsg("expected " + kind.ToDisplayString()); + } + else + { + recoveryMode = false; + } + return NextToken(); + } + + private int SyncPast(HashSet terminatingTokens) + { + while (!terminatingTokens.Contains(TokenKindCur)) + { + NextToken(); + } + int end = TokenEnd; + NextToken(); + return end; + } + + private int SyncTo(HashSet terminatingTokens) + { + int previous = TokenEnd; + NextToken(); + int current = previous; + while (!terminatingTokens.Contains(TokenKindCur)) + { + NextToken(); + previous = current; + current = TokenEnd; + } + return previous; + } + + private static readonly HashSet FORBIDDEN_KEYWORDS = new() + { + TokenKind.AS, + TokenKind.ASSERT, + TokenKind.CLASS, + TokenKind.DEL, + TokenKind.EXCEPT, + TokenKind.FINALLY, + TokenKind.FROM, + TokenKind.GLOBAL, + TokenKind.IMPORT, + TokenKind.IS, + TokenKind.NONLOCAL, + TokenKind.RAISE, + TokenKind.TRY, + TokenKind.WITH, + TokenKind.WHILE, + TokenKind.YIELD, + }; + + private void CheckForbiddenKeywords() + { + if (!FORBIDDEN_KEYWORDS.Contains(TokenKindCur)) + { + return; + } + string message = TokenKindCur switch + { + TokenKind.ASSERT => "'assert' not supported, use 'fail' instead", + TokenKind.DEL => "'del' not supported, use '.pop()' to delete an item from a dictionary or a list", + TokenKind.IMPORT => "'import' not supported, use 'load' instead", + TokenKind.IS => "'is' not supported, use '==' instead", + TokenKind.RAISE => "'raise' not supported, use 'fail' instead", + TokenKind.TRY => "'try' not supported, all exceptions are fatal", + TokenKind.WHILE => "'while' not supported, use 'for' instead", + _ => "keyword '" + TokenKindCur.ToDisplayString() + "' not supported", + }; + ReportError(TokenStart, "{0}", message); + } + + private int NextToken() + { + int prev = TokenStart; + if (TokenKindCur != TokenKind.EOF) + { + lexer.NextToken(); + } + CheckForbiddenKeywords(); + if (DEBUGGING) + { + Console.Error.Write(TokenString(TokenKindCur, TokenValue)); + } + return prev; + } + + private Identifier MakeErrorExpression(int start, int end) + { + return new Identifier(locs, lexer.BufferSlice(start, end), start); + } + + private Argument ParseArgument() + { + Expression expr; + + if (TokenKindCur == TokenKind.STAR_STAR) + { + int starStarOffset = NextToken(); + expr = ParseTest(); + return new Argument.StarStar(locs, starStarOffset, expr); + } + + if (TokenKindCur == TokenKind.STAR) + { + int starOffset = NextToken(); + expr = ParseTest(); + return new Argument.Star(locs, starOffset, expr); + } + + expr = ParseTest(); + if (expr is Identifier id) + { + if (TokenKindCur == TokenKind.EQUALS) + { + NextToken(); + Expression arg = ParseTest(); + return new Argument.Keyword(locs, id, arg); + } + } + + return new Argument.Positional(locs, expr); + } + + private Parameter ParseParameter(bool defStatement) + { + Expression? type = null; + + if (TokenKindCur == TokenKind.STAR_STAR) + { + int starStarOffset = NextToken(); + Identifier id = ParseIdent(); + if (defStatement) + { + type = MaybeParseTypeAnnotationAfter(TokenKind.COLON); + } + return new Parameter.StarStar(locs, starStarOffset, id, type); + } + + if (TokenKindCur == TokenKind.STAR) + { + int starOffset = NextToken(); + if (TokenKindCur == TokenKind.IDENTIFIER) + { + Identifier id = ParseIdent(); + if (defStatement) + { + type = MaybeParseTypeAnnotationAfter(TokenKind.COLON); + } + return new Parameter.Star(locs, starOffset, id, type); + } + return new Parameter.Star(locs, starOffset, null, null); + } + + Identifier id2 = ParseIdent(); + + if (defStatement) + { + type = MaybeParseTypeAnnotationAfter(TokenKind.COLON); + } + + if (TokenKindCur == TokenKind.EQUALS) + { + NextToken(); + Expression expr = ParseTest(); + return new Parameter.Optional(locs, id2, type, expr); + } + + return new Parameter.Mandatory(locs, id2, type); + } + + private Expression ParseCallSuffix(Expression fn) + { + ImmutableArray args = ImmutableArray.Empty; + int lparenOffset = Expect(TokenKind.LPAREN); + if (TokenKindCur != TokenKind.RPAREN) + { + args = ParseArguments(); + } + int rparenOffset = Expect(TokenKind.RPAREN); + return new CallExpression(locs, fn, locs.GetLocation(lparenOffset), args, rparenOffset); + } + + private Expression ParseCastExpression() + { + CheckAllowTypeSyntax(TokenStart, TokenKindCur, TokenValue); + int startOffset = Expect(TokenKind.CAST); + Expect(TokenKind.LPAREN); + Expression typeExpr = ParseTypeExprWithFallback(); + Expect(TokenKind.COMMA); + Expression valueExpr = ParseTest(); + if (TokenKindCur == TokenKind.COMMA) + { + Expect(TokenKind.COMMA); + } + int rparenOffset = Expect(TokenKind.RPAREN); + return new CastExpression(locs, startOffset, typeExpr, valueExpr, rparenOffset); + } + + private Expression ParseIsInstanceExpression() + { + CheckAllowTypeSyntax(TokenStart, TokenKindCur, TokenValue); + int startOffset = Expect(TokenKind.ISINSTANCE); + Expect(TokenKind.LPAREN); + Expression valueExpr = ParseTest(); + Expect(TokenKind.COMMA); + Expression typeExpr = ParseTypeExprWithFallback(); + if (TokenKindCur == TokenKind.COMMA) + { + Expect(TokenKind.COMMA); + } + int rparenOffset = Expect(TokenKind.RPAREN); + return new IsInstanceExpression(locs, startOffset, valueExpr, typeExpr, rparenOffset); + } + + private ImmutableArray ParseArguments() + { + bool seenArg = false; + var list = ImmutableArray.CreateBuilder(); + while (TokenKindCur != TokenKind.RPAREN && TokenKindCur != TokenKind.EOF) + { + if (seenArg) + { + if (TokenKindCur == TokenKind.FOR) + { + SyntaxErrorMsg("Starlark does not support Python-style generator expressions"); + } + Expect(TokenKind.COMMA); + if (TokenKindCur == TokenKind.RPAREN) + { + break; + } + } + list.Add(ParseArgument()); + seenArg = true; + } + return list.ToImmutable(); + } + + private Expression ParseSelectorSuffix(Expression e) + { + int dotOffset = Expect(TokenKind.DOT); + if (TokenKindCur == TokenKind.IDENTIFIER) + { + Identifier id = ParseIdent(); + return new DotExpression(locs, e, dotOffset, id); + } + + SyntaxErrorMsg("expected identifier after dot"); + SyncTo(EXPR_TERMINATOR_SET); + return e; + } + + private void ParseExprList(ImmutableArray.Builder list, bool trailingCommaAllowed) + { + while (TokenKindCur == TokenKind.COMMA) + { + Expect(TokenKind.COMMA); + if (EXPR_LIST_TERMINATOR_SET.Contains(TokenKindCur)) + { + if (!trailingCommaAllowed) + { + ReportError(TokenStart, "Trailing comma is allowed only in parenthesized tuples."); + } + break; + } + list.Add(ParseTest()); + } + } + + private List ParseDictEntryList() + { + var list = new List(); + while (TokenKindCur != TokenKind.RBRACE) + { + list.Add(ParseDictEntry()); + if (TokenKindCur == TokenKind.COMMA) + { + NextToken(); + } + else + { + break; + } + } + return list; + } + + private DictExpression.Entry ParseDictEntry() + { + Expression key = ParseTest(); + int colonOffset = Expect(TokenKind.COLON); + Expression value = ParseTest(); + return new DictExpression.Entry(locs, key, colonOffset, value); + } + + private StringLiteral ParseStringLiteral() + { + StringLiteral literal = + new StringLiteral(locs, TokenStart, Intern((string)TokenValue!), TokenEnd); + NextToken(); + if (TokenKindCur == TokenKind.STRING) + { + ReportError(TokenStart, "Implicit string concatenation is forbidden, use the + operator"); + } + return literal; + } + + private Expression ParsePrimary() + { + switch (TokenKindCur) + { + case TokenKind.INT: + { + var literal = new IntLiteral(locs, lexer.GetRaw(), TokenStart, TokenValue!); + NextToken(); + return literal; + } + + case TokenKind.FLOAT: + { + var literal = new FloatLiteral(locs, lexer.GetRaw(), TokenStart, (double)TokenValue!); + NextToken(); + return literal; + } + + case TokenKind.STRING: + return ParseStringLiteral(); + + case TokenKind.IDENTIFIER: + return ParseIdent(); + + case TokenKind.LBRACKET: + return ParseListMaker(); + + case TokenKind.LBRACE: + return ParseDictExpression(); + + case TokenKind.LPAREN: + { + int lparenOffset = NextToken(); + + if (TokenKindCur == TokenKind.RPAREN) + { + int rparen = NextToken(); + return new ListExpression(locs, true, lparenOffset, ImmutableArray.Empty, rparen); + } + + Expression e = ParseTest(); + + if (TokenKindCur == TokenKind.RPAREN) + { + NextToken(); + return e; + } + + if (TokenKindCur == TokenKind.COMMA) + { + var elems = ImmutableArray.CreateBuilder(); + elems.Add(e); + ParseExprList(elems, true); + int rparenOffset = Expect(TokenKind.RPAREN); + return new ListExpression(locs, true, lparenOffset, elems.ToImmutable(), rparenOffset); + } + + if (TokenKindCur == TokenKind.FOR) + { + SyntaxErrorMsg("Starlark does not support Python-style generator expressions"); + } + + Expect(TokenKind.RPAREN); + int end = SyncTo(EXPR_TERMINATOR_SET); + return MakeErrorExpression(lparenOffset, end); + } + + case TokenKind.MINUS: + case TokenKind.PLUS: + case TokenKind.TILDE: + { + TokenKind op = TokenKindCur; + int offset = NextToken(); + Expression x = ParsePrimaryWithSuffix(); + return new UnaryOperatorExpression(locs, op, offset, x); + } + + case TokenKind.CAST: + return ParseCastExpression(); + + case TokenKind.ISINSTANCE: + return ParseIsInstanceExpression(); + + case TokenKind.ELLIPSIS: + { + if (!insideTypeExpr) + { + SyntaxErrorMsg("ellipsis ('...') is not allowed outside type expressions"); + } + int offset = NextToken(); + return new Ellipsis(locs, offset); + } + + default: + { + int start = TokenStart; + SyntaxErrorMsg("expected expression"); + int end = SyncTo(EXPR_TERMINATOR_SET); + return MakeErrorExpression(start, end); + } + } + } + + private Expression ParsePrimaryWithSuffix() + { + Expression e = ParsePrimary(); + while (true) + { + if (TokenKindCur == TokenKind.DOT) + { + e = ParseSelectorSuffix(e); + } + else if (TokenKindCur == TokenKind.LBRACKET) + { + e = ParseSliceSuffix(e); + } + else if (TokenKindCur == TokenKind.LPAREN) + { + e = ParseCallSuffix(e); + } + else + { + return e; + } + } + } + + private Expression ParseSliceSuffix(Expression e) + { + int lbracketOffset = Expect(TokenKind.LBRACKET); + Expression? start = null; + Expression? end = null; + Expression? step = null; + + if (TokenKindCur != TokenKind.COLON) + { + start = ParseExpr(); + + if (TokenKindCur == TokenKind.RBRACKET) + { + int rbracketOffset = Expect(TokenKind.RBRACKET); + return new IndexExpression(locs, e, lbracketOffset, start, rbracketOffset); + } + } + + Expect(TokenKind.COLON); + if (TokenKindCur != TokenKind.COLON && TokenKindCur != TokenKind.RBRACKET) + { + end = ParseTest(); + } + if (TokenKindCur == TokenKind.COLON) + { + Expect(TokenKind.COLON); + if (TokenKindCur != TokenKind.RBRACKET) + { + step = ParseTest(); + } + } + int rbracketOffset2 = Expect(TokenKind.RBRACKET); + return new SliceExpression(locs, e, lbracketOffset, start, end, step, rbracketOffset2); + } + + private Expression ParseForLoopVariables() + { + Expression e1 = ParsePrimaryWithSuffix(); + if (TokenKindCur != TokenKind.COMMA) + { + return e1; + } + + var elems = ImmutableArray.CreateBuilder(); + elems.Add(e1); + while (TokenKindCur == TokenKind.COMMA) + { + Expect(TokenKind.COMMA); + if (EXPR_LIST_TERMINATOR_SET.Contains(TokenKindCur)) + { + break; + } + elems.Add(ParsePrimaryWithSuffix()); + } + return new ListExpression(locs, true, -1, elems.ToImmutable(), -1); + } + + private Expression ParseComprehensionSuffix(int loffset, Node body, TokenKind closingBracket) + { + var clauses = ImmutableArray.CreateBuilder(); + while (true) + { + if (TokenKindCur == TokenKind.FOR) + { + int forOffset = NextToken(); + Expression vars = ParseForLoopVariables(); + Expect(TokenKind.IN); + Expression seq = ParseTest(0); + clauses.Add(new Comprehension.For(locs, forOffset, vars, seq)); + } + else if (TokenKindCur == TokenKind.IF) + { + int ifOffset = NextToken(); + Expression cond = ParseTestNoCond(); + clauses.Add(new Comprehension.If(locs, ifOffset, cond)); + } + else if (TokenKindCur == closingBracket) + { + break; + } + else + { + SyntaxErrorMsg("expected '" + closingBracket.ToDisplayString() + "', 'for' or 'if'"); + int end = SyncPast(LIST_TERMINATOR_SET); + return MakeErrorExpression(loffset, end); + } + } + + bool isDict = closingBracket == TokenKind.RBRACE; + int roffset = Expect(closingBracket); + return new Comprehension(locs, isDict, loffset, body, clauses.ToImmutable(), roffset); + } + + private Expression ParseListMaker() + { + int lbracketOffset = Expect(TokenKind.LBRACKET); + if (TokenKindCur == TokenKind.RBRACKET) + { + int rbracketOffset = NextToken(); + return new ListExpression(locs, false, lbracketOffset, ImmutableArray.Empty, rbracketOffset); + } + + Expression expression = ParseTest(); + switch (TokenKindCur) + { + case TokenKind.RBRACKET: + { + int rbracketOffset = NextToken(); + return new ListExpression( + locs, false, lbracketOffset, ImmutableArray.Create(expression), rbracketOffset); + } + + case TokenKind.FOR: + return ParseComprehensionSuffix(lbracketOffset, expression, TokenKind.RBRACKET); + + case TokenKind.COMMA: + { + var elems = ImmutableArray.CreateBuilder(); + elems.Add(expression); + ParseExprList(elems, true); + if (TokenKindCur == TokenKind.RBRACKET) + { + int rbracketOffset = NextToken(); + return new ListExpression(locs, false, lbracketOffset, elems.ToImmutable(), rbracketOffset); + } + + Expect(TokenKind.RBRACKET); + int end = SyncPast(LIST_TERMINATOR_SET); + return MakeErrorExpression(lbracketOffset, end); + } + + default: + { + SyntaxErrorMsg("expected ',', 'for' or ']'"); + int end = SyncPast(LIST_TERMINATOR_SET); + return MakeErrorExpression(lbracketOffset, end); + } + } + } + + private Expression ParseDictExpression() + { + int lbraceOffset = Expect(TokenKind.LBRACE); + if (TokenKindCur == TokenKind.RBRACE) + { + int rbraceOffset = NextToken(); + return new DictExpression(locs, lbraceOffset, new List(), rbraceOffset); + } + + DictExpression.Entry entry = ParseDictEntry(); + if (TokenKindCur == TokenKind.FOR) + { + return ParseComprehensionSuffix(lbraceOffset, entry, TokenKind.RBRACE); + } + + var entries = new List { entry }; + if (TokenKindCur == TokenKind.COMMA) + { + Expect(TokenKind.COMMA); + entries.AddRange(ParseDictEntryList()); + } + if (TokenKindCur == TokenKind.RBRACE) + { + int rbraceOffset = NextToken(); + return new DictExpression(locs, lbraceOffset, entries, rbraceOffset); + } + + Expect(TokenKind.RBRACE); + int end = SyncPast(DICT_TERMINATOR_SET); + return MakeErrorExpression(lbraceOffset, end); + } + + private Identifier ParseIdent() + { + if (TokenKindCur != TokenKind.IDENTIFIER) + { + int start = TokenStart; + int end = Expect(TokenKind.IDENTIFIER); + return MakeErrorExpression(start, end); + } + + string name = (string)TokenValue!; + int offset = NextToken(); + return new Identifier(locs, name, offset); + } + + private Expression ParseBinOpExpression(int prec) + { + Expression x = ParseTest(prec + 1); + TokenKind? lastOp = null; + for (; ; ) + { + if (TokenKindCur == TokenKind.NOT) + { + Expect(TokenKind.NOT); + if (TokenKindCur != TokenKind.IN) + { + SyntaxErrorMsg("expected 'in'"); + } + lexer.Kind = TokenKind.NOT_IN; + } + + TokenKind op = TokenKindCur; + if (!operatorPrecedence[prec].Contains(op)) + { + return x; + } + + if (lastOp != null && operatorPrecedence[prec].Contains(TokenKind.EQUALS_EQUALS)) + { + ReportError( + TokenStart, + "Operator '{0}' is not associative with operator '{1}'. Use parens.", + lastOp.Value.ToDisplayString(), + op.ToDisplayString()); + } + + int opOffset = NextToken(); + Expression y = ParseTest(prec + 1); + x = OptimizeBinOpExpression(x, op, opOffset, y); + lastOp = op; + } + } + + private Expression OptimizeBinOpExpression(Expression x, TokenKind op, int opOffset, Expression y) + { + if (op == TokenKind.PLUS && x is StringLiteral sx && y is StringLiteral sy) + { + return new StringLiteral( + locs, + x.GetStartOffset(), + Intern(sx.GetValue() + sy.GetValue()), + y.GetEndOffset()); + } + return new BinaryOperatorExpression(locs, x, op, opOffset, y); + } + + private bool CheckAllowTypeSyntax(int offset, TokenKind tokenKind, object? tokenValue) + { + if (options.AllowTypeSyntax) + { + return true; + } + SyntaxErrorAt(offset, tokenKind, tokenValue, "type annotations are disallowed"); + return false; + } + + private Expression? MaybeParseTypeAnnotationAfter(TokenKind expectedToken) + { + if (TokenKindCur == expectedToken && CheckAllowTypeSyntax(TokenStart, TokenKindCur, TokenValue)) + { + NextToken(); + return ParseTypeExprWithFallback(); + } + return null; + } + + private Expression ParseTypeExprWithFallback() + { + Expression result; + this.insideTypeExpr = true; + if (options.TolerateInvalidTypeExpressions) + { + result = ParseTest(); + } + else + { + result = ParseTypeExpr(); + } + this.insideTypeExpr = false; + return result; + } + + private Expression ParseTypeExpr() + { + if (TokenKindCur != TokenKind.IDENTIFIER) + { + int start = TokenStart; + SyntaxErrorMsg("expected a type"); + int end = SyncTo(EXPR_TERMINATOR_SET); + return MakeErrorExpression(start, end); + } + Identifier typeOrConstructor = ParseIdent(); + Expression expr; + if (TokenKindCur == TokenKind.LBRACKET) + { + expr = ParseTypeApplication(typeOrConstructor); + } + else + { + expr = typeOrConstructor; + } + while (TokenKindCur == TokenKind.PIPE) + { + int opOffset = NextToken(); + Identifier secondTypeOrConstructor = ParseIdent(); + Expression y; + if (TokenKindCur == TokenKind.LBRACKET) + { + y = ParseTypeApplication(secondTypeOrConstructor); + } + else + { + y = secondTypeOrConstructor; + } + expr = new BinaryOperatorExpression(locs, expr, TokenKind.PIPE, opOffset, y); + } + return expr; + } + + private Expression ParseTypeArgument() + { + switch (TokenKindCur) + { + case TokenKind.LBRACKET: + return ParseTypeList(); + case TokenKind.LBRACE: + return ParseTypeDict(); + case TokenKind.LPAREN: + { + int lparenOffset = Expect(TokenKind.LPAREN); + int rparenOffset = Expect(TokenKind.RPAREN); + return new ListExpression(locs, true, lparenOffset, ImmutableArray.Empty, rparenOffset); + } + case TokenKind.STRING: + return ParseStringLiteral(); + case TokenKind.ELLIPSIS: + return ParsePrimary(); + default: + break; + } + if (TokenKindCur != TokenKind.IDENTIFIER) + { + int start = TokenStart; + SyntaxErrorMsg("expected a type argument"); + int end = SyncTo(EXPR_TERMINATOR_SET); + return MakeErrorExpression(start, end); + } + return ParseTypeExpr(); + } + + private Expression ParseTypeList() + { + int lbracketOffset = Expect(TokenKind.LBRACKET); + var elems = ImmutableArray.CreateBuilder(); + if (TokenKindCur != TokenKind.RBRACKET) + { + elems.Add(ParseTypeArgument()); + } + while (TokenKindCur != TokenKind.RBRACKET && TokenKindCur != TokenKind.EOF) + { + Expect(TokenKind.COMMA); + if (TokenKindCur == TokenKind.RBRACKET) + { + break; + } + elems.Add(ParseTypeArgument()); + } + int rbracketOffset = NextToken(); + return new ListExpression(locs, false, lbracketOffset, elems.ToImmutable(), rbracketOffset); + } + + private DictExpression.Entry ParseTypeDictEntry() + { + Expression key = ParseStringLiteral(); + int colonOffset = Expect(TokenKind.COLON); + Expression value = ParseTypeArgument(); + return new DictExpression.Entry(locs, key, colonOffset, value); + } + + private Expression ParseTypeDict() + { + int lbraceOffset = Expect(TokenKind.LBRACE); + + var entries = new List(); + if (TokenKindCur != TokenKind.RBRACE) + { + entries.Add(ParseTypeDictEntry()); + } + while (TokenKindCur != TokenKind.RBRACE && TokenKindCur != TokenKind.EOF) + { + Expect(TokenKind.COMMA); + if (TokenKindCur == TokenKind.RBRACE) + { + break; + } + entries.Add(ParseTypeDictEntry()); + } + + int rbraceOffset = NextToken(); + return new DictExpression(locs, lbraceOffset, entries, rbraceOffset); + } + + private Expression ParseTypeApplication(Identifier constructor) + { + Expect(TokenKind.LBRACKET); + var args = ImmutableArray.CreateBuilder(); + args.Add(ParseTypeArgument()); + while (TokenKindCur != TokenKind.RBRACKET && TokenKindCur != TokenKind.EOF) + { + Expect(TokenKind.COMMA); + args.Add(ParseTypeArgument()); + } + int rbracketOffset = Expect(TokenKind.RBRACKET); + return new TypeApplication(locs, constructor, args.ToImmutable(), rbracketOffset); + } + + private static bool IsTypeSoftKeyword(Node node) + { + return node is Identifier id && id.GetName() == TYPE_SOFT_KEYWORD; + } + + private Statement ParseTypeAliasStatementTail(Node typeSoftKeywordNode) + { + int startOffset = typeSoftKeywordNode.GetStartOffset(); + CheckAllowTypeSyntax(startOffset, TokenKind.IDENTIFIER, TYPE_SOFT_KEYWORD); + Identifier identifier = ParseIdent(); + ImmutableArray parameters = ParseOptionalTypeParameters(); + Expect(TokenKind.EQUALS); + Expression definition = ParseTypeExprWithFallback(); + return new TypeAliasStatement(locs, startOffset, identifier, parameters, definition); + } + + private ImmutableArray ParseOptionalTypeParameters() + { + if (TokenKindCur == TokenKind.LBRACKET) + { + CheckAllowTypeSyntax(TokenStart, TokenKindCur, TokenValue); + NextToken(); + var parameters = ImmutableArray.CreateBuilder(); + var uniqueParameterNames = new HashSet(); + parameters.Add(ParseTypeParameter(uniqueParameterNames)); + while (TokenKindCur != TokenKind.RBRACKET && TokenKindCur != TokenKind.EOF) + { + Expect(TokenKind.COMMA); + if (TokenKindCur == TokenKind.RBRACKET) + { + break; + } + parameters.Add(ParseTypeParameter(uniqueParameterNames)); + } + Expect(TokenKind.RBRACKET); + return parameters.ToImmutable(); + } + return ImmutableArray.Empty; + } + + private Identifier ParseTypeParameter(HashSet uniqueParameterNames) + { + int tokenStart = TokenStart; + TokenKind tokenKind = TokenKindCur; + object? tokenValue = TokenValue; + Identifier ident = ParseIdent(); + if (Identifier.IsValid(ident.GetName()) && !uniqueParameterNames.Add(ident.GetName())) + { + SyntaxErrorAt(tokenStart, tokenKind, tokenValue, "duplicate type parameter"); + } + return ident; + } + + private Expression ParseTest() + { + int start = TokenStart; + if (TokenKindCur == TokenKind.LAMBDA) + { + return ParseLambda(true); + } + + Expression expr = ParseTest(0); + if (TokenKindCur == TokenKind.IF) + { + NextToken(); + Expression condition = ParseTest(0); + if (TokenKindCur == TokenKind.ELSE) + { + NextToken(); + Expression elseClause = ParseTest(); + return new ConditionalExpression(locs, expr, condition, elseClause); + } + else + { + ReportError(start, "missing else clause in conditional expression or semicolon before if"); + return expr; + } + } + return expr; + } + + private Expression ParseTest(int prec) + { + if (prec >= operatorPrecedence.Count) + { + return ParsePrimaryWithSuffix(); + } + if (TokenKindCur == TokenKind.NOT && operatorPrecedence[prec].Contains(TokenKind.NOT)) + { + return ParseNotExpression(prec); + } + return ParseBinOpExpression(prec); + } + + private LambdaExpression ParseLambda(bool allowCond) + { + int lambdaOffset = Expect(TokenKind.LAMBDA); + ImmutableArray parameters = ParseParameters(false); + Expect(TokenKind.COLON); + Expression body = allowCond ? ParseTest() : ParseTestNoCond(); + return new LambdaExpression(locs, lambdaOffset, parameters, body); + } + + private Expression ParseTestNoCond() + { + if (TokenKindCur == TokenKind.LAMBDA) + { + return ParseLambda(false); + } + return ParseTest(0); + } + + private Expression ParseNotExpression(int prec) + { + int notOffset = Expect(TokenKind.NOT); + Expression x = ParseTest(prec); + return new UnaryOperatorExpression(locs, TokenKind.NOT, notOffset, x); + } + + private ImmutableArray ParseFileInput() + { + var list = ImmutableArray.CreateBuilder(); + while (TokenKindCur != TokenKind.EOF) + { + if (TokenKindCur == TokenKind.NEWLINE) + { + ExpectAndRecover(TokenKind.NEWLINE); + } + else if (recoveryMode) + { + SyncTo(STATEMENT_TERMINATOR_SET); + recoveryMode = false; + } + else + { + MaybeParseDocCommentBlock(); + if (TokenKindCur == TokenKind.EOF) + { + break; + } + ParseStatement(list); + } + } + return list.ToImmutable(); + } + + private Statement ParseLoadStatement() + { + int loadOffset = Expect(TokenKind.LOAD); + Expect(TokenKind.LPAREN); + if (TokenKindCur != TokenKind.STRING) + { + var module0 = new StringLiteral(locs, TokenStart, "", TokenEnd); + Expect(TokenKind.STRING); + return new LoadStatement(locs, loadOffset, module0, ImmutableArray.Empty, TokenEnd); + } + + StringLiteral module = ParseStringLiteral(); + if (TokenKindCur == TokenKind.RPAREN) + { + SyntaxErrorMsg("expected at least one symbol to load"); + return new LoadStatement(locs, loadOffset, module, ImmutableArray.Empty, TokenEnd); + } + Expect(TokenKind.COMMA); + + var bindings = ImmutableArray.CreateBuilder(); + ParseLoadSymbol(bindings); + while (TokenKindCur != TokenKind.RPAREN && TokenKindCur != TokenKind.EOF) + { + Expect(TokenKind.COMMA); + if (TokenKindCur == TokenKind.RPAREN) + { + break; + } + ParseLoadSymbol(bindings); + } + + int rparen = Expect(TokenKind.RPAREN); + return new LoadStatement(locs, loadOffset, module, bindings.ToImmutable(), rparen); + } + + private void ParseLoadSymbol(ImmutableArray.Builder symbols) + { + if (TokenKindCur != TokenKind.STRING && TokenKindCur != TokenKind.IDENTIFIER) + { + SyntaxErrorMsg("expected either a literal string or an identifier"); + return; + } + + string name = (string)TokenValue!; + int nameOffset = TokenStart + (TokenKindCur == TokenKind.STRING ? 1 : 0); + Identifier local = new Identifier(locs, name, nameOffset); + + Identifier original; + if (TokenKindCur == TokenKind.STRING) + { + original = local; + } + else + { + Expect(TokenKind.IDENTIFIER); + Expect(TokenKind.EQUALS); + if (TokenKindCur != TokenKind.STRING) + { + SyntaxErrorMsg("expected string"); + return; + } + original = new Identifier(locs, (string)TokenValue!, TokenStart + 1); + } + NextToken(); + symbols.Add(new LoadStatement.Binding(local, original)); + } + + private void ParseSimpleStatement(ImmutableArray.Builder list) + { + list.Add(ParseSmallStatement()); + mostRecentDocCommentBlock = null; + + while (TokenKindCur == TokenKind.SEMI) + { + NextToken(); + if (TokenKindCur == TokenKind.NEWLINE || TokenKindCur == TokenKind.DOC_COMMENT_TRAILING) + { + break; + } + list.Add(ParseSmallStatement()); + } + if (TokenKindCur == TokenKind.DOC_COMMENT_TRAILING) + { + NextToken(); + } + ExpectAndRecover(TokenKind.NEWLINE); + } + + private DocComments? MaybeParseTrailingDocComment(Location statementStart) + { + DocComments? result; + if (TokenKindCur == TokenKind.DOC_COMMENT_TRAILING) + { + result = (DocComments)TokenValue!; + NextToken(); + } + else + { + result = GetDocCommentBlockOnPreviousLine(statementStart.Line); + } + return result; + } + + private Statement ParseSmallStatement() + { + if (TokenKindCur == TokenKind.RETURN) + { + return ParseReturnStatement(); + } + + if (TokenKindCur == TokenKind.BREAK + || TokenKindCur == TokenKind.CONTINUE + || TokenKindCur == TokenKind.PASS) + { + TokenKind kind = TokenKindCur; + int offset = NextToken(); + return new FlowStatement(locs, kind, offset); + } + + if (TokenKindCur == TokenKind.LOAD) + { + return ParseLoadStatement(); + } + + Expression lhs = ParseExpr(); + + if (TokenKindCur == TokenKind.IDENTIFIER && IsTypeSoftKeyword(lhs)) + { + return ParseTypeAliasStatementTail(lhs); + } + + int colonOffset = TokenStart; + Expression? type = MaybeParseTypeAnnotationAfter(TokenKind.COLON); + + TokenKind? op = augmentedAssignments.TryGetValue(TokenKindCur, out TokenKind aug) ? aug : null; + if (TokenKindCur == TokenKind.EQUALS || op != null) + { + int opOffset = NextToken(); + Expression rhs = ParseExpr(); + DocComments? docComments = MaybeParseTrailingDocComment(lhs.GetStartLocation()); + if (type != null) + { + if (lhs is not Identifier) + { + SyntaxErrorAt( + colonOffset, + TokenKind.COLON, + null, + "type annotations must have a single identifier on the left-hand side"); + type = null; + } + if (op != null) + { + SyntaxErrorAt( + colonOffset, + TokenKind.COLON, + null, + "type annotations not allowed on augmented assignment statements"); + type = null; + } + } + return new AssignmentStatement(locs, lhs, type, op, opOffset, rhs, docComments); + } + else if (type != null) + { + DocComments? docComments = MaybeParseTrailingDocComment(lhs.GetStartLocation()); + if (lhs is not Identifier id) + { + SyntaxErrorAt( + colonOffset, + TokenKind.COLON, + null, + "type annotations must have a single identifier on the left-hand side"); + return new ExpressionStatement( + locs, MakeErrorExpression(lhs.GetStartOffset(), type.GetEndOffset())); + } + return new VarStatement(locs, id, type, docComments); + } + else + { + return new ExpressionStatement(locs, lhs); + } + } + + private IfStatement ParseIfStatement() + { + int ifOffset = Expect(TokenKind.IF); + Expression cond = ParseTest(); + Expect(TokenKind.COLON); + ImmutableArray body = ParseSuite(); + IfStatement ifStmt = new IfStatement(locs, TokenKind.IF, ifOffset, cond, body); + IfStatement tail = ifStmt; + while (TokenKindCur == TokenKind.ELIF) + { + int elifOffset = Expect(TokenKind.ELIF); + cond = ParseTest(); + Expect(TokenKind.COLON); + body = ParseSuite(); + IfStatement elif = new IfStatement(locs, TokenKind.ELIF, elifOffset, cond, body); + tail.SetElseBlock(ImmutableArray.Create(elif)); + tail = elif; + } + if (TokenKindCur == TokenKind.ELSE) + { + Expect(TokenKind.ELSE); + Expect(TokenKind.COLON); + body = ParseSuite(); + tail.SetElseBlock(body); + } + return ifStmt; + } + + private ForStatement ParseForStatement() + { + int forOffset = Expect(TokenKind.FOR); + Expression vars = ParseForLoopVariables(); + Expect(TokenKind.IN); + Expression collection = ParseExpr(); + Expect(TokenKind.COLON); + ImmutableArray body = ParseSuite(); + return new ForStatement(locs, forOffset, vars, collection, body); + } + + private DefStatement ParseDefStatement() + { + int defOffset = Expect(TokenKind.DEF); + Identifier ident = ParseIdent(); + ImmutableArray typeParams = ParseOptionalTypeParameters(); + Expect(TokenKind.LPAREN); + ImmutableArray parameters = ParseParameters(true); + Expect(TokenKind.RPAREN); + Expression? returnType = MaybeParseTypeAnnotationAfter(TokenKind.RARROW); + Expect(TokenKind.COLON); + ImmutableArray block = ParseSuite(); + return new DefStatement(locs, defOffset, ident, typeParams, parameters, returnType, block); + } + + private ImmutableArray ParseParameters(bool defStatement) + { + bool hasParam = false; + var list = ImmutableArray.CreateBuilder(); + + while (TokenKindCur != TokenKind.RPAREN + && TokenKindCur != TokenKind.COLON + && TokenKindCur != TokenKind.EOF) + { + if (hasParam) + { + Expect(TokenKind.COMMA); + if (TokenKindCur == TokenKind.RPAREN) + { + break; + } + } + Parameter param = ParseParameter(defStatement); + hasParam = true; + list.Add(param); + } + return list.ToImmutable(); + } + + private ImmutableArray ParseSuite() + { + var list = ImmutableArray.CreateBuilder(); + if (TokenKindCur == TokenKind.DOC_COMMENT_TRAILING) + { + NextToken(); + } + if (TokenKindCur == TokenKind.NEWLINE) + { + Expect(TokenKind.NEWLINE); + MaybeParseDocCommentBlock(); + if (TokenKindCur != TokenKind.INDENT) + { + ReportError(TokenStart, "expected an indented block"); + return list.ToImmutable(); + } + Expect(TokenKind.INDENT); + while (TokenKindCur != TokenKind.OUTDENT && TokenKindCur != TokenKind.EOF) + { + ParseStatement(list); + MaybeParseDocCommentBlock(); + } + ExpectAndRecover(TokenKind.OUTDENT); + } + else + { + ParseSimpleStatement(list); + } + return list.ToImmutable(); + } + + private ReturnStatement ParseReturnStatement() + { + int returnOffset = Expect(TokenKind.RETURN); + + Expression? result = null; + if (!STATEMENT_TERMINATOR_SET.Contains(TokenKindCur)) + { + result = ParseExpr(); + } + return new ReturnStatement(locs, returnOffset, result); + } +} diff --git a/src/Starlark/Syntax/Resolver.cs b/src/Starlark/Syntax/Resolver.cs new file mode 100644 index 000000000..b984e95c5 --- /dev/null +++ b/src/Starlark/Syntax/Resolver.cs @@ -0,0 +1,1405 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Linq; +using Starlark.Spelling; + +namespace Starlark.Syntax; + +/// +/// The Resolver resolves each identifier in a syntax tree to its binding, and performs other +/// validity checks. +/// +/// When a variable is defined, it is visible in the entire block. For example, a global +/// variable is visible in the entire file; a variable in a function is visible in the entire +/// function block (even on the lines before its first assignment). +/// +/// Resolution is a mutation of the syntax tree, as it attaches binding information to +/// Identifier nodes. Resolution errors are reported in the analogous manner to scan/parse errors: +/// for a StarlarkFile, they are appended to StarlarkFile.errors; for an expression they are +/// reported by a SyntaxError.Exception exception. +/// +public sealed class Resolver : NodeVisitor +{ + /// Scope discriminates the scope of a binding: global, local, etc. + public enum Scope + { + /// Binding is local to a function, comprehension, or file (e.g. load). + LOCAL, + + /// Binding is non-local and occurs outside any function or comprehension. + GLOBAL, + + /// Binding is local to a function, comprehension, or file, but shared with nested functions. + CELL, + + /// Binding is an implicit parameter whose value is the CELL of some enclosing function. + FREE, + + /// Binding is predeclared by the application (e.g. glob in Bazel). + PREDECLARED, + + /// Binding is predeclared by the core (e.g. None). + UNIVERSAL, + } + + /// Helper for the lowercase toString behavior of . + public static string ScopeToString(Scope scope) + { + return scope.ToString().ToLowerInvariant(); + } + + /// + /// A Binding is a static abstraction of a variable. The Resolver maps each Identifier to a + /// Binding. + /// + public class Binding + { + internal Scope scope; + + // index within frame (LOCAL/CELL), freevars (FREE), or module (GLOBAL) + private readonly int index; + + // True if there is at least one binding occurrence in the resolved syntax tree; false for + // bindings that are generated by successfully matching a name in Module#resolve. + internal readonly bool isSyntactic; + + // If isSyntactic is true, the first binding occurrence of this symbol. Otherwise, the first + // occurrence of this symbol (which must be non-binding). + internal readonly Identifier first; + + // Set by TypeTagger (possibly more than once) if applicable. Null is treated as untyped/Any. + private StarlarkType? type; + + internal Binding(Scope scope, int index, bool isSyntactic, Identifier first) + { + this.scope = scope; + this.index = index; + this.isSyntactic = isSyntactic; + this.first = first; + } + + /// + /// Returns the first binding occurrence of this symbol, or if there is none, the first + /// non-binding occurrence. + /// + internal Identifier GetFirst() => first; + + /// Returns whether any binding occurrence of this symbol appears in the syntax tree. + internal bool IsSyntactic() => isSyntactic; + + /// Returns the name of this binding's identifier. + public string GetName() => first.GetName(); + + /// Returns the scope of the binding. + public Scope GetScope() => scope; + + /// + /// Returns the index of a binding within its function's frame (LOCAL/CELL), freevars (FREE), + /// or module (GLOBAL). + /// + public int GetIndex() => index; + + public override string ToString() + { + string declaredAt = + isSyntactic + ? first.GetStartLocation().ToString()! + : scope == Scope.UNIVERSAL || scope == Scope.PREDECLARED + ? "" + // e.g., on a previous input of the REPL + : ""; + return string.Format( + "{0}[{1}] {2} @ {3}", ScopeToString(scope), index, first.GetName(), declaredAt); + } + + /// Returns the type of this binding, or null if a type is not set. + public StarlarkType? GetType() => type; + + /// Assigns (or clears) a type to this binding. May be called more than once. + internal void SetType(StarlarkType? type) + { + this.type = type; + } + } + + /// A for a variable of a list or dict comprehension. + public sealed class ComprehensionBinding : Binding + { + // Used only for determining the range of locations encompassing the comprehension's lexical + // scope. + private readonly Comprehension node; + + internal ComprehensionBinding(int index, Identifier first, Comprehension node) + : base(Scope.LOCAL, index, /* isSyntactic= */ true, first) + { + this.node = node; + } + + /// Returns true if the given location falls within the scope of the comprehension. + public bool InScope(Location loc) + { + if (!loc.File.Equals(node.GetStartLocation().File)) + { + return false; + } + // Following Python3, the first for clause of a comprehension is resolved outside the + // comprehension block. All the other loops are resolved in the scope of their own + // bindings, permitting forward references. + var for0 = (Comprehension.For)node.GetClauses()[0]; + Expression iterable0 = for0.GetIterable(); + if (loc.CompareTo(iterable0.GetStartLocation()) >= 0 + && loc.CompareTo(iterable0.GetEndLocation()) < 0) + { + return false; + } + if (loc.CompareTo(node.GetStartLocation()) >= 0 + && loc.CompareTo(node.GetEndLocation()) < 0) + { + return true; + } + return false; + } + } + + /// A Resolver.Function records information about a resolved function. + public sealed class Function + { + private readonly string name; + private readonly Location location; + private readonly ImmutableArray parameters; + private readonly ImmutableArray body; + private readonly bool hasVarargs; + private readonly bool hasKwargs; + private readonly int numKeywordOnlyParams; + private readonly ImmutableArray parameterNames; + private readonly bool isToplevel; + private readonly ImmutableArray locals; + private readonly int[] cellIndices; + private readonly ImmutableArray freevars; + private readonly ImmutableArray globals; + + // Set by type checking (possibly more than once) if applicable. Null is treated as + // untyped / Any. Always null for the function associated with a StarlarkFile object. + private Types.CallableType? functionType; + + // Set by type checking if applicable. + private bool usesTypeSyntax; + + internal Function( + string name, + Location loc, + ImmutableArray parameters, + ImmutableArray body, + bool hasVarargs, + bool hasKwargs, + int numKeywordOnlyParams, + IReadOnlyList locals, + IReadOnlyList freevars, + IReadOnlyList globals) + { + this.name = name; + this.location = loc; + this.parameters = parameters; + this.body = body; + this.hasVarargs = hasVarargs; + this.hasKwargs = hasKwargs; + this.numKeywordOnlyParams = numKeywordOnlyParams; + + var names = ImmutableArray.CreateBuilder(parameters.Length); + foreach (Parameter p in parameters) + { + names.Add(p.GetName()!); + } + this.parameterNames = names.ToImmutable(); + + this.isToplevel = name.Equals("", StringComparison.Ordinal); + this.locals = locals.ToImmutableArray(); + this.freevars = freevars.ToImmutableArray(); + this.globals = globals.ToImmutableArray(); + + // Create an index of the locals that are cells. + int ncells = 0; + int nlocals = this.locals.Length; + for (int i = 0; i < nlocals; i++) + { + if (this.locals[i].scope == Scope.CELL) + { + ncells++; + } + } + this.cellIndices = new int[ncells]; + for (int i = 0, j = 0; i < nlocals; i++) + { + if (this.locals[i].scope == Scope.CELL) + { + cellIndices[j++] = i; + } + } + } + + /// + /// Returns the name of the function. It may be "<toplevel>" for the implicit function + /// that holds the top-level statements of a file, or "<expr>" for the implicit + /// function that evaluates a single expression. + /// + public string GetName() => name; + + /// Returns the value denoted by the function's doc string literal, or null if absent. + public string? GetDocumentation() + { + if (GetBody().Count == 0) + { + return null; + } + Statement first = GetBody()[0]; + if (first is not ExpressionStatement exprStmt) + { + return null; + } + Expression expr = exprStmt.GetExpression(); + if (expr is not StringLiteral strLit) + { + return null; + } + return strLit.GetValue(); + } + + /// Returns the function's local bindings, parameters first. + public IReadOnlyList GetLocals() => locals; + + /// + /// Returns the indices within of the "cells", that is, local + /// variables of this function that are shared with nested functions. The caller must not + /// modify the result. + /// + public int[] GetCellIndices() => cellIndices; + + /// + /// Returns the list of names of globals referenced by this function. The order matches the + /// indices used in compiled code. + /// + public IReadOnlyList GetGlobals() => globals; + + /// + /// Returns the list of enclosing CELL or FREE bindings referenced by this function. + /// + public IReadOnlyList GetFreeVars() => freevars; + + /// Returns the location of the function's identifier. + public Location GetLocation() => location; + + /// + /// Returns the function's parameters, in "run-time order": non-keyword-only parameters, + /// keyword-only parameters, *args, and finally **kwargs. A bare * + /// parameter is dropped. + /// + public IReadOnlyList GetParameters() => parameters; + + /// Returns the effective statements of the function's body. + public IReadOnlyList GetBody() => body; + + /// Reports whether the function has an *args parameter. + public bool HasVarargs() => hasVarargs; + + /// Reports whether the function has a **kwargs parameter. + public bool HasKwargs() => hasKwargs; + + /// Returns the number of the function's keyword-only parameters. + public int NumKeywordOnlyParams() => numKeywordOnlyParams; + + /// Returns the number of non-residual parameters. + public int GetNumNonResidualParameters() + { + return parameters.Length - (hasKwargs ? 1 : 0) - (hasVarargs ? 1 : 0); + } + + /// Returns the number of ordinary (non-residual, non-keyword-only) parameters. + public int GetNumOrdinaryParameters() + { + return parameters.Length + - (hasKwargs ? 1 : 0) + - (hasVarargs ? 1 : 0) + - numKeywordOnlyParams; + } + + /// Returns the names of the parameters. Order is as for . + public IReadOnlyList GetParameterNames() => parameterNames; + + /// + /// isToplevel indicates that this is the <toplevel> function containing top-level + /// statements of a file. + /// + public bool IsToplevel() => isToplevel; + + /// Returns the type of this function, or null if a type is not set. + public Types.CallableType? GetFunctionType() => functionType; + + /// Assigns (or clears) a type to this function. May be called more than once. + internal void SetFunctionType(Types.CallableType? functionType) + { + this.functionType = functionType; + } + + /// + /// After type tagging has been performed, returns true if the non-lambda function is + /// considered to use static typing syntax - in other words, type annotations or cast + /// expressions. + /// + public bool UsesTypeSyntax() => usesTypeSyntax; + + /// Tags this function as using type syntax. + internal void SetUsesTypeSyntax() + { + this.usesTypeSyntax = true; + } + } + + /// + /// A static abstraction of a Starlark Module, that resolves names to scope and type information + /// rather than to dynamic Starlark values. + /// + public interface IModule : TypeContext + { + /// + /// Resolves a name to a GLOBAL, PREDECLARED, or UNIVERSAL binding. + /// + /// if the name is not defined. + Scope Resolve(string name); + + /// + /// Resolves a name to a corresponding type constructor. + /// + /// null if the name is known but not a type constructor. + /// if the name is not defined, as per . + TypeConstructor? GetTypeConstructor(string name); + } + + /// + /// An Undefined exception indicates a failure to resolve a top-level name. If Candidates + /// is non-null, it provides the set of accessible top-level names, which, along with local + /// names, will be used as candidates for spelling suggestions. + /// + public sealed class Undefined : Exception + { + public ISet? Candidates { get; } + + public Undefined(string message, ISet? candidates) + : base(message) + { + this.Candidates = candidates; + } + + public Undefined(string message) + : this(message, /* candidates= */ null) + { + } + } + + /// Represents a lexical block. + private sealed class Block + { + internal readonly Block? parent; // enclosing block, or null for tail of list + internal Node? syntax; // Comprehension, DefStatement/LambdaExpression, StarlarkFile, or null + internal readonly List frame; // accumulated locals of enclosing function + + // Accumulated CELL/FREE bindings of the enclosing function that will provide the values for + // the free variables of this function; null for toplevel functions and expressions. + internal readonly List? freevars; + + // Bindings for names defined in this block. Also, as an optimization, memoized lookups of + // enclosing bindings. + internal readonly Dictionary bindings = new(); + + internal Block(Block? parent, Node? syntax, List frame, List? freevars) + { + this.parent = parent; + this.syntax = syntax; + this.frame = frame; + this.freevars = freevars; + } + } + + private readonly List errors; + private readonly FileOptions options; + private readonly IModule module; + + // List whose order defines the numbering of global variables in this program. + private readonly List globals = new(); + + // A map from global variable names to their doc comments; added to by bind(); null if doc + // comments for global variables are not being collected. + private readonly Dictionary? docCommentsMap; + + // A cache of PREDECLARED, UNIVERSAL, and GLOBAL bindings queried from the module. + private readonly Dictionary toplevel = new(); + + // Linked list of blocks, innermost first, for functions and comprehensions and (finally) file. + private Block locals = null!; + private int loopCount; + + private Resolver( + List errors, + IModule module, + FileOptions options, + Dictionary? docCommentsMap) + { + // Don't visit keyword args or dot expression fields -- those aren't symbols. + this.skipNonSymbolIdentifiers = true; + + this.errors = errors; + this.module = module; + this.options = options; + this.docCommentsMap = docCommentsMap; + } + + // Formats and reports an error at the start of the specified node. + private void Errorf(Node node, string format, params object?[] args) + { + Errorf(node.GetStartLocation(), format, args); + } + + // Formats and reports an error at the specified location. + private void Errorf(Location loc, string format, params object?[] args) + { + errors.Add(new SyntaxError(loc, string.Format(format, args))); + } + + /// + /// First pass: add bindings for all variables to the current block. This is done because symbols + /// are sometimes used before their definition point. + /// + private void CreateBindingsForBlock(IEnumerable stmts) + { + foreach (Statement stmt in stmts) + { + CreateBindings(stmt); + } + } + + private void CreateBindings(Statement stmt) + { + switch (stmt.Kind) + { + case Statement.StatementKind.ASSIGNMENT: + { + var assignStmt = (AssignmentStatement)stmt; + CreateBindingsForLHS(assignStmt.GetLHS(), assignStmt.GetDocComments()); + break; + } + + case Statement.StatementKind.VAR: + { + var varStmt = (VarStatement)stmt; + Bind(varStmt.GetIdentifier(), /* isLoad= */ false, varStmt.GetDocComments()); + break; + } + + case Statement.StatementKind.IF: + { + var ifStmt = (IfStatement)stmt; + CreateBindingsForBlock(ifStmt.GetThenBlock()); + if (ifStmt.GetElseBlock() != null) + { + CreateBindingsForBlock(ifStmt.GetElseBlock()!); + } + break; + } + + case Statement.StatementKind.FOR: + { + var forStmt = (ForStatement)stmt; + CreateBindingsForLHS(forStmt.GetVars(), /* docComments= */ null); + CreateBindingsForBlock(forStmt.GetBody()); + break; + } + + case Statement.StatementKind.DEF: + { + var defStmt = (DefStatement)stmt; + Bind(defStmt.GetIdentifier(), /* isLoad= */ false, /* docComments= */ null); + break; + } + + case Statement.StatementKind.LOAD: + { + var load = (LoadStatement)stmt; + var names = new HashSet(); + foreach (LoadStatement.Binding b in load.GetBindings()) + { + // Reject load('...', '_private'). + Identifier orig = b.GetOriginalName(); + if (orig.IsPrivate() && !options.AllowLoadPrivateSymbols) + { + Errorf(orig, "symbol '{0}' is private and cannot be imported", orig.GetName()); + } + + // A load statement may not bind a single name more than once, even if + // options.allowToplevelRebinding. + Identifier local = b.GetLocalName(); + if (names.Add(local.GetName())) + { + Bind(local, /* isLoad= */ true, /* docComments= */ null); + } + else + { + Errorf(local, "load statement defines '{0}' more than once", local.GetName()); + } + } + break; + } + + case Statement.StatementKind.TYPE_ALIAS: + { + if (options.ResolveTypeSyntax) + { + var typeStmt = (TypeAliasStatement)stmt; + Bind(typeStmt.GetIdentifier(), /* isLoad= */ false, /* docComments= */ null); + } + break; + } + + // nothing to declare + case Statement.StatementKind.EXPRESSION: + case Statement.StatementKind.FLOW: + case Statement.StatementKind.RETURN: + break; + } + } + + /// Calls for appropriate identifiers of the LHS of an assignment. + private void CreateBindingsForLHS(Expression lhs, DocComments? docComments) + { + foreach (Identifier id in Identifier.BoundIdentifiers(lhs)) + { + Bind(id, /* isLoad= */ false, docComments); + } + } + + /// + /// Extends the visit() traversal to the lhs of an assignment or for loop. + /// + private void Assign(Expression lhs) + { + if (lhs is Identifier id) + { + // Bindings are created by the first pass (createBindings). + AssertIsBound(id); + } + else if (lhs is IndexExpression) + { + Visit(lhs); + } + else if (lhs is ListExpression listExpr) + { + foreach (Expression elem in listExpr.GetElements()) + { + Assign(elem); + } + } + else if (lhs is DotExpression dotExpr) + { + Visit(dotExpr.GetObject()); + // Do not visit the field. It is an identifier but does not correspond to a binding. + } + else + { + Errorf(lhs, "cannot assign to '{0}'", lhs); + } + } + + private void AssertIsBound(Identifier id) + { + if (id.GetBinding() == null) + { + throw new InvalidOperationException( + string.Format("{0} expected to be bound", id.GetName())); + } + } + + private void AssertIsNotBound(Identifier id) + { + if (id.GetBinding() != null) + { + throw new InvalidOperationException( + string.Format("{0} expected to not be bound", id.GetBinding())); + } + } + + public override void Visit(Identifier id) + { + // The visit() traversal should not reach any Identifier node more than once. It also should + // not reach any binding occurrence of an Identifier at all -- those are set by the 1st pass. + AssertIsNotBound(id); + Binding? bind = Use(id); + if (bind != null) + { + id.SetBinding(bind); + return; + } + } + + public override void Visit(ReturnStatement node) + { + if (locals.syntax is StarlarkFile) + { + Errorf(node, "return statements must be inside a function"); + } + base.Visit(node); + } + + public override void Visit(CallExpression node) + { + // validate call arguments + bool seenVarargs = false; + bool seenKwargs = false; + HashSet? keywords = null; + foreach (Argument arg in node.GetArguments()) + { + if (arg is Argument.Positional) + { + if (seenVarargs) + { + Errorf(arg, "positional argument may not follow *args"); + } + else if (seenKwargs) + { + Errorf(arg, "positional argument may not follow **kwargs"); + } + else if (keywords != null) + { + Errorf(arg, "positional argument may not follow keyword argument"); + } + } + else if (arg is Argument.Keyword) + { + string keyword = arg.GetName()!; + if (seenVarargs) + { + Errorf(arg, "keyword argument {0} may not follow *args", keyword); + } + else if (seenKwargs) + { + Errorf(arg, "keyword argument {0} may not follow **kwargs", keyword); + } + keywords ??= new HashSet(); + if (!keywords.Add(keyword)) + { + Errorf(arg, "duplicate keyword argument: {0}", keyword); + } + } + else if (arg is Argument.Star) + { + if (seenKwargs) + { + Errorf(arg, "*args may not follow **kwargs"); + } + else if (seenVarargs) + { + Errorf(arg, "multiple *args not allowed"); + } + seenVarargs = true; + } + else if (arg is Argument.StarStar) + { + if (seenKwargs) + { + Errorf(arg, "multiple **kwargs not allowed"); + } + seenKwargs = true; + } + } + + base.Visit(node); + } + + public override void Visit(ForStatement node) + { + if (locals.syntax is StarlarkFile) + { + Errorf( + node, + "for loops are not allowed at the top level. You may move it inside a function " + + "or use a comprehension, [f(x) for x in sequence]"); + } + loopCount++; + Visit(node.GetCollection()); + Assign(node.GetVars()); + VisitBlock(node.GetBody()); + if (loopCount <= 0) + { + throw new InvalidOperationException(); + } + loopCount--; + } + + public override void Visit(LoadStatement node) + { + if (locals.syntax is not StarlarkFile) + { + Errorf(node, "load statement not at top level"); + } + // Skip base.Visit: don't revisit local Identifier as a use. + } + + public override void Visit(FlowStatement node) + { + if (node.GetFlowKind() != TokenKind.PASS && loopCount <= 0) + { + Errorf(node, "{0} statement must be inside a for loop", node.GetFlowKind().ToDisplayString()); + } + base.Visit(node); + } + + public override void Visit(Comprehension node) + { + IReadOnlyList clauses = node.GetClauses(); + + // Following Python3, the first for clause is resolved outside the comprehension block. All + // the other loops are resolved in the scope of their own bindings, permitting forward + // references. + var for0 = (Comprehension.For)clauses[0]; + Visit(for0.GetIterable()); + + // A comprehension defines a distinct lexical block in the same function's frame. New + // bindings go in the frame but aren't visible to the parent block. + PushLocalBlock(node, this.locals.frame, this.locals.freevars); + + foreach (Comprehension.Clause clause in clauses) + { + if (clause is Comprehension.For forClause) + { + CreateBindingsForLHS(forClause.GetVars(), /* docComments= */ null); + } + } + for (int i = 0; i < clauses.Count; i++) + { + Comprehension.Clause clause = clauses[i]; + if (clause is Comprehension.For forClause) + { + if (i > 0) + { + Visit(forClause.GetIterable()); + } + Assign(forClause.GetVars()); + } + else + { + var ifClause = (Comprehension.If)clause; + Visit(ifClause.GetCondition()); + } + } + Visit(node.GetBody()); + PopLocalBlock(); + } + + public override void Visit(DefStatement node) + { + AssertIsBound(node.GetIdentifier()); + // resolveFunction() recurses into the body. + node.SetResolvedFunction( + ResolveFunction( + node, + node.GetIdentifier().GetName(), + node.GetIdentifier().GetStartLocation(), + node.GetParameters(), + node.GetReturnType(), + node.GetBody())); + } + + public override void Visit(LambdaExpression expr) + { + // resolveFunction() recurses into the body. + expr.SetResolvedFunction( + ResolveFunction( + expr, + "lambda", + expr.GetStartLocation(), + expr.GetParameters(), + /* returnType= */ null, + new List { ReturnStatement.Make(expr.GetBody()) })); + } + + public override void Visit(IfStatement node) + { + if (locals.syntax is StarlarkFile) + { + Errorf( + node, + "if statements are not allowed at the top level. You may move it inside a function " + + "or use an if expression (x if condition else y)."); + } + base.Visit(node); + } + + public override void Visit(AssignmentStatement node) + { + Visit(node.GetRHS()); + + // Disallow: [e, ...] += rhs. Other bad cases are handled in assign. + if (node.IsAugmented() && node.GetLHS() is ListExpression) + { + Errorf( + node.GetOperatorLocation(), + "cannot perform augmented assignment on a list or tuple expression"); + } + + if (node.GetType() != null && options.ResolveTypeSyntax) + { + Visit(node.GetType()!); + } + Assign(node.GetLHS()); + } + + public override void Visit(VarStatement node) + { + AssertIsBound(node.GetIdentifier()); + if (options.ResolveTypeSyntax) + { + Visit(node.GetType()); + } + } + + public override void Visit(CastExpression node) + { + if (options.ResolveTypeSyntax) + { + Visit(node.GetType()); + } + Visit(node.GetValue()); + } + + public override void Visit(IsInstanceExpression node) + { + Errorf(node, "isinstance() is not yet supported"); + } + + public override void Visit(TypeAliasStatement node) + { + if (locals.syntax is not StarlarkFile) + { + Errorf(node, "type alias statement not at top level"); + } + + if (options.ResolveTypeSyntax) + { + AssertIsBound(node.GetIdentifier()); + Visit(node.GetDefinition()); + } + } + + // Resolves a non-binding identifier to an existing binding, or null. + private Binding? Use(Identifier id) + { + string name = id.GetName(); + + // Locally defined in this function, comprehension, or file block, or an enclosing one? + Binding? bind = LookupLexical(name, locals); + if (bind != null) + { + return bind; + } + + // Defined at toplevel (global, predeclared, universal)? + if (toplevel.TryGetValue(name, out bind)) + { + return bind; + } + + Scope scope; + try + { + scope = module.Resolve(name); + } + catch (Undefined ex) + { + if (!Identifier.IsValid(name)) + { + // If Identifier was created by Parser.makeErrorExpression, it contains misparsed + // text. Ignore ex and report an appropriate error. + Errorf(id, "contains syntax errors"); + } + else if (ex.Candidates != null) + { + // Exception provided toplevel candidates. Show spelling suggestions of all symbols + // in scope. + string suggestion = SpellChecker.DidYouMean(name, GetAllSymbols(ex.Candidates)); + Errorf(id, "{0}{1}", ex.Message, suggestion); + } + else + { + Errorf(id, "{0}", ex.Message); + } + return null; + } + + switch (scope) + { + case Scope.GLOBAL: + bind = new Binding(scope, globals.Count, /* isSyntactic= */ false, id); + // Accumulate globals in module. + globals.Add(name); + break; + case Scope.PREDECLARED: + case Scope.UNIVERSAL: + bind = new Binding(scope, 0, /* isSyntactic= */ false, id); // index not used + break; + default: + throw new InvalidOperationException("bad scope: " + scope); + } + toplevel[name] = bind; + return bind; + } + + // lookupLexical finds a lexically enclosing local binding of the name, plumbing it through + // enclosing functions as needed. + private static Binding? LookupLexical(string name, Block b) + { + if (b.bindings.TryGetValue(name, out Binding? bind)) + { + return bind; + } + + if (b.parent != null) + { + bind = LookupLexical(name, b.parent); + if (bind != null) + { + // If a local binding was found in a parent block, and this block is a function, then + // it is a free variable of this function and must be plumbed through. + if (b.syntax is DefStatement || b.syntax is LambdaExpression) + { + Scope scope = bind.GetScope(); + if (scope == Scope.LOCAL || scope == Scope.FREE || scope == Scope.CELL) + { + if (scope == Scope.LOCAL) + { + bind.scope = Scope.CELL; + } + int index = b.freevars!.Count; + b.freevars.Add(bind); + bind = new Binding(Scope.FREE, index, /* isSyntactic= */ true, bind.first); + } + } + + // Memoize, to avoid duplicate free vars and repeated walks. + b.bindings[name] = bind; + } + } + + return bind; + } + + // Common code for def, lambda. + private Function ResolveFunction( + Node syntax, // DefStatement or LambdaExpression + string name, + Location loc, + IReadOnlyList parameters, + Expression? returnType, + IReadOnlyList body) + { + // Resolve parameter types and default initializer exprs in enclosing environment. + foreach (Parameter param in parameters) + { + if (param is Parameter.Optional) + { + Visit(param.GetDefaultValue()!); + } + if (param.GetType() != null && options.ResolveTypeSyntax) + { + Visit(param.GetType()!); + } + } + // Resolve return type in enclosing environment. + if (returnType != null && options.ResolveTypeSyntax) + { + Visit(returnType); + } + + // Enter function block. + var frame = new List(); + var freevars = new List(); + PushLocalBlock(syntax, frame, freevars); + + // Check parameter order and convert to run-time order: + // positionals, keyword-only, *args, **kwargs. + Parameter.Star? star = null; + Parameter.StarStar? starStar = null; + bool seenOptional = false; + int numKeywordOnlyParams = 0; + var paramsBuilder = new List(parameters.Count); + foreach (Parameter param in parameters) + { + if (param is Parameter.Mandatory) + { + // e.g. id + if (starStar != null) + { + Errorf( + param, + "required parameter {0} may not follow **{1}", + param.GetName(), + starStar.GetName()); + } + else if (star != null) + { + numKeywordOnlyParams++; + } + else if (seenOptional) + { + Errorf( + param, + "required positional parameter {0} may not follow an optional parameter", + param.GetName()); + } + BindParam(paramsBuilder, param); + } + else if (param is Parameter.Optional) + { + // e.g. id = default + seenOptional = true; + if (starStar != null) + { + Errorf(param, "optional parameter may not follow **{0}", starStar.GetName()); + } + else if (star != null) + { + numKeywordOnlyParams++; + } + BindParam(paramsBuilder, param); + } + else if (param is Parameter.Star pstar) + { + // * or *args + if (starStar != null) + { + Errorf(param, "* parameter may not follow **{0}", starStar.GetName()); + } + else if (star != null) + { + Errorf(param, "multiple * parameters not allowed"); + } + else + { + star = pstar; + } + } + else + { + // **kwargs + if (starStar != null) + { + Errorf(param, "multiple ** parameters not allowed"); + } + starStar = (Parameter.StarStar)param; + } + } + + // * or *args + if (star != null) + { + if (star.GetIdentifier() != null) + { + BindParam(paramsBuilder, star); + } + else if (numKeywordOnlyParams == 0) + { + Errorf(star, "bare * must be followed by keyword-only parameters"); + } + } + + // **kwargs + if (starStar != null) + { + BindParam(paramsBuilder, starStar); + } + + CreateBindingsForBlock(body); + VisitAll(body); + PopLocalBlock(); + + return new Function( + name, + loc, + paramsBuilder.ToImmutableArray(), + body.ToImmutableArray(), + star != null && star.GetIdentifier() != null, + starStar != null, + numKeywordOnlyParams, + frame, + freevars, + globals); + } + + private void BindParam(List paramsBuilder, Parameter param) + { + if (!Bind(param.GetIdentifier()!, /* isLoad= */ false, /* docComments= */ null)) + { + Errorf(param, "duplicate parameter: {0}", param.GetName()); + } + paramsBuilder.Add(param); + } + + /// + /// Process a binding use of a name by adding a binding to the current block if not already + /// bound, and associate the identifier with it. + /// + /// true if the name was newly bound in this block, or false if it already existed. + private bool Bind(Identifier id, bool isLoad, DocComments? docComments) + { + string name = id.GetName(); + bool isNew = false; + Binding? bind; + + // outside any function/comprehension, and not a (local) load? => global binding. + if (locals.syntax is StarlarkFile && !(isLoad && !options.LoadBindsGlobally)) + { + if (!toplevel.TryGetValue(name, out bind)) + { + // New global binding: add to module and to toplevel cache. + isNew = true; + bind = new Binding(Scope.GLOBAL, globals.Count, /* isSyntactic= */ true, id); + globals.Add(name); + if (docComments != null && docCommentsMap != null) + { + docCommentsMap[name] = docComments; + } + toplevel[name] = bind; + + // Does this new global binding conflict with a file-local load binding? + if (locals.bindings.TryGetValue(name, out Binding? prevLocal)) + { + GlobalLocalConflict(id, bind.scope, prevLocal); // global, local + } + } + else + { + ToplevelRebinding(id, bind); // global, global + } + } + else + { + // Binding is local to file, function, or comprehension. + if (!locals.bindings.TryGetValue(name, out bind)) + { + // New local binding: add to current block's bindings map, current function's frame. + isNew = true; + if (locals.syntax is Comprehension comprehension) + { + // Assumption: any block nested in a comprehension is either another + // comprehension or has its own frame (e.g. a lambda). + bind = new ComprehensionBinding(locals.frame.Count, id, comprehension); + } + else + { + bind = new Binding(Scope.LOCAL, locals.frame.Count, /* isSyntactic= */ true, id); + } + locals.bindings[name] = bind; + locals.frame.Add(bind); + } + + if (isLoad) + { + // Does this (file-local) load binding conflict with a previous one? + if (!isNew) + { + ToplevelRebinding(id, bind); // local, local + } + + // ...or a previous global? + if (toplevel.TryGetValue(name, out Binding? prev) && prev.scope == Scope.GLOBAL) + { + GlobalLocalConflict(id, bind.scope, prev); // local, global + } + } + } + + id.SetBinding(bind); + + return isNew; + } + + // Report conflicting top-level bindings of same scope, unless options.allowToplevelRebinding. + private void ToplevelRebinding(Identifier id, Binding prev) + { + if (!options.AllowToplevelRebinding) + { + Errorf(id, "'{0}' redeclared at top level", id.GetName()); + if (prev.isSyntactic) + { + Errorf(prev.first, "'{0}' previously declared here", id.GetName()); + } + } + } + + // Report global/local scope conflict on top-level bindings. + private void GlobalLocalConflict(Identifier id, Scope scope, Binding prev) + { + string newqual = scope == Scope.GLOBAL ? "global" : "file-local"; + string oldqual = prev.GetScope() == Scope.GLOBAL ? "global" : "file-local"; + Errorf(id, "conflicting {0} declaration of '{1}'", newqual, id.GetName()); + if (prev.isSyntactic) + { + Errorf(prev.first, "'{0}' previously declared as {1} here", id.GetName(), oldqual); + } + } + + // Returns the union of accessible local and top-level symbols. + private ISet GetAllSymbols(ISet predeclared) + { + var all = new HashSet(); + for (Block? b = locals; b != null; b = b.parent) + { + all.UnionWith(b.bindings.Keys); + } + all.UnionWith(predeclared); + all.UnionWith(toplevel.Keys); + return all; + } + + // Report an error if a load statement appears after another kind of statement. + private void CheckLoadAfterStatement(IReadOnlyList statements) + { + Statement? firstStatement = null; + + foreach (Statement statement in statements) + { + // Ignore string literals (e.g. docstrings). + if (statement is ExpressionStatement exprStmt + && exprStmt.GetExpression() is StringLiteral) + { + continue; + } + + if (statement is LoadStatement) + { + if (firstStatement == null) + { + continue; + } + Errorf(statement, "load statements must appear before any other statement"); + Errorf(firstStatement, "\tfirst non-load statement appears here"); + } + + firstStatement ??= statement; + } + } + + /// + /// Performs static checks, including resolution of identifiers in in the + /// environment defined by . Syntax must be resolved before it is + /// evaluated. + /// + public static void ResolveFile(StarlarkFile file, IModule module) + { + var r = new Resolver(file.errors, module, file.GetOptions(), file.docCommentsMap); + IReadOnlyList stmts = file.GetStatements(); + + // Check that load statements are on top. + if (r.options.RequireLoadStatementsFirst) + { + r.CheckLoadAfterStatement(stmts); + } + + var frame = new List(); + r.PushLocalBlock(file, frame, /* freevars= */ null); + + // First pass: creating bindings for statements in this block. + r.CreateBindingsForBlock(stmts); + + // Second pass: visit all references. + r.VisitAll(stmts); + + r.PopLocalBlock(); + + // If the final statement is an expression, synthesize a return statement. + int n = stmts.Count; + ImmutableArray bodyStmts; + if (n > 0 && stmts[n - 1] is ExpressionStatement lastExprStmt) + { + Expression expr = lastExprStmt.GetExpression(); + var builder = ImmutableArray.CreateBuilder(n); + for (int i = 0; i < n - 1; i++) + { + builder.Add(stmts[i]); + } + builder.Add(ReturnStatement.Make(expr)); + bodyStmts = builder.ToImmutable(); + } + else + { + bodyStmts = stmts.ToImmutableArray(); + } + + // Annotate with resolved information about the toplevel function. + file.SetResolvedFunction( + new Function( + "", + file.GetStartLocation(), + /* params= */ ImmutableArray.Empty, + /* body= */ bodyStmts, + /* hasVarargs= */ false, + /* hasKwargs= */ false, + /* numKeywordOnlyParams= */ 0, + frame, + /* freevars= */ new List(), + r.globals)); + } + + /// + /// Performs static checks, including resolution of identifiers in in the + /// environment defined by . + /// + /// on resolution error. + public static Function ResolveExpr(Expression expr, IModule module, FileOptions options) + { + var errors = new List(); + var r = new Resolver(errors, module, options, /* docCommentsMap= */ null); + + var frame = new List(); + r.PushLocalBlock(null, frame, /* freevars= */ null); // for bindings in list comprehensions + r.Visit(expr); + r.PopLocalBlock(); + + if (errors.Count != 0) + { + throw new SyntaxError.Exception(errors); + } + + // Return no-arg function that computes the expression. + return new Function( + "", + expr.GetStartLocation(), + /* params= */ ImmutableArray.Empty, + ImmutableArray.Create(ReturnStatement.Make(expr)), + /* hasVarargs= */ false, + /* hasKwargs= */ false, + /* numKeywordOnlyParams= */ 0, + frame, + /* freevars= */ new List(), + r.globals); + } + + private void PushLocalBlock(Node? syntax, List frame, List? freevars) + { + locals = new Block(locals, syntax, frame, freevars); + } + + private void PopLocalBlock() + { + locals = locals.parent!; + } +} diff --git a/src/Starlark/Syntax/TypeChecker.cs b/src/Starlark/Syntax/TypeChecker.cs new file mode 100644 index 000000000..732848dcb --- /dev/null +++ b/src/Starlark/Syntax/TypeChecker.cs @@ -0,0 +1,1622 @@ +// Copyright 2025 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Linq; +using Starlark.Spelling; + +namespace Starlark.Syntax; + +/// +/// A visitor for validating that expressions and statements respect the types of the symbols +/// appearing within them, as determined by the type tagger. +/// +/// In addition, this visitor modifies the function type on the +/// objects of s in the AST (originally populated by the +/// ) to have a more precise return type, if possible; and populates the +/// types on the objects of untyped variables with the inferred types +/// of their values in their first assignments in typed code. +/// +/// Type annotations are not traversed by this visitor. +/// +public sealed class TypeChecker : NodeVisitor +{ + private readonly List errors; + private readonly TypeContext typeContext; + + // Empty if we were invoked via inferTypeOf() to type-check an expression. Populated and mutated + // by visitation. Used as a stack: top == last element. + private readonly List functionStack = new(); + + // Formats and reports an error at the start of the specified node. + private void Errorf(Node node, string format, params object?[] args) + { + Errorf(node.GetStartLocation(), format, args); + } + + // Formats and reports an error at the specified location. + private void Errorf(Location loc, string format, params object?[] args) + { + errors.Add(new SyntaxError(loc, string.Format(format, args))); + } + + private void BinaryOperatorError( + StarlarkType xType, + TokenKind op, + Location operatorLocation, + StarlarkType yType, + bool augmentedAssignment, + string extraMessage) + { + Errorf( + operatorLocation, + "operator '{0}{1}' cannot be applied to types '{2}' and '{3}'{4}", + op.ToDisplayString(), + augmentedAssignment ? "=" : "", + xType, + yType, + extraMessage.Length == 0 ? "" : ": " + extraMessage); + } + + private void BinaryOperatorError( + StarlarkType xType, + TokenKind op, + Location operatorLocation, + StarlarkType yType, + bool augmentedAssignment) + { + BinaryOperatorError(xType, op, operatorLocation, yType, augmentedAssignment, ""); + } + + private static string Plural(int n) + { + return n == 1 ? "" : "s"; + } + + private TypeChecker(List errors, TypeContext typeContext) + { + this.errors = errors; + this.typeContext = typeContext; + } + + /// + /// Returns the annotated type of an identifier's symbol, asserting that the binding information + /// is present. + /// + /// If a type is not set on the binding it is taken to be Any. + /// + private StarlarkType GetType(Identifier id) + { + Resolver.Binding? binding = id.GetBinding(); + if (binding == null) + { + throw new ArgumentNullException(nameof(id)); + } + StarlarkType? type = binding.GetType(); + return type != null ? type : Types.ANY; + } + + private void ErrorIfKeyNotInt(IndexExpression index, StarlarkType objType, StarlarkType keyType) + { + if (!StarlarkType.AssignableFrom(Types.INT, keyType)) + { + Errorf( + index.GetLbracketLocation(), + "'{0}' of type '{1}' must be indexed by an integer, but got '{2}'", + index.GetObject(), + objType, + keyType); + } + } + + /// + /// Infers the type of an expression from a bottom-up traversal, relying on type information + /// stored in identifier bindings by the . + /// + /// May not be called on type expressions (annotations, var statements, type alias + /// statements). + /// + private StarlarkType Infer(Expression expr) + { + switch (expr.Kind) + { + case Expression.ExpressionKind.IDENTIFIER: + return GetType((Identifier)expr); + + case Expression.ExpressionKind.STRING_LITERAL: + return Types.STR; + + case Expression.ExpressionKind.INT_LITERAL: + return Types.INT; + + case Expression.ExpressionKind.FLOAT_LITERAL: + return Types.FLOAT; + + case Expression.ExpressionKind.CAST: + { + var cast = (CastExpression)expr; + var unused = Infer(cast.GetValue()); // only to verify the value expr is well-typed + return cast.GetStarlarkType()!; + } + + case Expression.ExpressionKind.DOT: + return InferDot((DotExpression)expr); + + case Expression.ExpressionKind.INDEX: + return InferIndex((IndexExpression)expr); + + case Expression.ExpressionKind.SLICE: + return InferSlice((SliceExpression)expr); + + case Expression.ExpressionKind.LAMBDA: + { + var lambda = (LambdaExpression)expr; + StarlarkType inferedReturnType = Infer(lambda.GetBody()); + Types.CallableType originalType = lambda.GetResolvedFunction()!.GetFunctionType()!; + if (!originalType.GetReturnType().Equals(inferedReturnType)) + { + // Update the lambda function type with a more precise return type. + lambda + .GetResolvedFunction()! + .SetFunctionType( + Types.Callable( + originalType.GetParameterNames(), + originalType.GetParameterTypes(), + originalType.GetNumPositionalOnlyParameters(), + originalType.GetNumPositionalParameters(), + originalType.GetMandatoryParameters(), + originalType.GetVarargsType(), + originalType.GetKwargsType(), + inferedReturnType)); + } + return lambda.GetResolvedFunction()!.GetFunctionType()!; + } + + case Expression.ExpressionKind.LIST_EXPR: + { + var list = (ListExpression)expr; + var elementTypes = new List(); + foreach (Expression element in list.GetElements()) + { + elementTypes.Add(Infer(element)); + } + return list.IsTuple() + ? Types.Tuple(elementTypes) + : Types.List(Types.Union(elementTypes)); + } + + case Expression.ExpressionKind.DICT_EXPR: + { + var dict = (DictExpression)expr; + var keyTypes = new List(); + var valueTypes = new List(); + foreach (var entry in dict.GetEntries()) + { + keyTypes.Add(Infer(entry.GetKey())); + valueTypes.Add(Infer(entry.GetValue())); + } + return Types.Dict(Types.Union(keyTypes), Types.Union(valueTypes)); + } + + case Expression.ExpressionKind.CALL: + return InferCall((CallExpression)expr); + + case Expression.ExpressionKind.CONDITIONAL: + { + var cond = (ConditionalExpression)expr; + return Types.Union(Infer(cond.GetThenCase()), Infer(cond.GetElseCase())); + } + + case Expression.ExpressionKind.BINARY_OPERATOR: + { + var binop = (BinaryOperatorExpression)expr; + StarlarkType xType = Infer(binop.GetX()); + StarlarkType yType = Infer(binop.GetY()); + return InferBinaryOperator( + binop.GetX(), + xType, + binop.GetOperator(), + binop.GetOperatorLocation(), + binop.GetY(), + yType, + /* augmentedAssignment= */ false); + } + + case Expression.ExpressionKind.UNARY_OPERATOR: + { + var unop = (UnaryOperatorExpression)expr; + if (unop.GetOperator() == TokenKind.NOT) + { + // NOT always returns a boolean (even if applied to Any or unions). + return Types.BOOL; + } + StarlarkType xType = Infer(unop.GetX()); + if (xType.Equals(Types.ANY) + || ((unop.GetOperator() == TokenKind.MINUS || unop.GetOperator() == TokenKind.PLUS) + && StarlarkType.AssignableFrom(Types.NUMERIC, xType)) + || (unop.GetOperator() == TokenKind.TILDE && xType.Equals(Types.INT))) + { + // Unary operators other than NOT preserve the type of their operand. + return xType; + } + Errorf( + unop.GetStartLocation(), + "operator '{0}' cannot be applied to type '{1}'", + unop.GetOperator().ToDisplayString(), + xType); + return Types.ANY; + } + + case Expression.ExpressionKind.COMPREHENSION: + return InferComprehension((Comprehension)expr); + + default: + // TODO: #28037 - support isinstance expressions. + Errorf(expr, "UNSUPPORTED: cannot typecheck {0} expression", expr.Kind); + return Types.ANY; + } + } + + /// + /// Returns the integer value of an expression if it's an integer value (or a unary expression + /// negating an integer value) which can be exactly represented as a 32-bit integer, or null + /// otherwise (in particular, if the expression itself is null). + /// + private static int? GetIntValueExact(Expression? expr) + { + if (expr is IntLiteral intLiteral) + { + return intLiteral.GetIntValueExact(); + } + else if (expr is UnaryOperatorExpression unop + && unop.GetOperator() == TokenKind.MINUS + && unop.GetX() is IntLiteral negatedIntLiteral) + { + int? x = negatedIntLiteral.GetIntValueExact(); + if (x != null) + { + return -x.Value; // safe since x >= 0 + } + } + return null; + } + + private StarlarkType InferDot(DotExpression dot) + { + return Types.Union(InferDotUnfolded(dot, Infer(dot.GetObject()))); + } + + /// + /// Infers the non-flattened unfolded list of possible types of a dot expression. + /// + private ImmutableArray InferDotUnfolded(DotExpression dot, StarlarkType objType) + { + string name = dot.GetField().GetName(); + + if (objType.Equals(Types.ANY)) + { + return ImmutableArray.Create(Types.ANY); + } + + IReadOnlyCollection objElemTypes = Types.UnfoldUnion(objType); + var resultTypes = ImmutableArray.CreateBuilder(objElemTypes.Count); + foreach (StarlarkType objElemType in objElemTypes) + { + StarlarkType? fieldType = objElemType.GetField(name, typeContext); + if (fieldType == null) + { + Errorf( + dot.GetDotLocation(), + "'{0}' of type '{1}' does not have field '{2}'", + dot.GetObject(), + objType, + name); + return ImmutableArray.Create(Types.ANY); + } + resultTypes.Add(fieldType); + } + return resultTypes.ToImmutable(); + } + + private StarlarkType InferIndex(IndexExpression index) + { + return Types.Union( + InferIndexUnfolded(index, Infer(index.GetObject()), Infer(index.GetKey()))); + } + + /// + /// Infers the non-flattened unfolded list of possible types of an index expression. + /// + private ImmutableArray InferIndexUnfolded( + IndexExpression index, StarlarkType objType, StarlarkType keyType) + { + Expression obj = index.GetObject(); + Expression key = index.GetKey(); + + if (objType.Equals(Types.ANY)) + { + return ImmutableArray.Create(Types.ANY); + } + + IReadOnlyCollection objElemTypes = Types.UnfoldUnion(objType); + var resultTypes = ImmutableArray.CreateBuilder(objElemTypes.Count); + foreach (StarlarkType objElemType in objElemTypes) + { + if (objElemType.Equals(Types.ANY)) + { + resultTypes.Add(Types.ANY); + } + else if (objElemType is Types.FixedLengthTupleType tupleType) + { + ErrorIfKeyNotInt(index, objElemType, keyType); + var elementTypes = tupleType.GetElementTypes(); + StarlarkType? resultType = null; + // Project out the type of the specific component if we can statically determine the index. + int? intKey = GetIntValueExact(key); + if (intKey != null) + { + int i = intKey.Value; + if (i < 0) + { + // Same logic as for EvalUtils#getSequenceIndex. + i += elementTypes.Count; + } + if (0 <= i && i < elementTypes.Count) + { + resultType = elementTypes[i]; + } + else + { + Errorf( + index.GetLbracketLocation(), + "'{0}' of type '{1}' is indexed by integer {2}, which is out-of-range", + obj, + objType, + intKey); + // Don't complain about uses of the result type when we don't even know what + // result type the user wanted. + return ImmutableArray.Create(Types.ANY); + } + } + if (resultType == null) + { + resultType = tupleType.ToHomogeneous().GetElementType(); + } + resultTypes.Add(resultType); + } + else if (objElemType is Types.AbstractSequenceType sequenceType) + { + ErrorIfKeyNotInt(index, objType, keyType); // fall through on error + resultTypes.Add(sequenceType.GetElementType()); + } + else if (objElemType is Types.AbstractMappingType mappingType) + { + if (!StarlarkType.AssignableFrom(mappingType.GetKeyType(), keyType)) + { + Errorf( + index.GetLbracketLocation(), + "'{0}' of type '{1}' requires key type '{2}', but got '{3}'", + obj, + objType, + mappingType.GetKeyType(), + keyType); + // Fall through to returning the value type. + } + resultTypes.Add(mappingType.GetValueType()); + } + else if (objElemType.Equals(Types.STR)) + { + ErrorIfKeyNotInt(index, objType, keyType); // fall through on error + resultTypes.Add(Types.STR); + } + else + { + Errorf(index.GetLbracketLocation(), "cannot index '{0}' of type '{1}'", obj, objType); + return ImmutableArray.Create(Types.ANY); + } + } + return resultTypes.ToImmutable(); + } + + private StarlarkType InferSlice(SliceExpression slice) + { + int? step = GetIntValueExact(slice.GetStep()); + if (step == null) + { + step = 1; + if (slice.GetStep() != null) + { + StarlarkType stepType = Infer(slice.GetStep()!); + if (!StarlarkType.AssignableFrom(Types.INT, stepType)) + { + Errorf(slice.GetStep()!, "got '{0}' for slice step, want int", stepType); + return Types.ANY; + } + } + } + else if (step == 0) + { + Errorf(slice.GetStep()!, "slice step cannot be zero"); + return Types.ANY; + } + if (slice.GetStart() != null) + { + StarlarkType startType = Infer(slice.GetStart()!); + if (!StarlarkType.AssignableFrom(Types.INT, startType)) + { + Errorf(slice.GetStart()!, "got '{0}' for start index, want int", startType); + return Types.ANY; + } + } + if (slice.GetStop() != null) + { + StarlarkType stopType = Infer(slice.GetStop()!); + if (!StarlarkType.AssignableFrom(Types.INT, stopType)) + { + Errorf(slice.GetStop()!, "got '{0}' for stop index, want int", stopType); + return Types.ANY; + } + } + + StarlarkType objType = Infer(slice.GetObject()); + if (objType.Equals(Types.ANY)) + { + return Types.ANY; + } + var resultTypes = new List(); + foreach (StarlarkType objElemType in Types.UnfoldUnion(objType)) + { + if (objElemType.Equals(Types.ANY)) + { + resultTypes.Add(Types.ANY); + } + else if (objElemType.Equals(Types.STR)) + { + resultTypes.Add(Types.STR); + } + else if (objElemType is Types.FixedLengthTupleType tupleType) + { + IReadOnlyList tupleElementTypes = tupleType.GetElementTypes(); + int len = tupleElementTypes.Count; + int? start = GetIntValueExact(slice.GetStart()); + int? stop = GetIntValueExact(slice.GetStop()); + var resultTupleElementTypes = ImmutableArray.CreateBuilder(); + if (step != null + && HaveExactSliceBound(slice.GetStart(), start) + && HaveExactSliceBound(slice.GetStop(), stop)) + { + if (step > 0) + { + int startClamped = start != null ? SyntaxUtils.ToSliceBound(start.Value, len) : 0; + int stopClamped = stop != null ? SyntaxUtils.ToSliceBound(stop.Value, len) : len; + for (long i = startClamped; i < stopClamped && (int)i == i; i += step.Value) + { + resultTupleElementTypes.Add(tupleElementTypes[(int)i]); + } + } + else + { + int startClamped = + start != null ? SyntaxUtils.ToReverseSliceBound(start.Value, len) : len - 1; + int stopClamped = + stop != null ? SyntaxUtils.ToReverseSliceBound(stop.Value, len) : -1; + for (long i = startClamped; i > stopClamped && (int)i == i; i += step.Value) + { + resultTupleElementTypes.Add(tupleElementTypes[(int)i]); + } + } + resultTypes.Add(Types.Tuple(resultTupleElementTypes.ToImmutable())); + } + else + { + resultTypes.Add(tupleType.ToHomogeneous()); + } + } + else if (objElemType is Types.AbstractSequenceType sequenceType) + { + resultTypes.Add(sequenceType); + } + else + { + Errorf( + slice.GetLbracketLocation(), + "invalid slice operand '{0}' of type '{1}', expected Sequence or str", + slice.GetObject(), + objElemType); + resultTypes.Add(Types.ANY); + } + } + return Types.Union(resultTypes); + } + + private static bool HaveExactSliceBound(Expression? expr, int? exprIntValueExact) + { + if (expr == null) + { + // Bound not specified, so we know its exact value (the default value) + return true; + } + if (exprIntValueExact != null) + { + // Bound is specified and is a 32-bit integer literal (or negation) + return true; + } + return false; + } + + private StarlarkType InferBinaryOperator( + Expression xExpr, + StarlarkType xType, + TokenKind op, + Location operatorLocation, + Expression yExpr, + StarlarkType yType, + bool augmentedAssignment) + { + switch (op) + { + case TokenKind.AND: + case TokenKind.OR: + case TokenKind.EQUALS_EQUALS: + case TokenKind.NOT_EQUALS: + // Boolean regardless of LHS and RHS. + return Types.BOOL; + + case TokenKind.LESS: + case TokenKind.LESS_EQUALS: + case TokenKind.GREATER: + case TokenKind.GREATER_EQUALS: + // Boolean or type error. + if (StarlarkType.Comparable(xType, yType)) + { + return Types.BOOL; + } + BinaryOperatorError(xType, op, operatorLocation, yType, augmentedAssignment); + return Types.ANY; + + default: + { + // Take the union of all types inferred by crossing the left and right union + // elements (each of which must be a valid combination of rhs and lhs for the + // operator). + IReadOnlyCollection xTypes = Types.UnfoldUnion(xType); + IReadOnlyCollection yTypes = Types.UnfoldUnion(yType); + var resultTypes = new List(); + foreach (StarlarkType xElemType in xTypes) + { + foreach (StarlarkType yElemType in yTypes) + { + StarlarkType? resultType = xElemType.InferBinaryOperator(op, yElemType, true); + if (resultType == null) + { + resultType = yElemType.InferBinaryOperator(op, xElemType, false); + } + if (resultType == null && op == TokenKind.STAR) + { + // Tuple repetition is the only case where we need to examine the exprs. + if (StarlarkType.AssignableFrom(Types.INT, xElemType) + && yElemType is Types.TupleType tupleY) + { + resultType = InferTupleRepetition(tupleY, xExpr); + } + else if (StarlarkType.AssignableFrom(Types.INT, yElemType) + && xElemType is Types.TupleType tupleX) + { + resultType = InferTupleRepetition(tupleX, yExpr); + } + } + if (resultType == null) + { + BinaryOperatorError(xType, op, operatorLocation, yType, augmentedAssignment); + return Types.ANY; + } + resultTypes.Add(resultType); + } + } + return Types.Union(resultTypes); + } + } + } + + private StarlarkType InferCall(CallExpression call) + { + // Collect and check the shape of the call's *args/**kwargs. + VarargsArgument? varargs = null; + KwargsArgument? kwargs = null; + int numArgs = call.GetArguments().Count; + if (numArgs > 0 && call.GetArguments()[numArgs - 1] is Argument.StarStar starStarArg) + { + kwargs = KwargsArgument.Of(starStarArg, this); + if (kwargs == null) + { + // error already reported + return Types.ANY; + } + numArgs--; + } + if (numArgs > 0 && call.GetArguments()[numArgs - 1] is Argument.Star starArg) + { + varargs = VarargsArgument.Of(starArg, this); + if (varargs == null) + { + // error already reported + return Types.ANY; + } + numArgs--; + } + + StarlarkType callFunctionType = Infer(call.GetFunction()); + if (callFunctionType.Equals(Types.ANY)) + { + return Types.ANY; + } + + // Collect call's argument types (excluding *args and **kwargs). + ImmutableArray argTypes = + call.GetArguments() + .Take(numArgs) + .Select(a => a.GetValue()) + .Select(Infer) + .ToImmutableArray(); + + IReadOnlyCollection callFunctionTypes = Types.UnfoldUnion(callFunctionType); + var returnTypes = new List(); + foreach (StarlarkType callFunctionElemType in callFunctionTypes) + { + if (callFunctionElemType.Equals(Types.ANY)) + { + // Nothing we can check. + returnTypes.Add(Types.ANY); + continue; + } + Types.CallableType? callable = + callFunctionElemType is Types.CallableType c ? c : null; + if (callable == null) + { + Errorf( + call.GetFunction(), + "'{0}' is not callable; got type '{1}'", + call.GetFunction(), + callFunctionType); + return Types.ANY; + } + // Indices of residual arguments in call.GetArguments() and their corresponding types in + // argTypes. + var residualPositional = new List(0); + var residualNamed = new List(0); + // Names of mandatory parameters (both positional and named) having a corresponding arg. + var seenMandatoryParameters = new List(callable.GetMandatoryParameters().Count); + for (int i = 0; i < numArgs; i++) + { + Argument arg = call.GetArguments()[i]; + int parameterIndex; + if (i < call.GetNumPositionalArguments()) + { + // positional argument + if (i < callable.GetNumPositionalParameters()) + { + parameterIndex = i; + } + else + { + residualPositional.Add(i); + continue; + } + } + else + { + // keyword argument + parameterIndex = IndexOf(callable.GetParameterNames(), arg.GetName()); + if (parameterIndex < callable.GetNumPositionalOnlyParameters()) + { + // Either no param was found (i<0) or it's positional-only (0<=i names, string? name) + { + if (name == null) + { + return -1; + } + for (int i = 0; i < names.Count; i++) + { + if (names[i].Equals(name, StringComparison.Ordinal)) + { + return i; + } + } + return -1; + } + + private sealed class VarargsArgument + { + public Expression Expr { get; } + public StarlarkType ElementType { get; } + + private VarargsArgument(Expression expr, StarlarkType elementType) + { + this.Expr = expr; + this.ElementType = elementType; + } + + public static VarargsArgument? Of(Argument.Star arg, TypeChecker checker) + { + Expression varargs = arg.GetValue(); + StarlarkType varargsType = checker.Infer(varargs); + StarlarkType? varargsElementType = FindElementType(varargsType); + if (varargsElementType == null) + { + checker.Errorf(varargs, "argument after * must be a sequence, not '{0}'", varargsType); + return null; + } + return new VarargsArgument(varargs, varargsElementType); + } + + /// + /// Finds the smallest Sequence[E] type which is a supertype of the given type, and + /// return E; or null if the given type does not have such a supertype. + /// + private static StarlarkType? FindElementType(StarlarkType maybeSequence) + { + if (maybeSequence.Equals(Types.ANY)) + { + return Types.ANY; + } + IReadOnlyCollection unfolded = Types.UnfoldUnion(maybeSequence); + var elements = new List(unfolded.Count); + foreach (StarlarkType unfoldedElem in unfolded) + { + if (unfoldedElem is Types.AbstractSequenceType sequence) + { + elements.Add(sequence.GetElementType()); + } + else + { + return null; + } + } + return Types.Union(elements); + } + } + + private sealed class KwargsArgument + { + public Expression Expr { get; } + public StarlarkType ValueType { get; } + + private KwargsArgument(Expression expr, StarlarkType valueType) + { + this.Expr = expr; + this.ValueType = valueType; + } + + public static KwargsArgument? Of(Argument.StarStar arg, TypeChecker checker) + { + Expression kwargs = arg.GetValue(); + StarlarkType kwargsType = checker.Infer(kwargs); + StarlarkType? kwargsValueType = FindValueType(kwargsType); + if (kwargsValueType == null) + { + checker.Errorf( + kwargs, "argument after ** must be a dict with string keys, not '{0}'", kwargsType); + return null; + } + return new KwargsArgument(kwargs, kwargsValueType); + } + + /// + /// Finds the smallest Mapping[K, V] type which is a supertype of the given type such + /// that K is a subtype of str, and returns V; or null if the given type does not have such a + /// supertype. + /// + private static StarlarkType? FindValueType(StarlarkType maybeMapping) + { + if (maybeMapping.Equals(Types.ANY)) + { + return Types.ANY; + } + IReadOnlyCollection unfolded = Types.UnfoldUnion(maybeMapping); + var values = new List(unfolded.Count); + foreach (StarlarkType unfoldedElem in unfolded) + { + if (unfoldedElem is Types.AbstractMappingType mapping + && StarlarkType.AssignableFrom(Types.STR, mapping.GetKeyType())) + { + values.Add(mapping.GetValueType()); + } + else + { + return null; + } + } + return Types.Union(values); + } + } + + /// + /// Returns true if the call's residual positional arguments (if any) satisfy the type checker. + /// Otherwise, reports an error and returns false. + /// + private bool CheckCallResidualPositionals( + List residualPositional, + CallExpression call, + Types.CallableType callable, + IReadOnlyList argTypes) + { + if (residualPositional.Count == 0) + { + return true; + } + else if (callable.GetVarargsType() == null) + { + // callable cannot accept residual positional args + if (callable.GetNumPositionalParameters() > 0) + { + Errorf( + call.GetArguments()[callable.GetNumPositionalParameters()], + "'{0}()' accepts no more than {1} positional argument{2} but got {3}", + call.GetFunction(), + callable.GetNumPositionalParameters(), + Plural(callable.GetNumPositionalParameters()), + call.GetNumPositionalArguments()); + } + else + { + Errorf( + call.GetArguments()[0], + "'{0}()' does not accept positional arguments, but got {1}", + call.GetFunction(), + call.GetNumPositionalArguments()); + } + return false; + } + else + { + // residual positional args go into callable's varargs + foreach (int argIndex in residualPositional) + { + Argument arg = call.GetArguments()[argIndex]; + StarlarkType argType = argTypes[argIndex]; + if (!CheckAssignable( + callable.GetVarargsType(), argType, call, arg, "residual positional arguments")) + { + return false; + } + } + } + return true; + } + + /// + /// Returns true if the call's residual named arguments (if any) satisfy the type checker. + /// Otherwise, reports an error and returns false. + /// + private bool CheckCallResidualNamed( + List residualNamed, + CallExpression call, + Types.CallableType callable, + IReadOnlyList argTypes) + { + if (residualNamed.Count == 0) + { + return true; + } + else if (callable.GetKwargsType() == null) + { + // callable cannot accept residual named args + var residualNamedArgs = + residualNamed.Select(i => call.GetArguments()[i]).ToList(); + Errorf( + residualNamedArgs[0], + "'{0}()' got unexpected keyword argument{1}: {2}{3}", + call.GetFunction(), + Plural(residualNamedArgs.Count), + string.Join(", ", residualNamedArgs.Select(a => a.GetName())), + // If there are multiple residual named args, it's likely due to calling the wrong + // function or misunderstanding the API, so arg spelling suggestions would not help. + residualNamedArgs.Count == 1 + ? SpellChecker.DidYouMean( + residualNamedArgs[0].GetName()!, + Sublist( + callable.GetParameterNames(), + callable.GetNumPositionalOnlyParameters(), + callable.GetParameterNames().Count)) + : ""); + return false; + } + else + { + // residual named args go into callable's kwargs + foreach (int argIndex in residualNamed) + { + Argument arg = call.GetArguments()[argIndex]; + StarlarkType argType = argTypes[argIndex]; + if (!CheckAssignable( + callable.GetKwargsType(), argType, call, arg, "residual keyword arguments")) + { + return false; + } + } + } + return true; + } + + private static IReadOnlyList Sublist(IReadOnlyList list, int fromInclusive, int toExclusive) + { + var result = new List(toExclusive - fromInclusive); + for (int i = fromInclusive; i < toExclusive; i++) + { + result.Add(list[i]); + } + return result; + } + + /// + /// Returns true if all mandatory parameters were explicitly supplied by the call or potentially + /// supplied through *args or **kwargs. Otherwise, reports an error and returns false. + /// + private bool CheckCallMissingMandatoryArgs( + List seenMandatoryParameters, + bool callHasVarargs, + bool callHasKwargs, + CallExpression call, + Types.CallableType callable) + { + if (seenMandatoryParameters.Count < callable.GetMandatoryParameters().Count) + { + var seenMandatorySet = seenMandatoryParameters.ToHashSet(); + // Identify mandatory parameters which were not seen and which cannot be possibly supplied + // from the call's *args or **kwargs. + var missingMandatory = new List(0); + for (int i = 0; i < callable.GetParameterNames().Count; i++) + { + string name = callable.GetParameterNames()[i]; + if (!seenMandatorySet.Contains(name)) + { + if (i < callable.GetNumPositionalOnlyParameters() && !callHasVarargs) + { + missingMandatory.Add(name); + } + else if (i < callable.GetNumPositionalParameters() + && !callHasVarargs + && !callHasKwargs) + { + missingMandatory.Add(name); + } + else if (i >= callable.GetNumPositionalParameters() && !callHasKwargs) + { + missingMandatory.Add(name); + } + } + } + if (missingMandatory.Count != 0) + { + Errorf( + call.GetLparenLocation(), + "'{0}()' missing {1} required argument{2}: {3}", + call.GetFunction(), + missingMandatory.Count, + Plural(missingMandatory.Count), + string.Join(", ", missingMandatory)); + return false; + } + } + return true; + } + + private StarlarkType InferComprehension(Comprehension comp) + { + foreach (Comprehension.Clause clause in comp.GetClauses()) + { + switch (clause) + { + case Comprehension.For forClause: + CheckForClause( + forClause.GetVars(), forClause.GetIterable(), "comprehension 'for' clause"); + break; + case Comprehension.If ifClause: + // Infer only to type-check. Condition is evaluated as truthy/falsy, which is + // valid for every type. + var unused = Infer(ifClause.GetCondition()); + break; + } + } + if (comp.IsDict()) + { + var bodyEntry = (DictExpression.Entry)comp.GetBody(); + return Types.Dict(Infer(bodyEntry.GetKey()), Infer(bodyEntry.GetValue())); + } + else + { + var bodyElement = (Expression)comp.GetBody(); + return Types.List(Infer(bodyElement)); + } + } + + /// Recursively type-checks the vars and the iterable, and assigns the vars to the iterable. + private void CheckForClause(Expression vars, Expression iterable, string what) + { + StarlarkType iterableType = Infer(iterable); + StarlarkType varsRhsType; // The type of the value assigned to the vars expression. + if (iterableType.Equals(Types.ANY)) + { + varsRhsType = Types.ANY; + } + else + { + var varUnionElements = new List(); + foreach (StarlarkType iterableUnionElement in Types.UnfoldUnion(iterableType)) + { + if (iterableUnionElement.Equals(Types.ANY)) + { + varUnionElements.Add(Types.ANY); + } + else if (iterableUnionElement is Types.AbstractCollectionType collection) + { + varUnionElements.Add(collection.GetElementType()); + } + else + { + Errorf(iterable, "{0} operand must be an iterable, got '{1}'", what, iterableType); + } + } + varsRhsType = Types.Union(varUnionElements); + } + Assign(vars, varsRhsType); + } + + private bool CheckAssignable( + StarlarkType? lhs, + StarlarkType? rhs, + CallExpression call, + Node node, + string nodeDescription) + { + if (lhs != null && rhs != null) + { + if (!StarlarkType.AssignableFrom(lhs, rhs)) + { + Errorf( + node, + "in call to '{0}()', {1} must be '{2}', not '{3}'", + call.GetFunction(), + nodeDescription, + lhs, + rhs); + return false; + } + } + return true; + } + + private static StarlarkType InferTupleRepetition(Types.TupleType tuple, Expression timesExpr) + { + int? times = GetIntValueExact(timesExpr); + if (times != null) + { + return tuple.Repeat(times.Value); + } + return tuple.ToHomogeneous(); + } + + /// + /// Returns the inferred type of an expression. + /// + /// The expression must have already been resolved and type-tagged. + /// + /// if a static type error is present in the expression. + public static StarlarkType InferTypeOf(Expression expr, TypeContext typeContext) + { + var errors = new List(); + var tc = new TypeChecker(errors, typeContext); + StarlarkType result = tc.Infer(expr); + if (errors.Count != 0) + { + throw new SyntaxError.Exception(tc.errors); + } + return result; + } + + /// + /// Recursively typechecks the assignment of type to the target + /// expression . + /// + private void Assign(Expression lhs, StarlarkType rhsType) + { + CheckState(UsesTypeSyntax()); + + if (lhs.Kind == Expression.ExpressionKind.LIST_EXPR) + { + AssignSequence((ListExpression)lhs, rhsType); + return; + } + + ImmutableArray lhsMeet = InferIndividualAssignmentTarget(lhs); + foreach (StarlarkType lhsType in lhsMeet) + { + if (!StarlarkType.AssignableFrom(lhsType, rhsType)) + { + Errorf(lhs, "cannot assign type '{0}' to {1}", rhsType, FormatExprWithMeetType(lhs, lhsMeet)); + break; + } + } + + if (lhs is Identifier id && id.GetBinding()!.GetType() == null) + { + // If a variable has not been typed, infer its type from the rhs of the 1st assignment. + id.GetBinding()!.SetType(rhsType); + } + } + + private static string FormatExprWithMeetType(Expression expr, ImmutableArray types) + { + if (types.Length == 1) + { + return string.Format("'{0}' of type '{1}'", expr, types[0]); + } + else + { + return string.Format( + "'{0}' which expects a value satisfying all of the {1} types [{2}]", + expr, + types.Length, + string.Join(", ", types.Select(t => string.Format("'{0}'", t)))); + } + } + + /// + /// Verifies that the expression can be used as the target of a non-sequence assignment (or + /// augmented assignment). Returns a non-flattened unfolded list of LHS acceptor types. + /// + private ImmutableArray InferIndividualAssignmentTarget(Expression lhs) + { + switch (lhs.Kind) + { + case Expression.ExpressionKind.INDEX: + { + var indexExpr = (IndexExpression)lhs; + StarlarkType objectType = Infer(indexExpr.GetObject()); + StarlarkType keyType = Infer(indexExpr.GetKey()); + if (!objectType.HasSetIndex()) + { + Errorf( + lhs, + "{0} of type '{1}' does not support item assignment", + indexExpr.GetObject(), + objectType); + } + return InferIndexUnfolded(indexExpr, objectType, keyType); + } + + case Expression.ExpressionKind.DOT: + { + var dotExpr = (DotExpression)lhs; + StarlarkType objectType = Infer(dotExpr.GetObject()); + if (!objectType.HasSetField()) + { + Errorf( + lhs, + "{0} of type '{1}' does not support field assignment", + dotExpr.GetObject(), + objectType); + } + return InferDotUnfolded(dotExpr, objectType); + } + + case Expression.ExpressionKind.IDENTIFIER: + return ImmutableArray.Create(Infer(lhs)); + + default: + { + StarlarkType lhsType = Infer(lhs); + Errorf(lhs, "{0} of type '{1}' is not a valid target for assignment", lhs, lhsType); + return ImmutableArray.Create(Types.ANY); + } + } + } + + private void AssignSequence(ListExpression lhs, StarlarkType rhsType) + { + if (rhsType.Equals(Types.ANY)) + { + foreach (Expression element in lhs.GetElements()) + { + Assign(element, Types.ANY); + } + return; + } + + // We effectively need to transform what may be a union of iterables into a fixed-length + // tuple of unions. + IReadOnlyCollection rhsUnionElements = Types.UnfoldUnion(rhsType); + foreach (StarlarkType rhsUnionElement in rhsUnionElements) + { + if (rhsUnionElement is Types.FixedLengthTupleType rhsTuple) + { + if (lhs.GetElements().Count != rhsTuple.GetElementTypes().Count) + { + Errorf( + lhs, + "cannot assign type '{0}' to '{1}'; want {2}-element sequence", + rhsType, + lhs, + lhs.GetElements().Count); + return; + } + } + else if (!rhsUnionElement.Equals(Types.ANY) + && rhsUnionElement is not Types.AbstractCollectionType) + { + Errorf(lhs, "cannot assign non-iterable type '{0}' to '{1}'", rhsType, lhs); + return; + } + } + for (int i = 0; i < lhs.GetElements().Count; i++) + { + var rhsElementTypes = new List(rhsUnionElements.Count); + foreach (StarlarkType rhsUnionElement in rhsUnionElements) + { + if (rhsUnionElement is Types.FixedLengthTupleType rhsTuple) + { + rhsElementTypes.Add(rhsTuple.GetElementTypes()[i]); + } + else if (rhsUnionElement is Types.AbstractCollectionType rhsCollection) + { + rhsElementTypes.Add(rhsCollection.GetElementType()); + } + else if (rhsUnionElement.Equals(Types.ANY)) + { + rhsElementTypes.Add(Types.ANY); + } + } + Assign(lhs.GetElements()[i], Types.Union(rhsElementTypes)); + } + } + + public override void Visit(StarlarkFile file) + { + CheckState( + functionStack.Count == 0, + "When type-checking a StarlarkFile, functionStack is expected to be initially empty"); + Resolver.Function toplevel = file.GetResolvedFunction()!; + Push(toplevel); + base.Visit(file); + CheckState(Pop().Equals(toplevel)); + } + + // Expressions should only be visited via infer(), not the visit() dispatch mechanism. + // Override Visit(Identifier) as a poison pill. + public override void Visit(Identifier id) + { + throw new InvalidOperationException( + string.Format( + "TypeChecker.Visit should not have reached Identifier node '{0}'", id.GetName())); + } + + public override void Visit(AssignmentStatement assignment) + { + if (!UsesTypeSyntax()) + { + return; + } + + if (assignment.IsAugmented()) + { + TokenKind op = assignment.GetOperator()!.Value; + Location operatorLocation = assignment.GetOperatorLocation(); + Expression lhs = assignment.GetLHS(); + Expression rhs = assignment.GetRHS(); + ImmutableArray lhsMeet = InferIndividualAssignmentTarget(lhs); + StarlarkType rhsType = Infer(assignment.GetRHS()); + foreach (StarlarkType lhsType in lhsMeet) + { + StarlarkType resultType = + InferBinaryOperator( + lhs, + lhsType, + op, + operatorLocation, + rhs, + rhsType, + /* augmentedAssignment= */ true); + if (!StarlarkType.AssignableFrom(lhsType, resultType)) + { + BinaryOperatorError( + lhsType, + op, + operatorLocation, + rhsType, + /* augmentedAssignment= */ true, + string.Format( + "cannot update {0} with a result value of type '{1}'", + FormatExprWithMeetType(lhs, lhsMeet), + resultType)); + } + } + } + else + { + var rhsType = Infer(assignment.GetRHS()); + Assign(assignment.GetLHS(), rhsType); + } + } + + public override void Visit(ForStatement node) + { + if (UsesTypeSyntax()) + { + CheckForClause(node.GetVars(), node.GetCollection(), "'for' loop"); + } + // Visit the for loop body even in untyped code; it may contain nested typed def statements. + VisitBlock(node.GetBody()); + } + + public override void Visit(DefStatement def) + { + Resolver.Function function = def.GetResolvedFunction()!; + Push(function); + if (function.UsesTypeSyntax()) + { + Types.CallableType callableType = CheckNotNull(function.GetFunctionType()); + int numOrdinaryParams = callableType.GetParameterTypes().Count; + for (int i = 0; i < numOrdinaryParams; i++) + { + Parameter param = def.GetParameters()[i]; + if (param.GetDefaultValue() != null) + { + StarlarkType defaultValueType = Infer(param.GetDefaultValue()!); + if (!StarlarkType.AssignableFrom( + callableType.GetParameterTypeByPos(i), defaultValueType)) + { + Errorf( + param.GetDefaultValue()!.GetStartLocation(), + "{0}(): parameter '{1}' has default value of type '{2}', declares '{3}'", + def.GetIdentifier().GetName(), + param.GetName(), + defaultValueType, + callableType.GetParameterTypeByPos(i)); + } + } + } + + Statement? implicitNoneReturn = GetImplicitNoneReturn(def.GetBody()); + if (implicitNoneReturn != null + && !StarlarkType.AssignableFrom(callableType.GetReturnType(), Types.NONE)) + { + Errorf( + implicitNoneReturn, + "{0}() declares return type '{1}' but may exit without an explicit 'return'", + def.GetIdentifier().GetName(), + callableType.GetReturnType()); + } + } + + // Visit body even in untyped code; it may contain nested typed def statements. + VisitBlock(def.GetBody()); + CheckState(Poll() == function); + } + + public override void Visit(IfStatement node) + { + if (UsesTypeSyntax()) + { + // Check type constraints in the condition. + Infer(node.GetCondition()); + } + // Visit then/else blocks even in untyped code; they may contain nested typed def statements. + VisitBlock(node.GetThenBlock()); + if (node.GetElseBlock() != null) + { + VisitBlock(node.GetElseBlock()!); + } + } + + public override void Visit(ExpressionStatement expr) + { + if (!UsesTypeSyntax()) + { + return; + } + // Check constraints in the expression, but ignore the resulting type. + // Don't dispatch to it via visit(). + Infer(expr.GetExpression()); + } + + // No need to override visit() for FlowStatement. + + public override void Visit(LoadStatement load) + { + // Don't descend into children. + } + + public override void Visit(ReturnStatement ret) + { + if (!UsesTypeSyntax()) + { + return; + } + StarlarkType returnType = ret.GetResult() == null ? Types.NONE : Infer(ret.GetResult()!); + CheckState(functionStack.Count != 0); + Resolver.Function function = Peek(); + Types.CallableType callableType = function.GetFunctionType()!; + if (!StarlarkType.AssignableFrom(callableType.GetReturnType(), returnType)) + { + Errorf( + ret.GetResult()!.GetStartLocation(), + "{0}() declares return type '{1}' but may return '{2}'", + function.GetName(), + callableType.GetReturnType(), + returnType); + } + } + + public override void Visit(TypeAliasStatement alias) + { + // Don't descend into children. + } + + public override void Visit(VarStatement var) + { + // Don't descend into children. + } + + /// + /// Heuristically checks whether a function body ends with an implicit None return, i.e. a + /// non-return statement, and if so, retrieves the statement after which the implicit None + /// return occurs. Recurses into if statement bodies. + /// + private static Statement? GetImplicitNoneReturn(IReadOnlyList body) + { + Statement last = body[body.Count - 1]; + if (last is ReturnStatement) + { + return null; + } + else if (last is IfStatement ifStmt) + { + // An if statement is considered to have an explicit return if it has both `then` and + // `else` branches, and both branches end with an explicit return. + if (ifStmt.GetElseBlock() == null) + { + return ifStmt; + } + Statement? thenImplicitNoneReturn = GetImplicitNoneReturn(ifStmt.GetThenBlock()); + return thenImplicitNoneReturn != null + ? thenImplicitNoneReturn + : GetImplicitNoneReturn(ifStmt.GetElseBlock()!); + } + return last; + } + + /// + /// Returns true if the current function is considered to use type syntax, or if we were invoked + /// via . If false, the current node must not be type-checked. + /// + private bool UsesTypeSyntax() + { + return functionStack.Count == 0 || Peek().UsesTypeSyntax(); + } + + /// + /// Checks that the given file's AST satisfies the types in the bindings of its identifiers. + /// + public static void CheckFile(StarlarkFile file, TypeContext typeContext) + { + var checker = new TypeChecker(file.errors, typeContext); + checker.Visit(file); + } + + // ==== helpers ==== + + private static void CheckState(bool cond) + { + if (!cond) + { + throw new InvalidOperationException(); + } + } + + private static void CheckState(bool cond, string message) + { + if (!cond) + { + throw new InvalidOperationException(message); + } + } + + private static T CheckNotNull(T? value) where T : class + { + return value ?? throw new ArgumentNullException(nameof(value)); + } + + // ==== Stack helpers (top == last element, matching Java ArrayDeque push/peek/pop/poll) ==== + + private void Push(Resolver.Function f) => functionStack.Add(f); + + private Resolver.Function Peek() => functionStack[functionStack.Count - 1]; + + private Resolver.Function Pop() + { + Resolver.Function f = functionStack[functionStack.Count - 1]; + functionStack.RemoveAt(functionStack.Count - 1); + return f; + } + + private Resolver.Function Poll() + { + // Java ArrayDeque.poll() retrieves and removes the head (top). Returns null if empty, but + // it's never empty at the call site. + return Pop(); + } +} diff --git a/src/Starlark/Syntax/TypeTagger.cs b/src/Starlark/Syntax/TypeTagger.cs new file mode 100644 index 000000000..c73e7077d --- /dev/null +++ b/src/Starlark/Syntax/TypeTagger.cs @@ -0,0 +1,524 @@ +// Copyright 2025 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Linq; +using Starlark.Spelling; + +namespace Starlark.Syntax; + +/// +/// A visitor for tagging the data structures of a resolved file with type information. +/// +/// This populates the function type on the objects in the AST +/// and records whether or not a given is considered to use static +/// type syntax; populates the variable types on the objects; and +/// populates the Starlark type stored in s. These type fields must all +/// be null prior to running the visitor. +/// +/// The types assigned to the fields are based solely on the type annotations in the program. +/// No type inference is done here. +/// +/// Only a file that has passed the Resolver without errors should be run through this +/// visitor. +/// +public sealed class TypeTagger : NodeVisitor +{ + private readonly Resolver.IModule module; + + private readonly List errors; + + // Empty if we are tagging a type expression (inside which no function definitions are allowed). + // Populated and mutated by visitation. Used as a stack: top is the most recently pushed. + private readonly List functionStack = new(); + + // Formats and reports an error at the start of the specified node. + private void Errorf(Node node, string format, params object?[] args) + { + Errorf(node.GetStartLocation(), format, args); + } + + // Formats and reports an error at the specified location. + private void Errorf(Location loc, string format, params object?[] args) + { + errors.Add(new SyntaxError(loc, string.Format(format, args))); + } + + private TypeTagger(List errors, Resolver.IModule module) + { + this.errors = errors; + this.module = module; + } + + private TypeTagger(List errors, Resolver.IModule module, Resolver.Function toplevel) + : this(errors, module) + { + functionStack.Add(toplevel); + } + + /// + /// Given an identifier denoting a type constructor, obtains the type constructor from the module. + /// + /// If no match, logs an error at the given node and returns null. + /// + private TypeConstructor? ResolveTypeConstructor(Identifier id) + { + string name = id.GetName(); + + var scope = id.GetBinding()!.GetScope(); + if (!(scope == Resolver.Scope.UNIVERSAL + || scope == Resolver.Scope.PREDECLARED + || scope == Resolver.Scope.GLOBAL)) + { + // Local names cannot be types. Don't allow `x: Foo` to succeed if Foo is a local + // shadowing a type name. + Errorf(id, "local symbol '{0}' cannot be used as a type", name); + return null; + } + + try + { + TypeConstructor? constructor = module.GetTypeConstructor(name); + if (constructor == null) + { + Errorf(id, "{0} symbol '{1}' cannot be used as a type", Resolver.ScopeToString(scope), name); + return null; + } + return constructor; + } + catch (Resolver.Undefined ex) + { + string suggestion = + ex.Candidates != null ? SpellChecker.DidYouMean(name, ex.Candidates) : ""; + Errorf(id, "{0}{1}", ex.Message, suggestion); + return null; + } + } + + private TypeConstructor.Arg ExtractArg(Expression expr) + { + switch (expr.Kind) + { + case Expression.ExpressionKind.BINARY_OPERATOR: + { + // Syntax sugar for union types, i.e. a|b == Union[a,b] + var binop = (BinaryOperatorExpression)expr; + if (binop.GetOperator() == TokenKind.PIPE) + { + StarlarkType x = ExtractType(binop.GetX()); + StarlarkType y = ExtractType(binop.GetY()); + return Types.Union(x, y); + } + Errorf(expr, "binary operator '{0}' is not supported", binop.GetOperator().ToDisplayString()); + return Types.ANY; + } + + case Expression.ExpressionKind.TYPE_APPLICATION: + { + var app = (TypeApplication)expr; + + TypeConstructor? constructor = ResolveTypeConstructor(app.GetConstructor()); + if (constructor == null) + { + return Types.ANY; + } + IReadOnlyList arguments = + app.GetArguments().Select(ExtractArg).ToList(); + + try + { + return constructor.CreateStarlarkType(arguments); + } + catch (TypeConstructor.Failure e) + { + Errorf(expr, "{0}", e.Message); + return Types.ANY; + } + } + + case Expression.ExpressionKind.IDENTIFIER: + { + TypeConstructor? constructor = ResolveTypeConstructor((Identifier)expr); + if (constructor == null) + { + return Types.ANY; + } + try + { + return constructor.CreateStarlarkType( + System.Array.Empty()); + } + catch (TypeConstructor.Failure e) + { + Errorf(expr, "{0}", e.Message); + return Types.ANY; + } + } + + case Expression.ExpressionKind.ELLIPSIS: + { + return TypeConstructor.Arg.ELLIPSIS; + } + + case Expression.ExpressionKind.LIST_EXPR: + { + var listExpr = (ListExpression)expr; + if (listExpr.IsTuple() && listExpr.GetElements().Count == 0) + { + return TypeConstructor.Arg.EMPTY_TUPLE; + } + break; + } + + default: + // fall through + break; + } + + // TODO(ilist@): full evaluation: lists and dicts + Errorf(expr, "unexpected expression '{0}'", expr); + return Types.ANY; + } + + private StarlarkType ExtractType(Expression expr) + { + TypeConstructor.Arg arg = ExtractArg(expr); + if (arg is not StarlarkType type) + { + Errorf(expr, "expression '{0}' is not a valid type.", expr); + return Types.ANY; + } + return type; + } + + /// + /// Statically evaluates a type expression to the it denotes. + /// + /// + /// if expr is not a type expression or if it could not be evaluated to a type. + /// + public static StarlarkType ExtractType(Expression expr, Resolver.IModule module) + { + var errors = new List(); + var r = new TypeTagger(errors, module); + StarlarkType result = r.ExtractType(expr); + if (errors.Count != 0) + { + throw new SyntaxError.Exception(r.errors); + } + return result; + } + + private Types.CallableType CreateFunctionType( + IReadOnlyList parameters, Expression? returnTypeExpr) + { + var names = ImmutableArray.CreateBuilder(); + var types = ImmutableArray.CreateBuilder(); + var mandatoryParameters = ImmutableHashSet.CreateBuilder(); + + int nparams = parameters.Count; + int numPositionalParameters = 0; + Parameter.Star? star = null; + Parameter.StarStar? starStar = null; + for (int i = 0; i < nparams; i++) + { + Parameter param = parameters[i]; + if (param is Parameter.Star pstar) + { + star = pstar; + continue; + } + if (param is Parameter.StarStar pstarstar) + { + starStar = pstarstar; + continue; + } + if (star == null) + { + numPositionalParameters++; + } + + string name = param.GetName()!; + Expression? typeExpr = param.GetType(); + + names.Add(name); + types.Add(typeExpr == null ? Types.ANY : ExtractType(typeExpr)); + if (param is Parameter.Mandatory) + { + mandatoryParameters.Add(name); + } + } + + StarlarkType? varargsType = null; + if (star != null && star.GetIdentifier() != null) + { + Expression? typeExpr = star.GetType(); + varargsType = typeExpr == null ? Types.ANY : ExtractType(typeExpr); + } + + StarlarkType? kwargsType = null; + if (starStar != null) + { + Expression? typeExpr = starStar.GetType(); + kwargsType = typeExpr == null ? Types.ANY : ExtractType(typeExpr); + } + + StarlarkType returnType = Types.ANY; + if (returnTypeExpr != null) + { + returnType = ExtractType(returnTypeExpr); + } + + return Types.Callable( + names.ToImmutable(), + types.ToImmutable(), + /* numPositionalOnlyParameters= */ 0, + numPositionalParameters, + mandatoryParameters.ToImmutable(), + varargsType, + kwargsType, + returnType); + } + + /// + /// Sets an identifier's type. + /// + /// The Binding on the identifier must have already been set by the resolver. + /// + /// Logs an error if the identifier is not the first binding occurrence of the + /// Binding. In this case, the type is not updated. + /// + private void SetType(Node node, Identifier id, StarlarkType type) + { + Resolver.Binding? binding = id.GetBinding(); + if (binding == null) + { + throw new ArgumentNullException( + nameof(id), string.Format("no binding set on identifier '{0}'", id.GetName())); + } + + if (binding.GetFirst() != id) + { + if (node is DefStatement) + { + // A def statement appearing in typed code constitutes an implicit type annotation on + // the function identifier's symbol. + Errorf(id, "function '{0}' was previously declared", id.GetName()); + } + else + { + Errorf(id, "type annotation on '{0}' may only appear at its declaration", id.GetName()); + } + if (binding.IsSyntactic()) + { + Errorf(binding.GetFirst(), "'{0}' previously declared here", id.GetName()); + } + return; + } + + if (binding.GetType() != null) + { + throw new ArgumentException( + string.Format( + "Expected type of binding {0} to be null but was {1}", binding, binding.GetType())); + } + binding.SetType(type); + } + + /// + /// Sets a resolved function's type. + /// + private static void SetType(Resolver.Function resolved, Types.CallableType type) + { + if (resolved == null) + { + throw new ArgumentNullException(nameof(resolved)); + } + if (resolved.GetFunctionType() != null) + { + throw new ArgumentException( + string.Format( + "Expected type of resolved function {0} to be null but was {1}", + resolved.GetName(), + resolved.GetFunctionType())); + } + resolved.SetFunctionType(type); + } + + public override void Visit(StarlarkFile file) + { + if (functionStack.Count != 0) + { + throw new InvalidOperationException( + "When tagging a StarlarkFile, functionStack is expected to be initially empty"); + } + Resolver.Function toplevel = file.GetResolvedFunction()!; + Push(toplevel); + base.Visit(file); + if (!Pop().Equals(toplevel)) + { + throw new InvalidOperationException(); + } + } + + public override void Visit(AssignmentStatement assignment) + { + if (assignment.GetType() != null) + { + SetUsesTypeSyntax(); + StarlarkType type = ExtractType(assignment.GetType()!); + SetType(assignment, (Identifier)assignment.GetLHS(), type); + } + + // Traverse children; RHS could contain a lambda. + base.Visit(assignment); + } + + public override void Visit(DefStatement def) + { + Resolver.Function resolvedFunction = def.GetResolvedFunction()!; + Push(resolvedFunction); + Types.CallableType type = CreateFunctionType(def.GetParameters(), def.GetReturnType()); + SetType(resolvedFunction, type); + SetType(def, def.GetIdentifier(), type); + // Parameter types handled by Visit(Parameter). + if (def.GetReturnType() != null || def.GetTypeParameters().Count != 0) + { + SetUsesTypeSyntax(); + } + + base.Visit(def); + if (!Pop().Equals(resolvedFunction)) + { + throw new InvalidOperationException(); + } + } + + public override void Visit(Parameter param) + { + if (param.GetIdentifier() != null) + { + // Default to ANY for unannotated params. This matches the behavior for the + // Resolver.Function's type. + StarlarkType type = Types.ANY; + if (param.GetType() != null) + { + SetUsesTypeSyntax(); + type = ExtractType(param.GetType()!); + } + SetType(param, param.GetIdentifier()!, type); + } + + base.Visit(param); + } + + public override void Visit(TypeAliasStatement node) + { + SetUsesTypeSyntax(); + base.Visit(node); + } + + public override void Visit(VarStatement var) + { + StarlarkType type = ExtractType(var.GetType()); + SetType(var, var.GetIdentifier(), type); + SetUsesTypeSyntax(); + + // No need to descend into type expression child. + } + + public override void Visit(CastExpression cast) + { + SetUsesTypeSyntax(); + cast.SetStarlarkType(ExtractType(cast.GetType())); + base.Visit(cast); + } + + public override void Visit(LambdaExpression lambda) + { + Types.CallableType type = + CreateFunctionType(lambda.GetParameters(), /* returnTypeExpr= */ null); + SetType(lambda.GetResolvedFunction()!, type); + + base.Visit(lambda); + } + + /// + /// Sets the Starlark types of the s and + /// s in the given AST (which must have already been processed by + /// ), based on the supplied annotations. + /// + public static void TagFile(StarlarkFile file, Resolver.IModule module) + { + var r = new TypeTagger(file.errors, module); + r.Visit(file); + } + + /// + /// Same as , but for an individual expression. + /// + /// on tagging error. + public static void TagExpr(Expression expr, Resolver.Function function, Resolver.IModule module) + { + var errors = new List(); + var r = new TypeTagger(errors, module, function); + + r.Visit(expr); + + if (errors.Count != 0) + { + throw new SyntaxError.Exception(errors); + } + } + + /// + /// Sets the Starlark type on a that the resolver generated to + /// wrap an expression. + /// + public static void TagExprFunction(Resolver.Function function, StarlarkType exprType) + { + Types.CallableType functionType = + Types.Callable( + /* parameterNames= */ ImmutableArray.Empty, + /* parameterTypes= */ ImmutableArray.Empty, + /* numPositionalOnlyParameters= */ 0, + /* numPositionalParameters= */ 0, + /* mandatoryParams= */ ImmutableHashSet.Empty, + /* varargsType= */ null, + /* kwargsType= */ null, + /* returns= */ exprType); + SetType(function, functionType); + } + + private void SetUsesTypeSyntax() + { + // If anything in the file (or in the expr if TypeTagger is invoked via tagExpr()) uses type + // syntax, the toplevel is considered to use type syntax. + functionStack[0].SetUsesTypeSyntax(); + // If anything nested in the most proximate def statement uses type syntax, the def statement + // is considered to use type syntax. + functionStack[functionStack.Count - 1].SetUsesTypeSyntax(); + } + + // ==== Stack helpers (top == last element, matching Java ArrayDeque push/peek/pop) ==== + + private void Push(Resolver.Function f) => functionStack.Add(f); + + private Resolver.Function Pop() + { + Resolver.Function f = functionStack[functionStack.Count - 1]; + functionStack.RemoveAt(functionStack.Count - 1); + return f; + } +} diff --git a/src/Starlark/Syntax/Types.cs b/src/Starlark/Syntax/Types.cs new file mode 100644 index 000000000..460d93b8e --- /dev/null +++ b/src/Starlark/Syntax/Types.cs @@ -0,0 +1,1289 @@ +// Copyright 2025 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using System.Linq; + +namespace Starlark.Syntax; + +/// +/// Definitions of types. +/// +/// +/// t1, t2 ::= None | bool | int | float | str | object +/// | t1|t2 | list[t1] +/// +/// +public static class Types +{ + /// + /// The Dynamic type of gradual typing; compatible with any other type, but not related by + /// subtyping to any other type. + /// + public static readonly StarlarkType ANY = new AnyType(); + + /// The top type of the type hierarchy. + public static readonly StarlarkType OBJECT = new ObjectType(); + + /// The bottom type of the type hierarchy. + public static readonly StarlarkType NEVER = new NeverType(); + + // Primitive types + public static readonly StarlarkType NONE = new NoneType(); + + public static readonly StarlarkType BOOL = new BoolType(); + public static readonly StarlarkType INT = new IntType(); + public static readonly StarlarkType FLOAT = new FloatType(); + public static readonly StarlarkType STR = new StrType(); + + // A frequently-used union `int | float`. + public static readonly UnionType NUMERIC = (UnionType)Union(INT, FLOAT); + + // A frequently-used empty tuple type. + public static readonly FixedLengthTupleType EMPTY_TUPLE = Tuple(ImmutableArray.Empty); + + // A frequently used function without parameters, that returns Any. + public static readonly CallableType NO_PARAMS_CALLABLE = + Callable( + ImmutableArray.Empty, + ImmutableArray.Empty, + 0, + 0, + ImmutableHashSet.Empty, + null, + null, + ANY); + + public static readonly TypeConstructor ANY_CONSTRUCTOR = WrapType("Any", ANY); + public static readonly TypeConstructor OBJECT_CONSTRUCTOR = WrapType("object", OBJECT); + public static readonly TypeConstructor NONE_CONSTRUCTOR = WrapType("None", NONE); + public static readonly TypeConstructor BOOL_CONSTRUCTOR = WrapType("bool", BOOL); + public static readonly TypeConstructor INT_CONSTRUCTOR = WrapType("int", INT); + public static readonly TypeConstructor FLOAT_CONSTRUCTOR = WrapType("float", FLOAT); + public static readonly TypeConstructor STR_CONSTRUCTOR = WrapType("str", STR); + public static readonly TypeConstructor LIST_CONSTRUCTOR = WrapTypeConstructor("list", List); + public static readonly TypeConstructor DICT_CONSTRUCTOR = WrapTypeConstructor("dict", Dict); + public static readonly TypeConstructor SET_CONSTRUCTOR = WrapTypeConstructor("set", Set); + public static readonly TypeConstructor TUPLE_CONSTRUCTOR = WrapTupleConstructor(); + + public static readonly TypeConstructor COLLECTION_CONSTRUCTOR = + WrapTypeConstructor("Collection", Collection); + + public static readonly TypeConstructor SEQUENCE_CONSTRUCTOR = + WrapTypeConstructor("Sequence", Sequence); + + public static readonly TypeConstructor MAPPING_CONSTRUCTOR = + WrapTypeConstructor("Mapping", Mapping); + + public static readonly ImmutableDictionary TYPE_UNIVERSE = + MakeTypeUniverse(); + + private static ImmutableDictionary MakeTypeUniverse() + { + var env = ImmutableDictionary.CreateBuilder(); + env.Add("Any", ANY_CONSTRUCTOR); + env.Add("object", OBJECT_CONSTRUCTOR); + env.Add("None", NONE_CONSTRUCTOR); + env.Add("bool", BOOL_CONSTRUCTOR); + env.Add("int", INT_CONSTRUCTOR); + env.Add("float", FLOAT_CONSTRUCTOR); + env.Add("str", STR_CONSTRUCTOR); + env.Add("list", LIST_CONSTRUCTOR); + env.Add("dict", DICT_CONSTRUCTOR); + env.Add("set", SET_CONSTRUCTOR); + env.Add("tuple", TUPLE_CONSTRUCTOR); + env.Add("Collection", COLLECTION_CONSTRUCTOR); + env.Add("Sequence", SEQUENCE_CONSTRUCTOR); + env.Add("Mapping", MAPPING_CONSTRUCTOR); + return env.ToImmutable(); + } + + // ==== Singleton primitive types ==== + + private sealed class AnyType : StarlarkType + { + public override string ToString() => "Any"; + + public override int GetHashCode() => typeof(AnyType).GetHashCode(); + + public override bool Equals(object? obj) => obj is AnyType; + + public override StarlarkType? GetField(string name, TypeContext context) => ANY; + + internal override StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType that, bool thisLeft) + { + return op switch + { + // If we are the LHS, fall through to RHS's inferBinaryOperator; RHS determines + // whether it is membership-testable. + // If we are the RHS, act as a membership-testable type that allows any LHS (e.g. + // list) and return bool. + TokenKind.IN or TokenKind.NOT_IN => thisLeft ? null : BOOL, + _ => ANY, + }; + } + + protected internal override bool IsComparable(StarlarkType that) + { + // Instead of enumerating all comparable types here, allow StarlarkType.comparable to + // defer to that.isComparable(ANY). + return that.Equals(ANY); + } + + public override bool HasSetIndex() => true; + + public override bool HasSetField() => true; + } + + private sealed class ObjectType : StarlarkType + { + public override string ToString() => "object"; + + public override int GetHashCode() => typeof(ObjectType).GetHashCode(); + + public override bool Equals(object? obj) => obj is ObjectType; + } + + private sealed class NeverType : StarlarkType + { + public override string ToString() => "Never"; + + public override int GetHashCode() => typeof(NeverType).GetHashCode(); + + public override bool Equals(object? obj) => obj is NeverType; + + protected internal override bool IsComparable(StarlarkType that) + { + // Regard Never - as the bottom type - to be comparable to anything; in particular, this + // allows empty lists (i.e. list[Never]) to be comparable to arbitrary non-empty lists. + return true; + } + + public override bool HasSetIndex() => true; + + public override bool HasSetField() => true; + } + + private sealed class NoneType : StarlarkType + { + public override string ToString() => "None"; + + public override int GetHashCode() => typeof(NoneType).GetHashCode(); + + public override bool Equals(object? obj) => obj is NoneType; + } + + private sealed class BoolType : StarlarkType + { + public override string ToString() => "bool"; + + public override int GetHashCode() => typeof(BoolType).GetHashCode(); + + public override bool Equals(object? obj) => obj is BoolType; + + protected internal override bool IsComparable(StarlarkType that) + { + return StarlarkType.AssignableFrom(BOOL, that); + } + } + + private sealed class IntType : StarlarkType + { + public override string ToString() => "int"; + + public override int GetHashCode() => typeof(IntType).GetHashCode(); + + public override bool Equals(object? obj) => obj is IntType; + + internal override StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType that, bool thisLeft) + { + return op switch + { + TokenKind.PLUS or TokenKind.MINUS or TokenKind.PERCENT or TokenKind.SLASH_SLASH => + NUMERIC.GetTypes().Contains(that) ? that : null, + TokenKind.SLASH => NUMERIC.GetTypes().Contains(that) ? FLOAT : null, + // Repetition operator (int * str, int * list, etc.) is assumed to be symmetric and + // implemented by the rhs, so defer to rhs for non-numeric case. + TokenKind.STAR => NUMERIC.GetTypes().Contains(that) ? that : null, + TokenKind.AMPERSAND or TokenKind.CARET or TokenKind.GREATER_GREATER + or TokenKind.LESS_LESS or TokenKind.PIPE => that.Equals(INT) ? INT : null, + _ => null, + }; + } + + protected internal override bool IsComparable(StarlarkType that) + { + return StarlarkType.AssignableFrom(NUMERIC, that); + } + } + + private sealed class FloatType : StarlarkType + { + public override string ToString() => "float"; + + public override int GetHashCode() => typeof(FloatType).GetHashCode(); + + public override bool Equals(object? obj) => obj is FloatType; + + internal override StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType that, bool thisLeft) + { + return op switch + { + TokenKind.PLUS or TokenKind.MINUS or TokenKind.PERCENT or TokenKind.SLASH + or TokenKind.SLASH_SLASH or TokenKind.STAR => + NUMERIC.GetTypes().Contains(that) ? FLOAT : null, + _ => null, + }; + } + + protected internal override bool IsComparable(StarlarkType that) + { + return StarlarkType.AssignableFrom(NUMERIC, that); + } + } + + private sealed class StrType : StarlarkType + { + public override string ToString() => "str"; + + public override int GetHashCode() => typeof(StrType).GetHashCode(); + + public override bool Equals(object? obj) => obj is StrType; + + internal override StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType that, bool thisLeft) + { + return op switch + { + TokenKind.PLUS => that.Equals(STR) ? STR : null, + // String substitution allows anything on the RHS + TokenKind.PERCENT => thisLeft ? STR : null, + TokenKind.STAR => that.Equals(INT) ? STR : null, + // If we are LHS, defer to the RHS. + // If we are RHS, explicitly handle Any since AnyType.inferBinaryOperator defers to us. + TokenKind.IN or TokenKind.NOT_IN => + !thisLeft && (that.Equals(STR) || that.Equals(ANY)) ? BOOL : null, + _ => null, + }; + } + + protected internal override bool IsComparable(StarlarkType that) + { + return that.Equals(STR) || that.Equals(ANY); + } + } + + // ==== Callable ==== + + /// Construct a CallableType representing a Starlark Function. + public static CallableType Callable( + IReadOnlyList parameterNames, + IReadOnlyList parameterTypes, + int numPositionalOnlyParameters, + int numPositionalParameters, + ImmutableHashSet mandatoryParams, + StarlarkType? varargsType, + StarlarkType? kwargsType, + StarlarkType returns) + { + if (parameterNames.Count != parameterTypes.Count) + { + throw new ArgumentException( + string.Format("{0} != {1}", parameterNames.Count, parameterTypes.Count)); + } + + return new GeneralCallableType( + parameterNames.ToImmutableArray(), + parameterTypes.ToImmutableArray(), + numPositionalOnlyParameters, + numPositionalParameters, + mandatoryParams, + varargsType, + kwargsType, + returns); + } + + /// + /// An interface for the general Starlark callable. + /// + /// There are 3 flavours of parameters: positional-only, ordinary, and keyword-only. + /// + public abstract class CallableType : StarlarkType + { + public abstract IReadOnlyList GetParameterNames(); + + public abstract IReadOnlyList GetParameterTypes(); + + public abstract int GetNumPositionalOnlyParameters(); + + public abstract int GetNumPositionalParameters(); + + public abstract ImmutableHashSet GetMandatoryParameters(); + + public abstract StarlarkType? GetVarargsType(); + + public abstract StarlarkType? GetKwargsType(); + + public abstract StarlarkType GetReturnType(); + + public StarlarkType GetParameterTypeByPos(int i) + { + return GetParameterTypes()[i]; + } + + public override string ToString() + { + // Approximate representation of the type - as much as Callable can do + return "Callable[[" + + string.Join(", ", GetParameterTypes().Select(t => t.ToString())) + + "], " + + GetReturnType() + + "]"; + } + + /// Returns a complete string representation of the type. + public string ToSignatureString() + { + var paramsList = new List(); + + // positional parameters + int i = 0; + for (; i < GetNumPositionalOnlyParameters(); i++) + { + string name = GetParameterNames()[i]; + StarlarkType type = GetParameterTypeByPos(i); + if (GetMandatoryParameters().Contains(name)) + { + paramsList.Add(type.ToString()!); + } + else + { + paramsList.Add("[" + type + "]"); + } + } + + if (i > 0) + { + // if there were positional-only parameters, we need to separate them + paramsList.Add("/"); + } + + for (; i < GetNumPositionalParameters(); i++) + { + string name = GetParameterNames()[i]; + StarlarkType type = GetParameterTypeByPos(i); + if (GetMandatoryParameters().Contains(name)) + { + paramsList.Add(name + ": " + type); + } + else + { + paramsList.Add(name + ": [" + type + "]"); + } + } + + if (GetVarargsType() != null) + { + paramsList.Add("*args: " + GetVarargsType()); + } + else if (i < GetParameterTypes().Count) + { + // if there are going to be kwonly params + paramsList.Add("*"); + } + + // keyword parameters + for (; i < GetParameterTypes().Count; i++) + { + string name = GetParameterNames()[i]; + string type = GetParameterTypeByPos(i).ToString()!; + if (GetMandatoryParameters().Contains(name)) + { + paramsList.Add(name + ": " + type); + } + else + { + paramsList.Add(name + ": [" + type + "]"); + } + } + + if (GetKwargsType() != null) + { + paramsList.Add("**kwargs: " + GetKwargsType()); + } + + return "(" + string.Join(", ", paramsList) + ") -> " + GetReturnType(); + } + } + + private sealed class GeneralCallableType : CallableType + { + private readonly ImmutableArray parameterNames; + private readonly ImmutableArray parameterTypes; + private readonly int numPositionalOnlyParameters; + private readonly int numPositionalParameters; + private readonly ImmutableHashSet mandatoryParameters; + private readonly StarlarkType? varargsType; + private readonly StarlarkType? kwargsType; + private readonly StarlarkType returnType; + + internal GeneralCallableType( + ImmutableArray parameterNames, + ImmutableArray parameterTypes, + int numPositionalOnlyParameters, + int numPositionalParameters, + ImmutableHashSet mandatoryParameters, + StarlarkType? varargsType, + StarlarkType? kwargsType, + StarlarkType returnType) + { + this.parameterNames = parameterNames; + this.parameterTypes = parameterTypes; + this.numPositionalOnlyParameters = numPositionalOnlyParameters; + this.numPositionalParameters = numPositionalParameters; + this.mandatoryParameters = mandatoryParameters; + this.varargsType = varargsType; + this.kwargsType = kwargsType; + this.returnType = returnType; + } + + public override IReadOnlyList GetParameterNames() => parameterNames; + + public override IReadOnlyList GetParameterTypes() => parameterTypes; + + public override int GetNumPositionalOnlyParameters() => numPositionalOnlyParameters; + + public override int GetNumPositionalParameters() => numPositionalParameters; + + public override ImmutableHashSet GetMandatoryParameters() => mandatoryParameters; + + public override StarlarkType? GetVarargsType() => varargsType; + + public override StarlarkType? GetKwargsType() => kwargsType; + + public override StarlarkType GetReturnType() => returnType; + + public override bool Equals(object? obj) + { + if (obj is not GeneralCallableType other) + { + return false; + } + + return parameterNames.SequenceEqual(other.parameterNames) + && parameterTypes.SequenceEqual(other.parameterTypes) + && numPositionalOnlyParameters == other.numPositionalOnlyParameters + && numPositionalParameters == other.numPositionalParameters + && mandatoryParameters.SetEquals(other.mandatoryParameters) + && Nullable.Equals(varargsType, other.varargsType) + && Nullable.Equals(kwargsType, other.kwargsType) + && returnType.Equals(other.returnType); + } + + public override int GetHashCode() + { + var hash = default(HashCode); + foreach (string n in parameterNames) + { + hash.Add(n); + } + foreach (StarlarkType t in parameterTypes) + { + hash.Add(t); + } + hash.Add(numPositionalOnlyParameters); + hash.Add(numPositionalParameters); + hash.Add(returnType); + hash.Add(varargsType); + hash.Add(kwargsType); + return hash.ToHashCode(); + } + } + + // ==== Union ==== + + /// + /// Constructs a union type. + /// + /// If the types set contains another Union type it's flattened. Duplicates are removed. + /// Occurrences of Never are removed. + /// + /// If types set contains Object type it's simplified to Object type. If the set contains a + /// single element, it is returned instead of constructing a union. And if the set is empty, + /// Never is returned. + /// + public static StarlarkType Union(params StarlarkType[] types) + { + return Union(ImmutableHashSet.CreateRange(types)); + } + + /// Constructs a union type. + public static StarlarkType Union(ImmutableHashSet types) + { + var subtypesBuilder = ImmutableHashSet.CreateBuilder(); + // Unions are flattened + foreach (StarlarkType type in types) + { + if (type is UnionType union) + { + subtypesBuilder.UnionWith(union.GetTypes()); + } + else if (!type.Equals(NEVER)) + { + subtypesBuilder.Add(type); + } + } + + ImmutableHashSet subtypes = subtypesBuilder.ToImmutable(); + if (subtypes.Contains(OBJECT)) + { + return OBJECT; + } + if (subtypes.Count == 1) + { + return subtypes.First(); + } + else if (subtypes.Count == 0) + { + return NEVER; + } + return new UnionType(subtypes); + } + + public static StarlarkType Union(IReadOnlyList types) + { + if (types.Count == 1) + { + // Optimize the common case. + return types[0]; + } + return Union(ImmutableHashSet.CreateRange(types)); + } + + /// + /// Returns the list of a union's types, or a singleton list if is not a + /// union. + /// + public static IReadOnlyCollection UnfoldUnion(StarlarkType type) + { + if (type is UnionType unionType) + { + return unionType.GetTypes(); + } + return ImmutableArray.Create(type); + } + + /// + /// Union type. + /// + /// Unions must contain at least two types, none of which may be Never or Object. See + /// . + /// + public sealed class UnionType : StarlarkType + { + private readonly ImmutableHashSet types; + + internal UnionType(ImmutableHashSet types) + { + this.types = types; + } + + public ImmutableHashSet GetTypes() => types; + + public override string ToString() + { + return string.Join("|", types.Select(t => t.ToString())); + } + + protected internal override bool IsComparable(StarlarkType that) + { + return types.All(type => StarlarkType.Comparable(type, that)); + } + + public override StarlarkType? GetField(string name, TypeContext context) + { + var resultTypes = new List(types.Count); + foreach (StarlarkType type in types) + { + StarlarkType? result = type.GetField(name, context); + if (result == null) + { + return null; + } + resultTypes.Add(result); + } + return Union(resultTypes); + } + + public override bool HasSetIndex() => types.All(t => t.HasSetIndex()); + + public override bool HasSetField() => types.All(t => t.HasSetField()); + + public override bool Equals(object? obj) + { + return obj is UnionType other && types.SetEquals(other.types); + } + + public override int GetHashCode() + { + // Order-independent hash. + int h = 0; + foreach (StarlarkType t in types) + { + h ^= t.GetHashCode(); + } + return h; + } + } + + // ==== List ==== + + public static ListType List(StarlarkType elementType) + { + return new ListType(elementType); + } + + /// List type. + public sealed class ListType : AbstractSequenceType + { + private readonly StarlarkType elementType; + + internal ListType(StarlarkType elementType) + { + this.elementType = elementType; + } + + public override StarlarkType GetElementType() => elementType; + + public override IReadOnlyList GetSupertypes() + { + return ImmutableArray.Create( + Sequence(GetElementType()), Collection(GetElementType())); + } + + public override string ToString() => "list[" + GetElementType() + "]"; + + internal override StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType that, bool thisLeft) + { + return op switch + { + TokenKind.PLUS => that is ListType thatList + ? List(Union(GetElementType(), thatList.GetElementType())) + : null, + TokenKind.STAR => that.Equals(INT) ? this : null, + _ => base.InferBinaryOperator(op, that, thisLeft), + }; + } + + public override StarlarkType? GetField(string name, TypeContext context) + { + return context.GetListFieldType(name); + } + + protected internal override bool IsComparable(StarlarkType that) + { + if (that.Equals(ANY)) + { + return true; + } + else if (that is ListType thatList) + { + return Comparable(GetElementType(), thatList.GetElementType()); + } + return false; + } + + public override bool HasSetIndex() => true; + + public override bool Equals(object? obj) + { + return obj is ListType other && elementType.Equals(other.elementType); + } + + public override int GetHashCode() => HashCode.Combine(typeof(ListType), elementType); + } + + // ==== Dict ==== + + public static DictType Dict(StarlarkType keyType, StarlarkType valueType) + { + return new DictType(keyType, valueType); + } + + /// Dict type. + public sealed class DictType : AbstractMappingType + { + private readonly StarlarkType keyType; + private readonly StarlarkType valueType; + + internal DictType(StarlarkType keyType, StarlarkType valueType) + { + this.keyType = keyType; + this.valueType = valueType; + } + + public override StarlarkType GetKeyType() => keyType; + + public override StarlarkType GetValueType() => valueType; + + public override IReadOnlyList GetSupertypes() + { + return ImmutableArray.Create( + Collection(GetKeyType()), Mapping(GetKeyType(), GetValueType())); + } + + public override string ToString() => "dict[" + GetKeyType() + ", " + GetValueType() + "]"; + + public override StarlarkType? GetField(string name, TypeContext context) + { + return context.GetDictFieldType(name); + } + + public override bool HasSetIndex() => true; + + public override bool Equals(object? obj) + { + return obj is DictType other + && keyType.Equals(other.keyType) + && valueType.Equals(other.valueType); + } + + public override int GetHashCode() => HashCode.Combine(typeof(DictType), keyType, valueType); + } + + // ==== Set ==== + + public static SetType Set(StarlarkType elementType) + { + return new SetType(elementType); + } + + /// Set type. + public sealed class SetType : AbstractCollectionType + { + private readonly StarlarkType elementType; + + internal SetType(StarlarkType elementType) + { + this.elementType = elementType; + } + + public override StarlarkType GetElementType() => elementType; + + public override IReadOnlyList GetSupertypes() + { + return ImmutableArray.Create(Collection(GetElementType())); + } + + public override string ToString() => "set[" + GetElementType() + "]"; + + public override StarlarkType? GetField(string name, TypeContext context) + { + return context.GetSetFieldType(name); + } + + internal override StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType that, bool thisLeft) + { + return op switch + { + // TODO: #27370 - we may want to tighten the type of a set intersection, but it's + // non-trivial. + TokenKind.AMPERSAND or TokenKind.MINUS => that is SetType ? this : null, + TokenKind.CARET or TokenKind.PIPE => that is SetType thatSet + ? Set(Union(GetElementType(), thatSet.GetElementType())) + : null, + _ => base.InferBinaryOperator(op, that, thisLeft), + }; + } + + public override bool Equals(object? obj) + { + return obj is SetType other && elementType.Equals(other.elementType); + } + + public override int GetHashCode() => HashCode.Combine(typeof(SetType), elementType); + } + + // ==== Tuple ==== + + public static FixedLengthTupleType Tuple(IReadOnlyList elementTypes) + { + return new FixedLengthTupleType(elementTypes.ToImmutableArray()); + } + + public static FixedLengthTupleType Tuple(StarlarkType first, params StarlarkType[] rest) + { + var builder = ImmutableArray.CreateBuilder(); + builder.Add(first); + builder.AddRange(rest); + return new FixedLengthTupleType(builder.ToImmutable()); + } + + public static HomogeneousTupleType HomogeneousTuple(StarlarkType elementType) + { + return new HomogeneousTupleType(elementType); + } + + /// Tuple type. + public abstract class TupleType : AbstractSequenceType + { + /// Returns the type of this tuple concatenated with another. + internal abstract TupleType Concatenate(TupleType rhs); + + /// Returns the type of this tuple repeated. + internal abstract TupleType Repeat(int times); + + /// Returns the homogeneous version of this tuple type. + public abstract HomogeneousTupleType ToHomogeneous(); + + internal override StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType that, bool thisLeft) + { + return op switch + { + TokenKind.PLUS => that is TupleType rhsTuple ? Concatenate(rhsTuple) : null, + // Special case handled by TypeChecker.inferTupleRepetition. + TokenKind.STAR => null, + _ => base.InferBinaryOperator(op, that, thisLeft), + }; + } + } + + /// Tuple type of a fixed length. + public sealed class FixedLengthTupleType : TupleType + { + private readonly ImmutableArray elementTypes; + + internal FixedLengthTupleType(ImmutableArray elementTypes) + { + this.elementTypes = elementTypes; + } + + public IReadOnlyList GetElementTypes() => elementTypes; + + public override StarlarkType GetElementType() => Union(elementTypes); + + public override IReadOnlyList GetSupertypes() + { + HomogeneousTupleType homogeneous = ToHomogeneous(); + return ImmutableArray.Create( + homogeneous, + Sequence(homogeneous.GetElementType()), + Collection(homogeneous.GetElementType())); + } + + public override string ToString() + { + return string.Format( + "tuple[{0}]", + elementTypes.IsEmpty + ? "()" + : string.Join(", ", elementTypes.Select(t => t.ToString()))); + } + + internal override TupleType Concatenate(TupleType rhs) + { + if (rhs is FixedLengthTupleType rhsFixedLength) + { + var builder = ImmutableArray.CreateBuilder(); + builder.AddRange(elementTypes); + builder.AddRange(rhsFixedLength.elementTypes); + return new FixedLengthTupleType(builder.ToImmutable()); + } + else + { + return ToHomogeneous().Concatenate(rhs); + } + } + + internal override TupleType Repeat(int times) + { + var builder = ImmutableArray.CreateBuilder(); + for (int i = 0; i < times; i++) + { + builder.AddRange(elementTypes); + } + return new FixedLengthTupleType(builder.ToImmutable()); + } + + public override HomogeneousTupleType ToHomogeneous() + { + return HomogeneousTuple(Union(elementTypes)); + } + + protected internal override bool IsComparable(StarlarkType that) + { + if (that.Equals(ANY)) + { + return true; + } + else if (that is FixedLengthTupleType thatTuple) + { + int commonLength = Math.Min(elementTypes.Length, thatTuple.elementTypes.Length); + for (int i = 0; i < commonLength; i++) + { + if (!Comparable(elementTypes[i], thatTuple.elementTypes[i])) + { + return false; + } + } + return true; + } + // Comparison with HomogeneousTupleType defers to HomogeneousTupleType. + return false; + } + + public override bool Equals(object? obj) + { + return obj is FixedLengthTupleType other && elementTypes.SequenceEqual(other.elementTypes); + } + + public override int GetHashCode() + { + var hash = default(HashCode); + hash.Add(typeof(FixedLengthTupleType)); + foreach (StarlarkType t in elementTypes) + { + hash.Add(t); + } + return hash.ToHashCode(); + } + } + + /// Tuple type of an indeterminate length. + public sealed class HomogeneousTupleType : TupleType + { + private readonly StarlarkType elementType; + + internal HomogeneousTupleType(StarlarkType elementType) + { + this.elementType = elementType; + } + + public override StarlarkType GetElementType() => elementType; + + public override IReadOnlyList GetSupertypes() + { + return ImmutableArray.Create( + Sequence(GetElementType()), Collection(GetElementType())); + } + + public override string ToString() => "tuple[" + GetElementType() + ", ...]"; + + internal override TupleType Concatenate(TupleType rhs) + { + return rhs is HomogeneousTupleType rhsHomogeneous + ? HomogeneousTuple(Union(GetElementType(), rhsHomogeneous.GetElementType())) + : Concatenate(rhs.ToHomogeneous()); + } + + internal override TupleType Repeat(int times) + { + return times > 0 ? this : EMPTY_TUPLE; + } + + public override HomogeneousTupleType ToHomogeneous() => this; + + protected internal override bool IsComparable(StarlarkType that) + { + if (that.Equals(ANY)) + { + return true; + } + else if (that is TupleType thatTuple) + { + return Comparable(GetElementType(), thatTuple.ToHomogeneous().GetElementType()); + } + return false; + } + + public override bool Equals(object? obj) + { + return obj is HomogeneousTupleType other && elementType.Equals(other.elementType); + } + + public override int GetHashCode() => HashCode.Combine(typeof(HomogeneousTupleType), elementType); + } + + // ==== Collection / Sequence / Mapping ==== + + /// Collection type. + public static CollectionType Collection(StarlarkType elementType) + { + return new CollectionType(elementType); + } + + /// Abstract collection type implementing common functionality. Exists to be subclassed. + public abstract class AbstractCollectionType : StarlarkType + { + public abstract StarlarkType GetElementType(); + + internal override StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType that, bool thisLeft) + { + return op switch + { + // `in` and `not in` are always valid for collections on the RHS. + TokenKind.IN or TokenKind.NOT_IN => thisLeft ? null : BOOL, + _ => null, + }; + } + } + + /// Collection type. + public sealed class CollectionType : AbstractCollectionType + { + private readonly StarlarkType elementType; + + internal CollectionType(StarlarkType elementType) + { + this.elementType = elementType; + } + + public override StarlarkType GetElementType() => elementType; + + public override string ToString() => "Collection[" + GetElementType() + "]"; + + public override bool Equals(object? obj) + { + return obj is CollectionType other && elementType.Equals(other.elementType); + } + + public override int GetHashCode() => HashCode.Combine(typeof(CollectionType), elementType); + } + + /// Sequence type. + public static SequenceType Sequence(StarlarkType elementType) + { + return new SequenceType(elementType); + } + + /// Abstract sequence type for common sequence functionality. Exists to be subclassed. + public abstract class AbstractSequenceType : AbstractCollectionType + { + public override IReadOnlyList GetSupertypes() + { + return ImmutableArray.Create(Collection(GetElementType())); + } + } + + /// Sequence type. + public sealed class SequenceType : AbstractSequenceType + { + private readonly StarlarkType elementType; + + internal SequenceType(StarlarkType elementType) + { + this.elementType = elementType; + } + + public override StarlarkType GetElementType() => elementType; + + public override string ToString() => "Sequence[" + GetElementType() + "]"; + + public override bool Equals(object? obj) + { + return obj is SequenceType other && elementType.Equals(other.elementType); + } + + public override int GetHashCode() => HashCode.Combine(typeof(SequenceType), elementType); + } + + /// Mapping type. + public static MappingType Mapping(StarlarkType keyType, StarlarkType valueType) + { + return new MappingType(keyType, valueType); + } + + /// Abstract mapping type for common map functionality. Exists to be subclassed. + public abstract class AbstractMappingType : AbstractCollectionType + { + public abstract StarlarkType GetKeyType(); + + public abstract StarlarkType GetValueType(); + + public override IReadOnlyList GetSupertypes() + { + return ImmutableArray.Create(Collection(GetKeyType())); + } + + public override StarlarkType GetElementType() => GetKeyType(); + + internal override StarlarkType? InferBinaryOperator(TokenKind op, StarlarkType rhs, bool thisLeft) + { + return op switch + { + TokenKind.PIPE => rhs is AbstractMappingType rhsMapping + ? Dict( + Union(GetKeyType(), rhsMapping.GetKeyType()), + Union(GetValueType(), rhsMapping.GetValueType())) + : null, + _ => base.InferBinaryOperator(op, rhs, thisLeft), + }; + } + } + + /// Mapping type. + public sealed class MappingType : AbstractMappingType + { + private readonly StarlarkType keyType; + private readonly StarlarkType valueType; + + internal MappingType(StarlarkType keyType, StarlarkType valueType) + { + this.keyType = keyType; + this.valueType = valueType; + } + + public override StarlarkType GetKeyType() => keyType; + + public override StarlarkType GetValueType() => valueType; + + public override string ToString() => "Mapping[" + GetKeyType() + ", " + GetValueType() + "]"; + + public override bool Equals(object? obj) + { + return obj is MappingType other + && keyType.Equals(other.keyType) + && valueType.Equals(other.valueType); + } + + public override int GetHashCode() => HashCode.Combine(typeof(MappingType), keyType, valueType); + } + + // ==== Type constructor factories ==== + + internal static TypeConstructor WrapType(string name, StarlarkType type) + { + return new DelegatingTypeConstructor(argsTuple => + { + if (argsTuple.Count != 0) + { + throw new TypeConstructor.Failure( + string.Format("'{0}' does not accept arguments", name)); + } + return type; + }); + } + + private static IReadOnlyList ToStarlarkTypes( + string name, IReadOnlyList args) + { + foreach (TypeConstructor.Arg arg in args) + { + if (arg is not StarlarkType) + { + throw new TypeConstructor.Failure( + string.Format("in application to {0}, got '{1}', expected a type", name, arg)); + } + } + return args.Cast().ToList(); + } + + /// + /// Returns a new type constructor wrapping the given one-argument type factory. + /// + internal static TypeConstructor WrapTypeConstructor( + string name, Func factory) + { + return new DelegatingTypeConstructor(args => + { + var types = ToStarlarkTypes(name, args); + return types.Count switch + { + 0 => factory(ANY), + 1 => factory(types[0]), + _ => throw new TypeConstructor.Failure( + string.Format("{0}[] accepts exactly 1 argument but got {1}", name, types.Count)), + }; + }); + } + + /// + /// Returns a new type constructor wrapping the given two-argument type factory. + /// + internal static TypeConstructor WrapTypeConstructor( + string name, Func factory) + { + return new DelegatingTypeConstructor(args => + { + var types = ToStarlarkTypes(name, args); + return types.Count switch + { + 0 => factory(ANY, ANY), + 2 => factory(types[0], types[1]), + _ => throw new TypeConstructor.Failure( + string.Format("{0}[] accepts exactly 2 arguments but got {1}", name, types.Count)), + }; + }); + } + + private static TypeConstructor WrapTupleConstructor() + { + // This is a function instead of a constant, so that the order of evaluation doesn't depend + // on the position in the class. + return new DelegatingTypeConstructor(args => + { + if (args.Count == 0) + { + // `tuple` is equivalent to `tuple[Any, ...]` + return HomogeneousTuple(ANY); + } + for (int i = 0; i < args.Count; i++) + { + TypeConstructor.Arg arg = args[i]; + if (arg.Equals(TypeConstructor.Arg.ELLIPSIS)) + { + if (i == 1 && args.Count == 2) + { + return HomogeneousTuple((StarlarkType)args[0]); + } + throw new TypeConstructor.Failure( + "in application to tuple, '...' can only appear as the second of exactly 2" + + " arguments, where the first argument is a type"); + } + else if (arg.Equals(TypeConstructor.Arg.EMPTY_TUPLE)) + { + if (args.Count == 1) + { + return EMPTY_TUPLE; + } + throw new TypeConstructor.Failure( + "in application to tuple, '()' can only appear if it is the only argument"); + } + else if (arg is not StarlarkType) + { + throw new TypeConstructor.Failure( + string.Format( + "in application to tuple, got '{0}', expected a type", arg)); + } + } + return Tuple(args.Cast().ToList()); + }); + } + + /// Adapts a delegate to the interface. + private sealed class DelegatingTypeConstructor : TypeConstructor + { + private readonly Func, StarlarkType> factory; + + internal DelegatingTypeConstructor( + Func, StarlarkType> factory) + { + this.factory = factory; + } + + public StarlarkType CreateStarlarkType(IReadOnlyList argsTuple) + { + return factory(argsTuple); + } + } +} diff --git a/src/Starlark/Syntax/VarStatement.cs b/src/Starlark/Syntax/VarStatement.cs index 28385898a..8ea3210ec 100644 --- a/src/Starlark/Syntax/VarStatement.cs +++ b/src/Starlark/Syntax/VarStatement.cs @@ -46,7 +46,7 @@ internal VarStatement( public Identifier GetIdentifier() => identifier; /// Returns the type expression associated with the variable. - public Expression GetType() => type; + public new Expression GetType() => type; /// Returns the Sphinx autodoc-style doc comments attached to this statement, if any. public DocComments? GetDocComments() => docComments; diff --git a/tests/Copybara.Tests/SmokeTests.cs b/tests/Copybara.Tests/SmokeTests.cs new file mode 100644 index 000000000..8c2afe188 --- /dev/null +++ b/tests/Copybara.Tests/SmokeTests.cs @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; +using FluentAssertions; +using Xunit; + +namespace Copybara.Tests; + +/// +/// Smoke tests exercising a slice of each ported foundation module, to prove the +/// port is wired together and behaves like the Java original for basic cases. +/// +public class SmokeTests +{ + [Fact] + public void Preconditions_CheckNotNull_ThrowsOnNull() + { + var act = () => Preconditions.CheckNotNull(null); + act.Should().Throw(); + } + + [Fact] + public void ValidationException_CheckCondition_FormatsPrintfStyle() + { + var act = () => ValidationException.CheckCondition(false, "bad %s value: %s", "foo", 42); + act.Should().Throw().WithMessage("bad foo value: 42"); + } + + [Fact] + public void Author_ParsesAndFormats() + { + var author = AuthorParser.Parse("Foo Bar "); + author.Name.Should().Be("Foo Bar"); + author.Email.Should().Be("foo@bar.com"); + author.ToString().Should().Be("Foo Bar "); + } + + [Fact] + public void Author_EqualityByEmail() + { + var a = new Author("Name One", "same@example.com"); + var b = new Author("Name Two", "same@example.com"); + a.Should().Be(b); // authors with the same non-empty email are equal + } + + [Fact] + public void ImmutableListMultimap_PreservesInsertionOrder() + { + var map = ImmutableListMultimap.CreateBuilder() + .Put("a", 1).Put("a", 2).Put("b", 3).Build(); + map["a"].Should().Equal(1, 2); + map["b"].Should().Equal(3); + map.ContainsEntry("a", 2).Should().BeTrue(); + } + + [Theory] + [InlineData("foo/bar.java", true)] + [InlineData("foo/deep/nested/bar.java", true)] + [InlineData("foo/bar.txt", false)] + public void Glob_IncludeExclude_Matches(string path, bool expected) + { + var glob = Glob.CreateGlob(new[] { "foo/**/*.java", "foo/*.java" }); + glob.RelativeTo("").Matches(path).Should().Be(expected); + } +} From b75ff87872e24be50e20192d7e786eafef7d1139 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 06:45:54 +0000 Subject: [PATCH 08/15] Port Starlark tree-walking evaluator, reflective dispatch, and builtins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Starlark now parses, resolves, and EXECUTES real code end-to-end: - Eval tree-walker (assignments, control flow, comprehensions, closures/cells) - Reflective [StarlarkMethod]/[Param] dispatch: MethodDescriptor, ParamDescriptor, CallUtils, BuiltinFunction, StarlarkFunction, plus Starlark.Call/Fastcall/ ExecFile/Eval drivers - MethodLibrary universe builtins (len/str/type/list/dict/range/sorted/min/max/…) and StringModule (join/split/replace/format/strip/…), registered into UNIVERSE Deferred: StarlarkSet, float builtin, flag-guarded params, dynamic type checks. Whole solution builds 0/0; dotnet test = 15 passed (7 new StarlarkEvalTests executing functions, comprehensions, string methods, and a closure counter). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- TODO.md | 12 +- src/Starlark/Eval/BuiltinFunction.cs | 227 ++++++ src/Starlark/Eval/CallUtils.cs | 99 +++ src/Starlark/Eval/Eval.cs | 813 ++++++++++++++++++++++ src/Starlark/Eval/FormatParser.cs | 102 +++ src/Starlark/Eval/MethodDescriptor.cs | 249 +++++++ src/Starlark/Eval/MethodLibrary.cs | 433 ++++++++++++ src/Starlark/Eval/ParamDescriptor.cs | 142 ++++ src/Starlark/Eval/StarlarkFunction.cs | 287 ++++++++ src/Starlark/Eval/StarlarkHelpers.cs | 37 +- src/Starlark/Eval/StarlarkRuntime.cs | 267 +++++++ src/Starlark/Eval/StarlarkThread.cs | 3 + src/Starlark/Eval/StringModule.cs | 602 ++++++++++++++++ tests/Copybara.Tests/StarlarkEvalTests.cs | 111 +++ 14 files changed, 3375 insertions(+), 9 deletions(-) create mode 100644 src/Starlark/Eval/BuiltinFunction.cs create mode 100644 src/Starlark/Eval/CallUtils.cs create mode 100644 src/Starlark/Eval/Eval.cs create mode 100644 src/Starlark/Eval/FormatParser.cs create mode 100644 src/Starlark/Eval/MethodDescriptor.cs create mode 100644 src/Starlark/Eval/MethodLibrary.cs create mode 100644 src/Starlark/Eval/ParamDescriptor.cs create mode 100644 src/Starlark/Eval/StarlarkFunction.cs create mode 100644 src/Starlark/Eval/StarlarkRuntime.cs create mode 100644 src/Starlark/Eval/StringModule.cs create mode 100644 tests/Copybara.Tests/StarlarkEvalTests.cs diff --git a/TODO.md b/TODO.md index 121c20f2e..57b6c0b05 100644 --- a/TODO.md +++ b/TODO.md @@ -64,10 +64,14 @@ builtins), `lib` (json, proto, etc.), `spelling`. `Sequence`), `Mutability`, `StarlarkSemantics`, `Printer`, `Module`, `StarlarkThread`, `EvalUtils`, `StarlarkValue`/`NoneType`/`EvalException`/ `Starlark` helpers, callable interfaces. - - ⬜ DEFERRED (critical for config loading): `Eval` tree-walking evaluator; - reflection dispatch (`CallUtils`/`MethodDescriptor`/`ParamDescriptor`/ - `BuiltinFunction`/`StarlarkFunction`) mapping `[StarlarkMethod]` → calls; - `MethodLibrary`/`StringModule` (string methods); `StarlarkSet`. + - ✅ Evaluator + dispatch: `Eval` tree-walker, reflective dispatch + (`CallUtils`/`MethodDescriptor`/`ParamDescriptor`/`BuiltinFunction`/ + `StarlarkFunction`) mapping `[StarlarkMethod]`→calls, `MethodLibrary` + + `StringModule` builtins, and `Starlark.ExecFile`/`Eval`/`Call` drivers. + Parse→resolve→execute of real Starlark works (functions, comprehensions, + closures, builtins, string methods) — verified by xUnit `StarlarkEvalTests`. + - ⬜ Still deferred: `StarlarkSet` + its EvalUtils operator branches, `float` + builtin, flag-guarded params, dynamic arg/return type-checking. - ✅ `spelling/` — `SpellChecker`. - ⬜ `lib/json` — `Json` module (interop with `System.Text.Json`). - ⬜ Reflection strategy: reflect over `[StarlarkMethod]`/`[Param]` at startup, diff --git a/src/Starlark/Eval/BuiltinFunction.cs b/src/Starlark/Eval/BuiltinFunction.cs new file mode 100644 index 000000000..bd44c0738 --- /dev/null +++ b/src/Starlark/Eval/BuiltinFunction.cs @@ -0,0 +1,227 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Starlark.Syntax; + +namespace Starlark.Eval; + +/// +/// A BuiltinFunction is a callable Starlark value that reflectively invokes a +/// -annotated method of a receiver. Port of +/// net.starlark.java.eval.BuiltinFunction. +/// +public sealed class BuiltinFunction : IStarlarkCallable +{ + private readonly object receiver; + private readonly string name; + private readonly MethodDescriptor desc; + + /// Constructs a BuiltinFunction bound to a receiver and descriptor. + public BuiltinFunction(object receiver, string name, MethodDescriptor desc) + { + this.receiver = receiver; + this.name = name; + this.desc = desc; + } + + /// Constructs a BuiltinFunction from a receiver and descriptor (name from descriptor). + public static BuiltinFunction Of(object receiver, MethodDescriptor desc) => + new(receiver, desc.Name, desc); + + public string Name => name; + + public MethodDescriptor Descriptor => desc; + + public bool IsImmutable() => true; + + public Location Location => Location.BUILTIN; + + public object? Fastcall(StarlarkThread thread, object?[] positional, object?[] named) + { + // String methods have StringModule.INSTANCE as the true receiver and the string as the + // first (self) positional parameter. + object recv = receiver; + if (receiver is string self) + { + recv = StringModule.INSTANCE; + var pos2 = new object?[positional.Length + 1]; + pos2[0] = self; + Array.Copy(positional, 0, pos2, 1, positional.Length); + positional = pos2; + } + + ParamDescriptor[] parameters = desc.Parameters; + int nparams = parameters.Length; + object?[] vector = new object?[nparams]; + + // Assign positional arguments. + int argIndex = 0; + int paramIndex = 0; + var varargs = new List(); + for (; argIndex < positional.Length && paramIndex < nparams; paramIndex++) + { + ParamDescriptor param = parameters[paramIndex]; + if (!param.IsPositional) + { + break; + } + object? value = positional[argIndex++]; + CheckParamValue(param, value); + vector[paramIndex] = value; + } + + // Surplus positionals. + if (desc.AcceptsExtraArgs) + { + for (; argIndex < positional.Length; argIndex++) + { + varargs.Add(positional[argIndex]); + } + } + else if (argIndex < positional.Length) + { + if (argIndex == 0) + { + throw Starlark.Errorf("{0}() got unexpected positional argument", name); + } + throw Starlark.Errorf( + "{0}() accepts no more than {1} positional argument{2} but got {3}", + name, argIndex, Plural(argIndex), positional.Length); + } + + // Assign named arguments. + Dict.Builder? kwargsBuilder = desc.AcceptsExtraKwargs ? new Dict.Builder() : null; + for (int i = 0; i < named.Length; i += 2) + { + string key = (string)named[i]!; + object? value = named[i + 1]; + int found = -1; + for (int p = 0; p < nparams; p++) + { + if (parameters[p].IsNamed && parameters[p].Name == key) + { + found = p; + break; + } + } + if (found >= 0) + { + if (vector[found] != null) + { + throw Starlark.Errorf("{0}() got multiple values for argument '{1}'", name, key); + } + CheckParamValue(parameters[found], value); + vector[found] = value; + } + else if (kwargsBuilder != null) + { + kwargsBuilder.Put(key, value); + } + else + { + throw Starlark.Errorf("{0}() got unexpected keyword argument '{1}'", name, key); + } + } + + // Apply defaults / report missing. + List? missingPositional = null; + List? missingNamed = null; + for (int p = 0; p < nparams; p++) + { + if (vector[p] != null) + { + continue; + } + ParamDescriptor param = parameters[p]; + object? def = param.DefaultValue; + if (def != null) + { + vector[p] = def; + } + else + { + if (param.IsPositional) + { + (missingPositional ??= new List()).Add(param.Name); + } + else + { + (missingNamed ??= new List()).Add(param.Name); + } + } + } + if (missingPositional != null) + { + throw Starlark.Errorf( + "{0}() missing {1} required positional argument{2}: {3}", + name, missingPositional.Count, Plural(missingPositional.Count), + string.Join(", ", missingPositional)); + } + if (missingNamed != null) + { + throw Starlark.Errorf( + "{0}() missing {1} required named argument{2}: {3}", + name, missingNamed.Count, Plural(missingNamed.Count), + string.Join(", ", missingNamed)); + } + + Tuple? varargsTuple = desc.AcceptsExtraArgs ? Tuple.CopyOf(varargs) : null; + Dict? kwargsDict = kwargsBuilder?.Build(thread.Mutability); + + return desc.Call(recv, vector, varargsTuple, kwargsDict, thread); + } + + public object? Call(StarlarkThread thread, Tuple args, Dict kwargs) + { + object?[] positional = args.ToArray(); + var named = new List(); + foreach (var e in kwargs.Entries) + { + named.Add(e.Key); + named.Add(e.Value); + } + return Fastcall(thread, positional, named.ToArray()); + } + + private void CheckParamValue(ParamDescriptor param, object? value) + { + IReadOnlyList? allowed = param.AllowedClasses; + if (allowed == null) + { + return; + } + if (param.IsNoneable && ReferenceEquals(value, Starlark.None)) + { + return; + } + foreach (Type t in allowed) + { + if (value != null && t.IsInstanceOfType(value)) + { + return; + } + if (t == typeof(NoneType) && ReferenceEquals(value, Starlark.None)) + { + return; + } + } + throw Starlark.Errorf( + "in call to {0}(), parameter '{1}' got value of type '{2}', want one of the allowed types", + name, param.Name, Starlark.Type(value)); + } + + private static string Plural(int n) => n == 1 ? "" : "s"; + + public override string ToString() => name; +} diff --git a/src/Starlark/Eval/CallUtils.cs b/src/Starlark/Eval/CallUtils.cs new file mode 100644 index 000000000..c49cb686a --- /dev/null +++ b/src/Starlark/Eval/CallUtils.cs @@ -0,0 +1,99 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Reflection; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// Helper functions for implementing reflective method dispatch of +/// -annotated methods. Port of net.starlark.java.eval.CallUtils. +/// +/// Descriptors are computed once per CLR and cached. Unlike Java, this port +/// does not vary descriptors by (flag-guarded methods are not +/// modeled), so a single per-type cache suffices. +/// +public static class CallUtils +{ + private sealed class ClassDescriptor + { + internal MethodDescriptor? SelfCall; + internal ImmutableDictionary Methods = + ImmutableDictionary.Empty; + } + + private static readonly ConcurrentDictionary Cache = new(); + + private static ClassDescriptor GetDescriptor(Type clazz) + { + // String methods live on StringModule. + if (clazz == typeof(string)) + { + clazz = typeof(StringModule); + } + return Cache.GetOrAdd(clazz, BuildClassDescriptor); + } + + private static ClassDescriptor BuildClassDescriptor(Type clazz) + { + var desc = new ClassDescriptor(); + var methods = ImmutableDictionary.CreateBuilder(); + + // Deterministic order: sort by name for stability. + var infos = clazz + .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) + .OrderBy(m => m.Name, StringComparer.Ordinal); + + foreach (MethodInfo m in infos) + { + var attr = m.GetCustomAttribute(inherit: true); + if (attr == null) + { + continue; + } + MethodDescriptor md = MethodDescriptor.Of(m, attr); + if (md.IsSelfCall) + { + desc.SelfCall ??= md; + continue; + } + // First declaration wins (base classes appear too via FlattenHierarchy). + if (!methods.ContainsKey(md.Name)) + { + methods[md.Name] = md; + } + } + + desc.Methods = methods.ToImmutable(); + return desc; + } + + /// + /// Returns a map of methods, keyed by Starlark name, of all + /// -annotated methods of the given class (excluding the selfCall method). + /// + public static ImmutableDictionary GetAnnotatedMethods(Type clazz) => + GetDescriptor(clazz).Methods; + + /// Returns the selfCall method descriptor of the given class, or null. + public static MethodDescriptor? GetSelfCallMethodDescriptor(Type clazz) => + GetDescriptor(clazz).SelfCall; + + /// Returns the names of the annotated (non-selfCall) methods of the class. + public static IReadOnlyCollection GetMethodNames(Type clazz) => + GetDescriptor(clazz).Methods.Keys.ToImmutableArray(); +} diff --git a/src/Starlark/Eval/Eval.cs b/src/Starlark/Eval/Eval.cs new file mode 100644 index 000000000..20fa56c11 --- /dev/null +++ b/src/Starlark/Eval/Eval.cs @@ -0,0 +1,813 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Numerics; +using Starlark.Syntax; + +namespace Starlark.Eval; + +/// +/// The tree-walking evaluator for Starlark statements and expressions. Port of +/// net.starlark.java.eval.Eval. +/// +internal static class Eval +{ + // Control flow signal returned by statement execution. + private const TokenKind PASS = TokenKind.PASS; + + /// + /// Executes the body of a StarlarkFunction. The frame at depth 0 is the callable's frame (pushed + /// by ); its locals are installed here. + /// + internal static object? ExecFunctionBody( + StarlarkThread thread, StarlarkFunction fn, object?[] locals, IReadOnlyList statements) + { + StarlarkThread.Frame fr = thread.FrameAt(0); + fr.Locals = locals; + fr.Result = Starlark.None; + ExecStatements(thread, fr, fn, statements); + return fr.Result; + } + + private static StarlarkFunction Fn(StarlarkThread.Frame fr) => (StarlarkFunction)fr.Fn; + + private static TokenKind ExecStatements( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, + IReadOnlyList statements) + { + for (int i = 0; i < statements.Count; i++) + { + TokenKind flow = Exec(thread, fr, fn, statements[i]); + if (flow != PASS) + { + return flow; + } + } + return PASS; + } + + private static TokenKind Exec( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, Statement st) + { + if (++thread.steps >= thread.stepLimit) + { + throw new EvalException("Starlark computation cancelled: too many steps"); + } + + switch (st.Kind) + { + case Statement.StatementKind.ASSIGNMENT: + ExecAssignment(thread, fr, fn, (AssignmentStatement)st); + return PASS; + case Statement.StatementKind.EXPRESSION: + Eval_(thread, fr, fn, ((ExpressionStatement)st).GetExpression()); + return PASS; + case Statement.StatementKind.FLOW: + return ((FlowStatement)st).GetFlowKind(); + case Statement.StatementKind.FOR: + return ExecFor(thread, fr, fn, (ForStatement)st); + case Statement.StatementKind.DEF: + { + var def = (DefStatement)st; + StarlarkFunction newFn = NewFunction(thread, fr, fn, def.GetResolvedFunction()!); + AssignIdentifier(fr, fn, def.GetIdentifier(), newFn); + return PASS; + } + case Statement.StatementKind.IF: + return ExecIf(thread, fr, fn, (IfStatement)st); + case Statement.StatementKind.LOAD: + ExecLoad(thread, fr, fn, (LoadStatement)st); + return PASS; + case Statement.StatementKind.RETURN: + return ExecReturn(thread, fr, fn, (ReturnStatement)st); + case Statement.StatementKind.TYPE_ALIAS: + case Statement.StatementKind.VAR: + return PASS; + } + throw new ArgumentException("unexpected statement: " + st.Kind); + } + + private static void ExecAssignment( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, AssignmentStatement node) + { + try + { + if (node.IsAugmented()) + { + ExecAugmentedAssignment(thread, fr, fn, node); + } + else + { + object? rvalue = Eval_(thread, fr, fn, node.GetRHS()); + Assign(thread, fr, fn, node.GetLHS(), rvalue); + } + } + catch (EvalException) + { + fr.SetErrorLocation(node.GetOperatorLocation()); + throw; + } + } + + private static TokenKind ExecFor( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, ForStatement node) + { + object? seqObj = Eval_(thread, fr, fn, node.GetCollection()); + IEnumerable seq; + try + { + seq = Starlark.ToIterable(seqObj); + } + catch (EvalException) + { + fr.SetErrorLocation(node.GetCollection().GetStartLocation()); + throw; + } + EvalUtils.AddIterator(seqObj); + try + { + foreach (object? item in seq) + { + Assign(thread, fr, fn, node.GetVars(), item); + switch (ExecStatements(thread, fr, fn, node.GetBody())) + { + case TokenKind.PASS: + case TokenKind.CONTINUE: + continue; + case TokenKind.BREAK: + return PASS; + case TokenKind.RETURN: + return TokenKind.RETURN; + default: + throw new InvalidOperationException("unreachable"); + } + } + } + catch (EvalException) + { + fr.SetErrorLocation(node.GetStartLocation()); + throw; + } + finally + { + EvalUtils.RemoveIterator(seqObj); + } + return PASS; + } + + private static TokenKind ExecIf( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, IfStatement node) + { + bool cond = Starlark.Truth(Eval_(thread, fr, fn, node.GetCondition())); + if (cond) + { + return ExecStatements(thread, fr, fn, node.GetThenBlock()); + } + if (node.GetElseBlock() != null) + { + return ExecStatements(thread, fr, fn, node.GetElseBlock()!); + } + return PASS; + } + + private static TokenKind ExecReturn( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, ReturnStatement node) + { + Expression? result = node.GetResult(); + if (result != null) + { + fr.Result = Eval_(thread, fr, fn, result); + } + return TokenKind.RETURN; + } + + private static void ExecLoad( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, LoadStatement node) + { + StarlarkThread.Loader? loader = thread.GetLoader(); + if (loader == null) + { + fr.SetErrorLocation(node.GetStartLocation()); + throw Starlark.Errorf("load statements may not be executed in this thread"); + } + string moduleName = node.GetImport().GetValue(); + Module? module = loader(moduleName); + if (module == null) + { + fr.SetErrorLocation(node.GetStartLocation()); + throw Starlark.Errorf("module '{0}' not found", moduleName); + } + foreach (LoadStatement.Binding binding in node.GetBindings()) + { + Identifier orig = binding.GetOriginalName(); + object? value = module.GetGlobal(orig.GetName()); + if (value == null) + { + fr.SetErrorLocation(orig.GetStartLocation()); + throw Starlark.Errorf( + "file '{0}' does not contain symbol '{1}'", moduleName, orig.GetName()); + } + AssignIdentifier(fr, fn, binding.GetLocalName(), value); + } + } + + // ---- Assignment ---- + + private static void Assign( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, Expression lhs, object? value) + { + switch (lhs) + { + case Identifier ident: + AssignIdentifier(fr, fn, ident, value); + break; + case IndexExpression index: + { + object? obj = Eval_(thread, fr, fn, index.GetObject()); + object? key = Eval_(thread, fr, fn, index.GetKey()); + EvalUtils.SetIndex(obj, key, value); + break; + } + case ListExpression list: + AssignSequence(thread, fr, fn, list.GetElements(), value); + break; + case DotExpression dot: + { + object? obj = Eval_(thread, fr, fn, dot.GetObject()); + string field = dot.GetField().GetName(); + try + { + EvalUtils.SetField(obj, field, value!); + } + catch (EvalException) + { + fr.SetErrorLocation(dot.GetDotLocation()); + throw; + } + break; + } + default: + throw Starlark.Errorf("cannot assign to '{0}'", lhs); + } + } + + private static void AssignIdentifier( + StarlarkThread.Frame fr, StarlarkFunction fn, Identifier id, object? value) + { + Resolver.Binding bind = id.GetBinding()!; + switch (bind.GetScope()) + { + case Resolver.Scope.LOCAL: + fr.Locals![bind.GetIndex()] = value; + break; + case Resolver.Scope.CELL: + ((StarlarkFunction.Cell)fr.Locals![bind.GetIndex()]!).X = value; + break; + case Resolver.Scope.GLOBAL: + fn.SetGlobal(bind.GetIndex(), value!); + break; + default: + throw new InvalidOperationException(bind.GetScope().ToString()); + } + } + + private static void AssignSequence( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, + IReadOnlyList lhs, object? x) + { + int nrhs = Starlark.Len(x); + int nlhs = lhs.Count; + if (nrhs < 0 || x is string) + { + throw Starlark.Errorf( + "got '{0}' in sequence assignment (want {1}-element sequence)", Starlark.Type(x), nlhs); + } + object?[] rhs = Starlark.ToArray(x); + if (nlhs != nrhs) + { + throw Starlark.Errorf( + "too {0} values to unpack (got {1}, want {2})", nrhs < nlhs ? "few" : "many", nrhs, nlhs); + } + for (int i = 0; i < nlhs; i++) + { + Assign(thread, fr, fn, lhs[i], rhs[i]); + } + } + + private static void ExecAugmentedAssignment( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, AssignmentStatement stmt) + { + Expression lhs = stmt.GetLHS(); + TokenKind op = stmt.GetOperator()!.Value; + Expression rhs = stmt.GetRHS(); + + if (lhs is Identifier ident) + { + object? x = Eval_(thread, fr, fn, lhs); + object? y = Eval_(thread, fr, fn, rhs); + object? z; + try + { + z = InplaceBinaryOp(thread, op, x, y); + } + catch (EvalException) + { + fr.SetErrorLocation(stmt.GetOperatorLocation()); + throw; + } + AssignIdentifier(fr, fn, ident, z); + } + else if (lhs is IndexExpression index) + { + object? obj = Eval_(thread, fr, fn, index.GetObject()); + object? key = Eval_(thread, fr, fn, index.GetKey()); + object? x = EvalUtils.Index(thread, obj, key!); + object? y = Eval_(thread, fr, fn, rhs); + object? z; + try + { + z = InplaceBinaryOp(thread, op, x, y); + EvalUtils.SetIndex(obj, key, z); + } + catch (EvalException) + { + fr.SetErrorLocation(stmt.GetOperatorLocation()); + throw; + } + } + else if (lhs is DotExpression dot) + { + object? obj = Eval_(thread, fr, fn, dot.GetObject()); + string field = dot.GetField().GetName(); + try + { + object? x = Starlark.GetAttr(thread, obj, field, null); + object? y = Eval_(thread, fr, fn, rhs); + object? z = InplaceBinaryOp(thread, op, x, y); + EvalUtils.SetField(obj, field, z!); + } + catch (EvalException) + { + fr.SetErrorLocation(dot.GetDotLocation()); + throw; + } + } + else + { + fr.SetErrorLocation(stmt.GetOperatorLocation()); + throw Starlark.Errorf("cannot perform augmented assignment on '{0}'", lhs); + } + } + + private static object? InplaceBinaryOp(StarlarkThread thread, TokenKind op, object? x, object? y) + { + if (op == TokenKind.PLUS && x is StarlarkList xList && y is StarlarkList yList) + { + xList.Extend(yList); + return xList; + } + if (op == TokenKind.PIPE && x is Dict xDict && y is Dict yDict) + { + xDict.PutEntries(yDict.Entries); + return xDict; + } + return EvalUtils.BinaryOp(op, x, y, thread); + } + + // ---- Expressions ---- + + private static object? Eval_( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, Expression expr) + { + if (++thread.steps >= thread.stepLimit) + { + throw new EvalException("Starlark computation cancelled: too many steps"); + } + + switch (expr.Kind) + { + case Expression.ExpressionKind.BINARY_OPERATOR: + return EvalBinaryOperator(thread, fr, fn, (BinaryOperatorExpression)expr); + case Expression.ExpressionKind.COMPREHENSION: + return EvalComprehension(thread, fr, fn, (Comprehension)expr); + case Expression.ExpressionKind.CONDITIONAL: + { + var cond = (ConditionalExpression)expr; + object? v = Eval_(thread, fr, fn, cond.GetCondition()); + return Eval_(thread, fr, fn, Starlark.Truth(v) ? cond.GetThenCase() : cond.GetElseCase()); + } + case Expression.ExpressionKind.DICT_EXPR: + return EvalDict(thread, fr, fn, (DictExpression)expr); + case Expression.ExpressionKind.DOT: + return EvalDot(thread, fr, fn, (DotExpression)expr); + case Expression.ExpressionKind.CALL: + return EvalCall(thread, fr, fn, (CallExpression)expr); + case Expression.ExpressionKind.CAST: + return Eval_(thread, fr, fn, ((CastExpression)expr).GetValue()); + case Expression.ExpressionKind.ISINSTANCE: + fr.SetErrorLocation(expr.GetStartLocation()); + throw new EvalException("isinstance() is not yet supported"); + case Expression.ExpressionKind.IDENTIFIER: + return EvalIdentifier(thread, fr, fn, (Identifier)expr); + case Expression.ExpressionKind.INDEX: + return EvalIndex(thread, fr, fn, (IndexExpression)expr); + case Expression.ExpressionKind.INT_LITERAL: + { + object n = ((IntLiteral)expr).GetValue(); + return n switch + { + int i => StarlarkInt.Of(i), + long l => StarlarkInt.Of(l), + BigInteger b => StarlarkInt.Of(b), + _ => throw new InvalidOperationException("bad int literal"), + }; + } + case Expression.ExpressionKind.FLOAT_LITERAL: + return StarlarkFloat.Of(((FloatLiteral)expr).GetValue()); + case Expression.ExpressionKind.LAMBDA: + return NewFunction(thread, fr, fn, ((LambdaExpression)expr).GetResolvedFunction()!); + case Expression.ExpressionKind.LIST_EXPR: + return EvalList(thread, fr, fn, (ListExpression)expr); + case Expression.ExpressionKind.SLICE: + return EvalSlice(thread, fr, fn, (SliceExpression)expr); + case Expression.ExpressionKind.STRING_LITERAL: + return ((StringLiteral)expr).GetValue(); + case Expression.ExpressionKind.UNARY_OPERATOR: + return EvalUnaryOperator(thread, fr, fn, (UnaryOperatorExpression)expr); + } + throw new ArgumentException("unexpected expression: " + expr.Kind); + } + + private static object? EvalBinaryOperator( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, BinaryOperatorExpression binop) + { + object? x = Eval_(thread, fr, fn, binop.GetX()); + switch (binop.GetOperator()) + { + case TokenKind.AND: + return Starlark.Truth(x) ? Eval_(thread, fr, fn, binop.GetY()) : x; + case TokenKind.OR: + return Starlark.Truth(x) ? x : Eval_(thread, fr, fn, binop.GetY()); + default: + object? y = Eval_(thread, fr, fn, binop.GetY()); + try + { + return EvalUtils.BinaryOp(binop.GetOperator(), x, y, thread); + } + catch (EvalException) + { + fr.SetErrorLocation(binop.GetOperatorLocation()); + throw; + } + } + } + + private static object? EvalDict( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, DictExpression dictexpr) + { + Dict dict = Dict.Of(thread.Mutability); + foreach (DictExpression.Entry entry in dictexpr.GetEntries()) + { + object? k = Eval_(thread, fr, fn, entry.GetKey()); + object? v = Eval_(thread, fr, fn, entry.GetValue()); + int before = dict.Count; + try + { + dict.PutEntry(k, v); + } + catch (EvalException) + { + fr.SetErrorLocation(entry.GetColonLocation()); + throw; + } + if (dict.Count == before) + { + fr.SetErrorLocation(entry.GetColonLocation()); + throw Starlark.Errorf( + "dictionary expression has duplicate key: {0}", Starlark.Repr(k, thread.GetSemantics())); + } + } + return dict; + } + + private static object? EvalDot( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, DotExpression dot) + { + object? obj = Eval_(thread, fr, fn, dot.GetObject()); + string nm = dot.GetField().GetName(); + try + { + return Starlark.GetAttr(thread, obj, nm, null); + } + catch (EvalException) + { + fr.SetErrorLocation(dot.GetDotLocation()); + throw; + } + } + + private static object? EvalIndex( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, IndexExpression index) + { + object? obj = Eval_(thread, fr, fn, index.GetObject()); + object? key = Eval_(thread, fr, fn, index.GetKey()); + try + { + return EvalUtils.Index(thread, obj, key!); + } + catch (EvalException) + { + fr.SetErrorLocation(index.GetLbracketLocation()); + throw; + } + } + + private static object? EvalSlice( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, SliceExpression slice) + { + object? x = Eval_(thread, fr, fn, slice.GetObject()); + object start = slice.GetStart() == null ? Starlark.None : Eval_(thread, fr, fn, slice.GetStart()!)!; + object stop = slice.GetStop() == null ? Starlark.None : Eval_(thread, fr, fn, slice.GetStop()!)!; + object step = slice.GetStep() == null ? Starlark.None : Eval_(thread, fr, fn, slice.GetStep()!)!; + try + { + return Starlark.Slice(thread.Mutability, x!, start, stop, step); + } + catch (EvalException) + { + fr.SetErrorLocation(slice.GetLbracketLocation()); + throw; + } + } + + private static object? EvalUnaryOperator( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, UnaryOperatorExpression unop) + { + object? x = Eval_(thread, fr, fn, unop.GetX()); + try + { + return EvalUtils.UnaryOp(unop.GetOperator(), x); + } + catch (EvalException) + { + fr.SetErrorLocation(unop.GetStartLocation()); + throw; + } + } + + private static object? EvalIdentifier( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, Identifier id) + { + Resolver.Binding bind = id.GetBinding()!; + object? result; + switch (bind.GetScope()) + { + case Resolver.Scope.LOCAL: + result = fr.Locals![bind.GetIndex()]; + break; + case Resolver.Scope.CELL: + result = ((StarlarkFunction.Cell)fr.Locals![bind.GetIndex()]!).X; + break; + case Resolver.Scope.FREE: + result = fn.GetFreeVar(bind.GetIndex()).X; + break; + case Resolver.Scope.GLOBAL: + result = fn.GetGlobal(bind.GetIndex()); + break; + case Resolver.Scope.PREDECLARED: + result = fn.Module.GetPredeclared(id.GetName()); + break; + case Resolver.Scope.UNIVERSAL: + result = Starlark.UNIVERSE.TryGetValue(id.GetName(), out object? v) ? v : null; + break; + default: + throw new InvalidOperationException(bind.ToString()); + } + if (result == null) + { + fr.SetErrorLocation(id.GetStartLocation()); + throw Starlark.Errorf( + "{0} variable '{1}' is referenced before assignment.", + Resolver.ScopeToString(bind.GetScope()), id.GetName()); + } + return result; + } + + private static object? EvalList( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, ListExpression expr) + { + IReadOnlyList elems = expr.GetElements(); + int n = elems.Count; + object?[] array = new object?[n]; + for (int i = 0; i < n; i++) + { + array[i] = Eval_(thread, fr, fn, elems[i]); + } + return expr.IsTuple() ? Tuple.Wrap(array) : StarlarkList.Wrap(thread.Mutability, array); + } + + private static object? EvalCall( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, CallExpression call) + { + object? fnValue = Eval_(thread, fr, fn, call.GetFunction()); + + IReadOnlyList arguments = call.GetArguments(); + int numNonStarArgs = arguments.Count; + Argument.StarStar? starstar = null; + if (numNonStarArgs > 0 && arguments[numNonStarArgs - 1] is Argument.StarStar ss) + { + starstar = ss; + numNonStarArgs--; + } + Argument.Star? star = null; + if (numNonStarArgs > 0 && arguments[numNonStarArgs - 1] is Argument.Star s) + { + star = s; + numNonStarArgs--; + } + int numPositionalArguments = call.GetNumPositionalArguments(); + + var positional = new List(); + var named = new List(); + + int i; + for (i = 0; i < numPositionalArguments; i++) + { + positional.Add(Eval_(thread, fr, fn, arguments[i].GetValue())); + } + for (; i < numNonStarArgs; i++) + { + Argument arg = arguments[i]; + named.Add(arg.GetName()); + named.Add(Eval_(thread, fr, fn, arg.GetValue())); + } + if (star != null) + { + object? value = Eval_(thread, fr, fn, star.GetValue()); + if (value is not IStarlarkIterable && !(value is System.Collections.IEnumerable && value is IStarlarkValue)) + { + fr.SetErrorLocation(star.GetStartLocation()); + throw Starlark.Errorf("argument after * must be an iterable, not {0}", Starlark.Type(value)); + } + foreach (object? o in Starlark.ToIterable(value)) + { + positional.Add(o); + } + } + if (starstar != null) + { + object? value = Eval_(thread, fr, fn, starstar.GetValue()); + if (value is not Dict kwargs) + { + fr.SetErrorLocation(starstar.GetStartLocation()); + throw Starlark.Errorf("argument after ** must be a dict, not {0}", Starlark.Type(value)); + } + foreach (var e in kwargs.Entries) + { + if (e.Key is not string key) + { + fr.SetErrorLocation(starstar.GetStartLocation()); + throw Starlark.Errorf("keywords must be strings, not {0}", Starlark.Type(e.Key)); + } + named.Add(key); + named.Add(e.Value); + } + } + + Location loc = call.GetLparenLocation(); + fr.SetLocation(loc); + try + { + return Starlark.Fastcall(thread, fnValue, positional.ToArray(), named.ToArray()); + } + catch (EvalException) + { + fr.SetErrorLocation(loc); + throw; + } + } + + private static object? EvalComprehension( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, Comprehension comp) + { + Dict? dict = comp.IsDict() ? Dict.Of(thread.Mutability) : null; + StarlarkList? list = comp.IsDict() ? null : StarlarkList.NewList(thread.Mutability); + IReadOnlyList clauses = comp.GetClauses(); + + void ExecClauses(int index) + { + if (index < clauses.Count) + { + Comprehension.Clause clause = clauses[index]; + if (clause is Comprehension.For forClause) + { + object? seqObj = Eval_(thread, fr, fn, forClause.GetIterable()); + IEnumerable seq = Starlark.ToIterable(seqObj); + EvalUtils.AddIterator(seqObj); + try + { + foreach (object? elem in seq) + { + Assign(thread, fr, fn, forClause.GetVars(), elem); + ExecClauses(index + 1); + } + } + catch (EvalException) + { + fr.SetErrorLocation(forClause.GetStartLocation()); + throw; + } + finally + { + EvalUtils.RemoveIterator(seqObj); + } + } + else + { + var ifClause = (Comprehension.If)clause; + if (Starlark.Truth(Eval_(thread, fr, fn, ifClause.GetCondition()))) + { + ExecClauses(index + 1); + } + } + return; + } + + if (dict != null) + { + var body = (DictExpression.Entry)comp.GetBody(); + object? k = Eval_(thread, fr, fn, body.GetKey()); + Starlark.CheckHashable(k); + object? v = Eval_(thread, fr, fn, body.GetValue()); + dict.PutEntry(k, v); + } + else + { + list!.AddElement(Eval_(thread, fr, fn, (Expression)comp.GetBody())); + } + } + + ExecClauses(0); + return comp.IsDict() ? dict : list; + } + + // ---- Function creation ---- + + private static StarlarkFunction NewFunction( + StarlarkThread thread, StarlarkThread.Frame fr, StarlarkFunction fn, Resolver.Function rfn) + { + int nparams = + rfn.GetParameters().Count - (rfn.HasKwargs() ? 1 : 0) - (rfn.HasVarargs() ? 1 : 0); + + object?[]? defaults = null; + for (int i = 0; i < nparams; i++) + { + Expression? defExpr = rfn.GetParameters()[i].GetDefaultValue(); + if (defExpr == null && defaults == null) + { + continue; // skip prefix of required parameters + } + defaults ??= new object?[nparams - i]; + object? defaultValue = defExpr == null ? StarlarkFunction.MANDATORY : Eval_(thread, fr, fn, defExpr); + defaults[i - (nparams - defaults.Length)] = defaultValue; + } + defaults ??= Array.Empty(); + + // Capture cells for free variables. + IReadOnlyList freeVarBindings = rfn.GetFreeVars(); + object?[] freevars = new object?[freeVarBindings.Count]; + int j = 0; + foreach (Resolver.Binding bind in freeVarBindings) + { + switch (bind.GetScope()) + { + case Resolver.Scope.FREE: + freevars[j++] = fn.GetFreeVar(bind.GetIndex()); + break; + case Resolver.Scope.CELL: + freevars[j++] = fr.Locals![bind.GetIndex()]; + break; + default: + throw new InvalidOperationException("unexpected: " + bind); + } + } + + return new StarlarkFunction( + rfn, + fn.Module, + fn.GlobalIndex, + Tuple.Wrap(defaults), + Tuple.Wrap(freevars)); + } +} diff --git a/src/Starlark/Eval/FormatParser.cs b/src/Starlark/Eval/FormatParser.cs new file mode 100644 index 000000000..7ee255baf --- /dev/null +++ b/src/Starlark/Eval/FormatParser.cs @@ -0,0 +1,102 @@ +// Copyright 2015 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text; + +namespace Starlark.Eval; + +/// +/// Implements str.format replacement-field interpolation. Port of +/// net.starlark.java.eval.FormatParser. +/// +internal static class FormatParser +{ + internal static string Format(string pattern, Tuple args, Dict kwargs, StarlarkSemantics semantics) + { + var sb = new StringBuilder(); + int autoIndex = 0; + int i = 0; + int n = pattern.Length; + while (i < n) + { + char c = pattern[i]; + if (c == '{') + { + if (i + 1 < n && pattern[i + 1] == '{') + { + sb.Append('{'); + i += 2; + continue; + } + int close = pattern.IndexOf('}', i + 1); + if (close < 0) + { + throw Starlark.Errorf("Found '{{' without matching '}}'"); + } + string key = pattern.Substring(i + 1, close - i - 1); + object? value = ResolveKey(key, args, kwargs, ref autoIndex); + sb.Append(Starlark.Str(value, semantics)); + i = close + 1; + } + else if (c == '}') + { + if (i + 1 < n && pattern[i + 1] == '}') + { + sb.Append('}'); + i += 2; + continue; + } + throw Starlark.Errorf("Found '}}' without matching '{{'"); + } + else + { + sb.Append(c); + i++; + } + } + return sb.ToString(); + } + + private static object? ResolveKey(string key, Tuple args, Dict kwargs, ref int autoIndex) + { + // Field name may contain a trailing conversion/format spec after ':' or '!'; unsupported + // here beyond field selection, so take the leading identifier/index only. + int cut = key.IndexOfAny(new[] { ':', '!' }); + string field = cut < 0 ? key : key.Substring(0, cut); + + if (field.Length == 0) + { + if (autoIndex >= args.Count) + { + throw Starlark.Errorf( + "Not enough arguments for format string (needed at least {0})", autoIndex + 1); + } + return args[autoIndex++]; + } + if (int.TryParse(field, out int idx)) + { + if (idx < 0 || idx >= args.Count) + { + throw Starlark.Errorf("Index '{0}' out of range", idx); + } + return args[idx]; + } + object? v = kwargs.Get(field); + if (v == null && !kwargs.ContainsKeyJava(field)) + { + throw Starlark.Errorf("Missing argument '{0}'", field); + } + return v; + } +} diff --git a/src/Starlark/Eval/MethodDescriptor.cs b/src/Starlark/Eval/MethodDescriptor.cs new file mode 100644 index 000000000..17dee5d7b --- /dev/null +++ b/src/Starlark/Eval/MethodDescriptor.cs @@ -0,0 +1,249 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// A MethodDescriptor is a wrapper around a C# method (or property) callable from Starlark, together +/// with its and parameter descriptors. Port of +/// net.starlark.java.eval.MethodDescriptor. +/// +public sealed class MethodDescriptor +{ + private readonly MethodInfo method; + private readonly StarlarkMethodAttribute annotation; + private readonly string name; + private readonly ParamDescriptor[] parameters; + private readonly bool structField; + private readonly bool useStarlarkThread; + private readonly bool useStarlarkSemantics; + private readonly bool selfCall; + private readonly bool allowReturnNones; + private readonly bool extraPositionals; + private readonly bool extraKeywords; + + // The full CLR parameter list of the underlying method, in declaration order. Each entry maps a + // CLR parameter to how it is filled at call time. + private readonly ClrParam[] clrParams; + + private enum ClrKind { Param, Thread, Semantics, ExtraPositionals, ExtraKeywords } + + private readonly struct ClrParam + { + internal ClrKind Kind { get; init; } + + // Index into `parameters` when Kind == Param. + internal int ParamIndex { get; init; } + } + + private MethodDescriptor( + MethodInfo method, + StarlarkMethodAttribute annotation, + string name, + ParamDescriptor[] parameters, + bool structField, + bool useStarlarkThread, + bool useStarlarkSemantics, + bool selfCall, + bool allowReturnNones, + bool extraPositionals, + bool extraKeywords, + ClrParam[] clrParams) + { + this.method = method; + this.annotation = annotation; + this.name = name; + this.parameters = parameters; + this.structField = structField; + this.useStarlarkThread = useStarlarkThread; + this.useStarlarkSemantics = useStarlarkSemantics; + this.selfCall = selfCall; + this.allowReturnNones = allowReturnNones; + this.extraPositionals = extraPositionals; + this.extraKeywords = extraKeywords; + this.clrParams = clrParams; + } + + /// Constructs a MethodDescriptor from a C# method and its annotation. + internal static MethodDescriptor Of(MethodInfo method, StarlarkMethodAttribute annotation) + { + ParameterInfo[] clr = method.GetParameters(); + var clrParams = new ClrParam[clr.Length]; + var starlarkParams = new List(); + bool extraPositionals = false; + bool extraKeywords = false; + + for (int i = 0; i < clr.Length; i++) + { + ParameterInfo p = clr[i]; + Type t = p.ParameterType; + if (t == typeof(StarlarkThread)) + { + clrParams[i] = new ClrParam { Kind = ClrKind.Thread }; + } + else if (t == typeof(StarlarkSemantics)) + { + clrParams[i] = new ClrParam { Kind = ClrKind.Semantics }; + } + else + { + var attr = p.GetCustomAttribute(); + // Heuristic for *args / **kwargs: a trailing Tuple/Sequence parameter with no + // ParamAttribute becomes extraPositionals; a trailing Dict without a ParamAttribute + // becomes extraKeywords. To be explicit, callers annotate these with a ParamAttribute + // whose name signals the residual; here we rely on the annotation being absent. + bool isResidual = attr == null; + if (isResidual && (t == typeof(Dict)) && !extraKeywords) + { + clrParams[i] = new ClrParam { Kind = ClrKind.ExtraKeywords }; + extraKeywords = true; + } + else if (isResidual + && (t == typeof(Tuple) + || typeof(ISequence).IsAssignableFrom(t) + || typeof(System.Collections.IEnumerable).IsAssignableFrom(t) && t != typeof(string)) + && !extraPositionals) + { + clrParams[i] = new ClrParam { Kind = ClrKind.ExtraPositionals }; + extraPositionals = true; + } + else + { + var pd = ParamDescriptor.Of(p, attr); + clrParams[i] = new ClrParam { Kind = ClrKind.Param, ParamIndex = starlarkParams.Count }; + starlarkParams.Add(pd); + } + } + } + + return new MethodDescriptor( + method, + annotation, + annotation.Name, + starlarkParams.ToArray(), + annotation.StructField, + annotation.UseStarlarkThread, + annotation.UseStarlarkSemantics, + annotation.SelfCall, + annotation.AllowReturnNones, + extraPositionals, + extraKeywords, + clrParams); + } + + /// The Starlark name of the method. + public string Name => name; + + /// The declared Starlark parameters (excluding thread/semantics/residuals). + public ParamDescriptor[] Parameters => parameters; + + /// Whether this method is accessed as a struct field. + public bool IsStructField => structField; + + /// Whether the enclosing value is callable via this method. + public bool IsSelfCall => selfCall; + + /// Whether this method accepts extra positional arguments (*args). + public bool AcceptsExtraArgs => extraPositionals; + + /// Whether this method accepts extra keyword arguments (**kwargs). + public bool AcceptsExtraKwargs => extraKeywords; + + /// Whether the StarlarkThread is passed to the method. + public bool IsUseStarlarkThread => useStarlarkThread || HasClrKind(ClrKind.Thread); + + private bool HasClrKind(ClrKind kind) + { + foreach (ClrParam c in clrParams) + { + if (c.Kind == kind) + { + return true; + } + } + return false; + } + + /// Documentation for the method. + public string Doc => annotation.Doc; + + /// + /// Invokes the underlying method. supplies the values for the declared + /// Starlark parameters, in order, followed by *args (if accepted), + /// **kwargs (if accepted). The StarlarkThread/StarlarkSemantics slots are filled from + /// . + /// + internal object? Call(object receiver, object?[] vector, Tuple? varargs, Dict? kwargs, StarlarkThread thread) + { + object?[] clrArgs = new object?[clrParams.Length]; + for (int i = 0; i < clrParams.Length; i++) + { + ClrParam c = clrParams[i]; + switch (c.Kind) + { + case ClrKind.Param: + clrArgs[i] = vector[c.ParamIndex]; + break; + case ClrKind.Thread: + clrArgs[i] = thread; + break; + case ClrKind.Semantics: + clrArgs[i] = thread.GetSemantics(); + break; + case ClrKind.ExtraPositionals: + clrArgs[i] = varargs ?? Tuple.Empty(); + break; + case ClrKind.ExtraKeywords: + clrArgs[i] = kwargs ?? Dict.Of(thread.Mutability); + break; + } + } + + object? result; + try + { + result = method.Invoke(receiver, clrArgs); + } + catch (TargetInvocationException ex) + { + Exception? e = ex.InnerException; + switch (e) + { + case null: + throw; + case EvalException: + throw e; + default: + throw new EvalException(e.Message, e); + } + } + + if (method.ReturnType == typeof(void)) + { + return Starlark.None; + } + if (result == null) + { + if (allowReturnNones) + { + return Starlark.None; + } + return Starlark.None; + } + return result; + } +} diff --git a/src/Starlark/Eval/MethodLibrary.cs b/src/Starlark/Eval/MethodLibrary.cs new file mode 100644 index 000000000..387f7c96a --- /dev/null +++ b/src/Starlark/Eval/MethodLibrary.cs @@ -0,0 +1,433 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// The universe of predeclared Starlark functions: len, str, repr, type, +/// list, dict, tuple, range, sorted, enumerate, etc. Port of +/// net.starlark.java.eval.MethodLibrary. +/// +public sealed class MethodLibrary : IStarlarkValue +{ + /// The singleton receiver for the universe builtins. + public static readonly MethodLibrary INSTANCE = new(); + + private MethodLibrary() { } + + [StarlarkMethod("len", UseStarlarkThread = true)] + public StarlarkInt Len([Param(Name = "x")] object x, StarlarkThread thread) + { + int len = Starlark.Len(x); + if (len < 0) + { + throw Starlark.Errorf("{0} is not iterable", Starlark.Type(x)); + } + return StarlarkInt.Of(len); + } + + [StarlarkMethod("str", UseStarlarkThread = true)] + public string Str([Param(Name = "x")] object? x, StarlarkThread thread) => + Starlark.Str(x, thread.GetSemantics()); + + [StarlarkMethod("repr", UseStarlarkThread = true)] + public string Repr([Param(Name = "x")] object? x, StarlarkThread thread) => + Starlark.Repr(x, thread.GetSemantics()); + + [StarlarkMethod("type")] + public string Type([Param(Name = "x")] object? x) => Starlark.Type(x); + + [StarlarkMethod("bool")] + public bool Bool([Param(Name = "x", DefaultValue = "False")] object? x) => Starlark.Truth(x); + + [StarlarkMethod("tuple")] + public Tuple TupleFn([Param(Name = "x", DefaultValue = "()")] object x) + { + if (x is Tuple t) + { + return t; + } + return Tuple.Wrap(Starlark.ToArray(x)); + } + + [StarlarkMethod("list", UseStarlarkThread = true)] + public StarlarkList ListFn([Param(Name = "x", DefaultValue = "[]")] object x, StarlarkThread thread) => + StarlarkList.Wrap(thread.Mutability, Starlark.ToArray(x)); + + [StarlarkMethod("dict", UseStarlarkThread = true)] + public Dict DictFn( + [Param(Name = "pairs", DefaultValue = "[]")] object pairs, + Dict kwargs, + StarlarkThread thread) + { + if (pairs is StarlarkList sl && sl.Count == 0) + { + return kwargs; + } + Dict dict = Dict.Of(thread.Mutability); + Dict.UpdateCommon("dict", dict, pairs, kwargs.Entries); + return dict; + } + + [StarlarkMethod("range", UseStarlarkThread = true)] + public RangeList Range( + [Param(Name = "start_or_stop")] StarlarkInt startOrStop, + [Param(Name = "stop", DefaultValue = "unbound")] object? stopOrUnbound, + [Param(Name = "step", DefaultValue = "1")] StarlarkInt stepI, + StarlarkThread thread) + { + int start; + int stop; + if (ReferenceEquals(stopOrUnbound, Starlark.UNBOUND)) + { + start = 0; + stop = startOrStop.ToInt("stop"); + } + else + { + start = startOrStop.ToInt("start"); + stop = Starlark.ToInt(stopOrUnbound, "stop"); + } + int step = stepI.ToInt("step"); + if (step == 0) + { + throw Starlark.Errorf("step cannot be 0"); + } + return new RangeList(start, stop, step); + } + + [StarlarkMethod("enumerate", UseStarlarkThread = true)] + public StarlarkList Enumerate( + [Param(Name = "list", Named = true)] object input, + [Param(Name = "start", DefaultValue = "0", Named = true)] StarlarkInt startI, + StarlarkThread thread) + { + int start = startI.ToInt("start"); + object?[] array = Starlark.ToArray(input); + for (int i = 0; i < array.Length; i++) + { + array[i] = Tuple.Pair(StarlarkInt.Of(i + start), array[i]); + } + return StarlarkList.Wrap(thread.Mutability, array); + } + + [StarlarkMethod("zip", UseStarlarkThread = true)] + public StarlarkList Zip(Tuple args, StarlarkThread thread) + { + StarlarkList result = StarlarkList.NewList(thread.Mutability); + int ncols = args.Count; + if (ncols > 0) + { + var iterators = new IEnumerator[ncols]; + for (int i = 0; i < ncols; i++) + { + iterators[i] = Starlark.ToIterable(args[i]).GetEnumerator(); + } + while (true) + { + var elem = new object?[ncols]; + for (int i = 0; i < ncols; i++) + { + if (!iterators[i].MoveNext()) + { + return result; + } + elem[i] = iterators[i].Current; + } + result.AddElement(Tuple.Wrap(elem)); + } + } + return result; + } + + [StarlarkMethod("reversed", UseStarlarkThread = true)] + public StarlarkList Reversed([Param(Name = "sequence")] object sequence, StarlarkThread thread) + { + object?[] array = Starlark.ToArray(sequence); + Array.Reverse(array); + return StarlarkList.Wrap(thread.Mutability, array); + } + + [StarlarkMethod("sorted", UseStarlarkThread = true)] + public StarlarkList Sorted( + [Param(Name = "iterable")] object iterable, + [Param(Name = "key", DefaultValue = "None", Named = true, Noneable = true)] object? key, + [Param(Name = "reverse", DefaultValue = "False", Named = true)] bool reverse, + StarlarkThread thread) + { + object?[] array = Starlark.ToArray(iterable); + Comparison baseCmp = (a, b) => Starlark.CompareUnchecked(a, b); + if (ReferenceEquals(key, Starlark.None)) + { + StableSort(array, baseCmp, reverse); + return StarlarkList.Wrap(thread.Mutability, array); + } + + var keyed = new (object? K, object? V)[array.Length]; + for (int i = 0; i < array.Length; i++) + { + object? k = Starlark.Fastcall(thread, key, new[] { array[i] }, Array.Empty()); + keyed[i] = (k, array[i]); + } + var indices = Enumerable.Range(0, keyed.Length).ToArray(); + Array.Sort(indices, (ia, ib) => + { + int cmp = Starlark.CompareUnchecked(keyed[ia].K, keyed[ib].K); + if (reverse) + { + cmp = -cmp; + } + return cmp != 0 ? cmp : ia.CompareTo(ib); // stable + }); + var result = new object?[keyed.Length]; + for (int i = 0; i < indices.Length; i++) + { + result[i] = keyed[indices[i]].V; + } + return StarlarkList.Wrap(thread.Mutability, result); + } + + private static void StableSort(object?[] array, Comparison cmp, bool reverse) + { + var indices = Enumerable.Range(0, array.Length).ToArray(); + Array.Sort(indices, (ia, ib) => + { + int c = cmp(array[ia], array[ib]); + if (reverse) + { + c = -c; + } + return c != 0 ? c : ia.CompareTo(ib); + }); + object?[] copy = (object?[])array.Clone(); + for (int i = 0; i < indices.Length; i++) + { + array[i] = copy[indices[i]]; + } + } + + [StarlarkMethod("min", UseStarlarkThread = true)] + public object? Min( + [Param(Name = "key", DefaultValue = "None", Named = true, Positional = false, Noneable = true)] object? key, + Tuple args, + StarlarkThread thread) => FindExtreme(args, key, /* max= */ false, thread); + + [StarlarkMethod("max", UseStarlarkThread = true)] + public object? Max( + [Param(Name = "key", DefaultValue = "None", Named = true, Positional = false, Noneable = true)] object? key, + Tuple args, + StarlarkThread thread) => FindExtreme(args, key, /* max= */ true, thread); + + private static object? FindExtreme(Tuple args, object? key, bool max, StarlarkThread thread) + { + IEnumerable items; + if (args.Count == 1) + { + items = Starlark.ToIterable(args[0]); + } + else if (args.Count == 0) + { + throw Starlark.Errorf("expected at least one item"); + } + else + { + items = args; + } + + bool haveKey = !ReferenceEquals(key, Starlark.None); + object? best = null; + object? bestKey = null; + bool first = true; + foreach (object? item in items) + { + object? itemKey = haveKey + ? Starlark.Fastcall(thread, key, new[] { item }, Array.Empty()) + : item; + if (first) + { + best = item; + bestKey = itemKey; + first = false; + continue; + } + int cmp = Starlark.CompareUnchecked(itemKey, bestKey); + if (max ? cmp > 0 : cmp < 0) + { + best = item; + bestKey = itemKey; + } + } + if (first) + { + throw Starlark.Errorf("argument is empty"); + } + return best; + } + + [StarlarkMethod("abs")] + public object Abs([Param(Name = "x")] object x) + { + if (x is StarlarkInt si) + { + return si.Signum() < 0 ? StarlarkInt.Uminus(si) : si; + } + if (x is StarlarkFloat sf) + { + return StarlarkFloat.Of(Math.Abs(sf.ToDouble())); + } + throw Starlark.Errorf("got {0} for x, want int or float", Starlark.Type(x)); + } + + [StarlarkMethod("all")] + public bool All([Param(Name = "elements")] object elements) + { + foreach (object? x in Starlark.ToIterable(elements)) + { + if (!Starlark.Truth(x)) + { + return false; + } + } + return true; + } + + [StarlarkMethod("any")] + public bool Any([Param(Name = "elements")] object elements) + { + foreach (object? x in Starlark.ToIterable(elements)) + { + if (Starlark.Truth(x)) + { + return true; + } + } + return false; + } + + [StarlarkMethod("int")] + public StarlarkInt IntFn( + [Param(Name = "x")] object x, + [Param(Name = "base", DefaultValue = "unbound", Named = true)] object? baseO) + { + if (x is string s) + { + int @base = ReferenceEquals(baseO, Starlark.UNBOUND) ? 10 : Starlark.ToInt(baseO, "base"); + try + { + return StarlarkInt.Parse(s, @base); + } + catch (FormatException ex) + { + throw Starlark.Errorf("{0}", ex.Message); + } + catch (ArgumentException ex) + { + throw Starlark.Errorf("{0}", ex.Message); + } + } + if (!ReferenceEquals(baseO, Starlark.UNBOUND)) + { + throw Starlark.Errorf("int() can't convert non-string with explicit base"); + } + switch (x) + { + case bool b: + return StarlarkInt.Of(b ? 1 : 0); + case StarlarkInt si: + return si; + case StarlarkFloat sf: + return StarlarkInt.OfFiniteDouble(sf.ToDouble()); + default: + throw Starlark.Errorf("{0} is not of type string or int or float", Starlark.Type(x)); + } + } + + [StarlarkMethod("hash")] + public StarlarkInt Hash([Param(Name = "value")] string value) + { + // Java String.hashCode algorithm, for cross-implementation determinism. + int h = 0; + foreach (char c in value) + { + h = unchecked(31 * h + c); + } + return StarlarkInt.Of(h); + } + + [StarlarkMethod("print", UseStarlarkThread = true)] + public void Print( + [Param(Name = "sep", DefaultValue = "\" \"", Named = true, Positional = false)] string sep, + Tuple args, + StarlarkThread thread) + { + var p = new Printer(); + string separator = ""; + foreach (object? x in args) + { + p.Append(separator); + p.DebugPrint(x, thread); + separator = sep; + } + thread.GetPrintHandler()(thread, p.ToString()); + } + + [StarlarkMethod("fail", UseStarlarkThread = true)] + public void Fail( + [Param(Name = "msg", DefaultValue = "None", Named = true, Positional = false, Noneable = true)] object? msg, + [Param(Name = "sep", DefaultValue = "\" \"", Named = true, Positional = false)] string sep, + Tuple args, + StarlarkThread thread) + { + var printer = new Printer(); + bool needSep = false; + if (!ReferenceEquals(msg, Starlark.None)) + { + printer.DebugPrint(msg, thread); + needSep = true; + } + foreach (object? arg in args) + { + if (needSep) + { + printer.Append(sep); + } + printer.DebugPrint(arg, thread); + needSep = true; + } + throw new EvalException(printer.ToString()); + } + + [StarlarkMethod("getattr", UseStarlarkThread = true)] + public object? Getattr( + [Param(Name = "x")] object? x, + [Param(Name = "name")] string name, + [Param(Name = "default", DefaultValue = "unbound")] object? defaultValue, + StarlarkThread thread) + { + object? def = ReferenceEquals(defaultValue, Starlark.UNBOUND) ? null : defaultValue; + return Starlark.GetAttr(thread, x, name, def); + } + + [StarlarkMethod("hasattr", UseStarlarkThread = true)] + public bool Hasattr( + [Param(Name = "x")] object? x, + [Param(Name = "name")] string name, + StarlarkThread thread) => Starlark.HasAttr(thread, x, name); + + [StarlarkMethod("dir", UseStarlarkThread = true)] + public StarlarkList DirFn([Param(Name = "x")] object? x, StarlarkThread thread) => + Starlark.Dir(thread, x); +} diff --git a/src/Starlark/Eval/ParamDescriptor.cs b/src/Starlark/Eval/ParamDescriptor.cs new file mode 100644 index 000000000..d1d90e5ea --- /dev/null +++ b/src/Starlark/Eval/ParamDescriptor.cs @@ -0,0 +1,142 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// A ParamDescriptor is a descriptor of a formal parameter of a Java method callable from Starlark. +/// Port of net.starlark.java.eval.ParamDescriptor. +/// +/// In the .NET port the parameter metadata comes from a applied to +/// the corresponding C# parameter (plus the parameter's declared CLR type). +/// +public sealed class ParamDescriptor +{ + private readonly string name; + private readonly object? defaultValue; + private readonly bool named; + private readonly bool positional; + private readonly bool noneable; + private readonly IReadOnlyList? allowedClasses; // null means any + + private ParamDescriptor( + string name, + string defaultExpr, + bool named, + bool positional, + bool noneable, + IReadOnlyList? allowedClasses) + { + this.name = name; + this.defaultValue = string.IsNullOrEmpty(defaultExpr) ? null : EvalDefault(name, defaultExpr); + this.named = named; + this.positional = positional; + this.noneable = noneable; + this.allowedClasses = allowedClasses; + } + + /// Returns the descriptor for the given C# parameter. + internal static ParamDescriptor Of(ParameterInfo param, ParamAttribute? attr) + { + attr ??= new ParamAttribute(); + string name = string.IsNullOrEmpty(attr.Name) ? param.Name ?? "" : attr.Name; + + // A parameter that is positional by default but not marked named remains positional-only. + bool positional = attr.Positional; + bool named = attr.Named; + if (!positional && !named) + { + // Default: positional. (Matches Java Param defaults where positional=true, named=false.) + positional = true; + } + + IReadOnlyList? allowed = null; + if (attr.AllowedTypes.Length > 0) + { + allowed = attr.AllowedTypes; + } + + return new ParamDescriptor( + name, attr.DefaultValue, named, positional, attr.Noneable, allowed); + } + + /// The parameter's Starlark name. + public string Name => name; + + /// The default value, or null if the parameter is mandatory. + public object? DefaultValue => defaultValue; + + /// Whether the parameter may be specified by name. + public bool IsNamed => named; + + /// Whether the parameter may be specified positionally. + public bool IsPositional => positional; + + /// Whether None is a permissible value. + public bool IsNoneable => noneable; + + /// The allowed CLR classes for this parameter, or null if any is allowed. + public IReadOnlyList? AllowedClasses => allowedClasses; + + public override string ToString() => name; + + // Evaluates the default-value expression of a parameter. Only the small "bootstrap" grammar of + // literal forms is supported here (which covers every default used by the standard library); + // this avoids a cycle with the evaluator during type initialization. + private static object EvalDefault(string name, string expr) + { + switch (expr) + { + case "None": + return Starlark.None; + case "True": + return true; + case "False": + return false; + case "unbound": + return Starlark.UNBOUND; + case "0": + return StarlarkInt.Of(0); + case "1": + return StarlarkInt.Of(1); + case "-1": + return StarlarkInt.Of(-1); + case "[]": + return StarlarkList.Empty(); + case "()": + return Tuple.Empty(); + case "{}": + return Dict.Empty(); + case "\" \"": + return " "; + case "\"\"": + return ""; + } + // Quoted string literal. + if (expr.Length >= 2 && expr[0] == '"' && expr[^1] == '"') + { + return expr.Substring(1, expr.Length - 2); + } + // Plain integer. + if (long.TryParse(expr, out long l)) + { + return StarlarkInt.Of(l); + } + throw new InvalidOperationException( + string.Format("unsupported default value expression for parameter {0}: {1}", name, expr)); + } +} diff --git a/src/Starlark/Eval/StarlarkFunction.cs b/src/Starlark/Eval/StarlarkFunction.cs new file mode 100644 index 000000000..45cf2ff05 --- /dev/null +++ b/src/Starlark/Eval/StarlarkFunction.cs @@ -0,0 +1,287 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Starlark.Syntax; + +namespace Starlark.Eval; + +/// +/// A StarlarkFunction is a user-defined function (def or lambda) that may be called +/// from Starlark. Port of net.starlark.java.eval.StarlarkFunction. +/// +public sealed class StarlarkFunction : IStarlarkCallable +{ + /// A Cell is a boxed reference to a variable shared by nested functions (closures). + public sealed class Cell + { + public object? X; + + public Cell(object? x) => X = x; + } + + /// Sentinel marking a mandatory parameter slot in the defaults tuple. + internal static readonly object MANDATORY = new MandatoryMarker(); + + private sealed class MandatoryMarker { } + + private readonly Resolver.Function rfn; + private readonly Module module; + private readonly int[] globalIndex; + private readonly Tuple defaultValues; + private readonly Tuple freevars; + + internal StarlarkFunction( + Resolver.Function rfn, + Module module, + int[] globalIndex, + Tuple defaultValues, + Tuple freevars) + { + this.rfn = rfn; + this.module = module; + this.globalIndex = globalIndex; + this.defaultValues = defaultValues; + this.freevars = freevars; + } + + /// The resolved function metadata. + public Resolver.Function Rfn => rfn; + + /// The module in which the function was defined. + public Module Module => module; + + internal int[] GlobalIndex => globalIndex; + + public string Name => rfn.GetName(); + + public Location Location => rfn.GetLocation(); + + public bool IsImmutable() + { + // Only correct if defaults and freevars are immutable, which we assume. + return true; + } + + /// Returns the value of the ith global variable, mapping via the module. + internal object? GetGlobal(int index) => module.GetGlobalByIndex(globalIndex[index]); + + internal void SetGlobal(int index, object value) => + module.SetGlobalByIndex(globalIndex[index], value); + + internal Cell GetFreeVar(int index) => (Cell)freevars[index]!; + + /// Returns the names of parameters, in run-time order. + public IReadOnlyList GetParameterNames() => rfn.GetParameterNames(); + + public bool HasVarargs() => rfn.HasVarargs(); + + public bool HasKwargs() => rfn.HasKwargs(); + + public object? Fastcall(StarlarkThread thread, object?[] positional, object?[] named) + { + object?[] locals = ProcessArgs(thread, positional, named); + + // Spill cells. + foreach (int index in rfn.GetCellIndices()) + { + locals[index] = new Cell(locals[index]); + } + + // Recursion check. + if (!thread.IsRecursionAllowed() && IsRecursiveCall(thread)) + { + throw Starlark.Errorf("function '{0}' called recursively", Name); + } + + return Eval.ExecFunctionBody(thread, this, locals, rfn.GetBody()); + } + + public object? Call(StarlarkThread thread, Tuple args, Dict kwargs) + { + object?[] positional = args.ToArray(); + var named = new List(); + foreach (var e in kwargs.Entries) + { + named.Add(e.Key); + named.Add(e.Value); + } + return Fastcall(thread, positional, named.ToArray()); + } + + private bool IsRecursiveCall(StarlarkThread thread) + { + // The top frame (depth 0) is this function, just pushed by Starlark.Fastcall. Search the + // enclosing frames for the same resolved definition. + int n = thread.GetCallStackSize(); + for (int i = 1; i < n; i++) + { + if (thread.FrameFnAt(i) is StarlarkFunction sf && ReferenceEquals(sf.rfn, rfn)) + { + return true; + } + } + return false; + } + + // Binds positional and named arguments to a fresh locals array of the correct size. + private object?[] ProcessArgs(StarlarkThread thread, object?[] positional, object?[] named) + { + int nlocals = rfn.GetLocals().Count; + object?[] locals = new object?[nlocals]; + + int numOrdinary = rfn.GetNumOrdinaryParameters(); + int numPositional = rfn.GetNumNonResidualParameters(); // ordinary + keyword-only + int nparams = rfn.GetParameters().Count; + + var varargsList = HasVarargs() ? new List() : null; + + // Positional arguments. + int p = 0; + for (; p < positional.Length && p < numOrdinary; p++) + { + locals[p] = positional[p]; + } + int numNonSurplusPositional = p; + if (p < positional.Length) + { + if (varargsList != null) + { + for (; p < positional.Length; p++) + { + varargsList.Add(positional[p]); + } + } + else + { + if (numOrdinary > 0) + { + throw Starlark.Errorf( + "{0}() accepts no more than {1} positional argument{2} but got {3}", + Name, numOrdinary, Plural(numOrdinary), positional.Length); + } + throw Starlark.Errorf( + "{0}() does not accept positional arguments, but got {1}", Name, positional.Length); + } + } + + // Named arguments. + IReadOnlyList paramNames = rfn.GetParameterNames(); + Dict.Builder? kwargsBuilder = HasKwargs() ? new Dict.Builder() : null; + for (int i = 0; i < named.Length; i += 2) + { + string key = (string)named[i]!; + object? value = named[i + 1]; + int found = -1; + for (int j = 0; j < numPositional; j++) + { + if (paramNames[j] == key) + { + found = j; + break; + } + } + if (found >= 0) + { + if (locals[found] != null) + { + throw Starlark.Errorf("{0}() got multiple values for parameter '{1}'", Name, key); + } + locals[found] = value; + } + else if (kwargsBuilder != null) + { + kwargsBuilder.Put(key, value); + } + else + { + throw Starlark.Errorf("{0}() got unexpected keyword argument '{1}'", Name, key); + } + } + + // Residual parameters. + if (HasVarargs()) + { + int idx = numPositional; // *args slot follows ordinary+keyword-only params + locals[idx] = varargsList!.Count == 0 ? Tuple.Empty() : Tuple.CopyOf(varargsList); + } + if (HasKwargs()) + { + int idx = numPositional + (HasVarargs() ? 1 : 0); + locals[idx] = kwargsBuilder!.Build(thread.Mutability); + } + + // Apply defaults / report missing. + ApplyDefaultsReportMissing(locals, numPositional); + + return locals; + } + + private void ApplyDefaultsReportMissing(object?[] locals, int numPositional) + { + // defaultValues covers the trailing suffix of the non-residual parameters. + int nparams = numPositional; + int ndefaults = defaultValues.Count; + int firstDefaulted = nparams - ndefaults; + + List? missingPositional = null; + List? missingNamed = null; + int numOrdinary = rfn.GetNumOrdinaryParameters(); + IReadOnlyList paramNames = rfn.GetParameterNames(); + + for (int i = 0; i < nparams; i++) + { + if (locals[i] != null) + { + continue; + } + object? def = i >= firstDefaulted ? defaultValues[i - firstDefaulted] : null; + if (def != null && !ReferenceEquals(def, MANDATORY)) + { + locals[i] = def; + } + else + { + if (i < numOrdinary) + { + (missingPositional ??= new List()).Add(paramNames[i]); + } + else + { + (missingNamed ??= new List()).Add(paramNames[i]); + } + } + } + if (missingPositional != null) + { + throw Starlark.Errorf( + "{0}() missing {1} required positional argument{2}: {3}", + Name, missingPositional.Count, Plural(missingPositional.Count), + string.Join(", ", missingPositional)); + } + if (missingNamed != null) + { + throw Starlark.Errorf( + "{0}() missing {1} required keyword-only argument{2}: {3}", + Name, missingNamed.Count, Plural(missingNamed.Count), + string.Join(", ", missingNamed)); + } + } + + private static string Plural(int n) => n == 1 ? "" : "s"; + + public void Repr(Printer printer, StarlarkSemantics semantics) => + printer.Append(""); + + public override string ToString() => ""; +} diff --git a/src/Starlark/Eval/StarlarkHelpers.cs b/src/Starlark/Eval/StarlarkHelpers.cs index 5f86d520e..408fa57a9 100644 --- a/src/Starlark/Eval/StarlarkHelpers.cs +++ b/src/Starlark/Eval/StarlarkHelpers.cs @@ -49,12 +49,39 @@ internal UnboundMarker() { } } /// - /// The universal bindings predeclared in every Starlark file. Currently only the literal - /// constants; builtin functions are added once the reflective registry is ported. + /// The universal bindings predeclared in every Starlark file: the literal constants plus the + /// core builtin functions from . /// - public static readonly ImmutableDictionary UNIVERSE = - new Dictionary { ["False"] = false, ["True"] = true, ["None"] = None } - .ToImmutableDictionary(); + public static readonly ImmutableDictionary UNIVERSE = MakeUniverse(); + + private static ImmutableDictionary MakeUniverse() + { + var env = new Dictionary + { + ["False"] = false, + ["True"] = true, + ["None"] = None, + }; + AddMethods(env, MethodLibrary.INSTANCE); + return env.ToImmutableDictionary(); + } + + /// + /// Adds to a for each StarlarkMethod-annotated + /// method of 's type (excluding struct fields and the selfCall method). + /// + public static void AddMethods(IDictionary env, object receiver) + { + foreach (var e in CallUtils.GetAnnotatedMethods(receiver.GetType())) + { + MethodDescriptor desc = e.Value; + if (desc.IsStructField) + { + continue; + } + env[e.Key] = new BuiltinFunction(receiver, desc.Name, desc); + } + } /// Thrown when a value is not a legal Starlark value. public sealed class InvalidStarlarkValueException : ArgumentException diff --git a/src/Starlark/Eval/StarlarkRuntime.cs b/src/Starlark/Eval/StarlarkRuntime.cs new file mode 100644 index 000000000..5411c93c7 --- /dev/null +++ b/src/Starlark/Eval/StarlarkRuntime.cs @@ -0,0 +1,267 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Immutable; +using Starlark.Syntax; +using FileOptions = Starlark.Syntax.FileOptions; + +namespace Starlark.Eval; + +/// +/// The dynamic entry points of the Starlark interpreter: calling, attribute access, and the +/// exec/eval file drivers. Port of the corresponding members of +/// net.starlark.java.eval.Starlark. +/// +public static partial class Starlark +{ + // ---- Calling ---- + + /// + /// Calls the callable with positional and keyword arguments. + /// is a flat array of alternating name/value pairs. + /// + public static object? Fastcall( + StarlarkThread thread, object? fn, object?[] positional, object?[] named) + { + IStarlarkCallable callable = GetStarlarkCallable(thread, fn); + thread.Push(callable); + try + { + return callable.Fastcall(thread, positional, named); + } + finally + { + thread.Pop(); + } + } + + /// Calls the callable with a list of positional args and a map of keyword args. + public static object? Call( + StarlarkThread thread, object? fn, IReadOnlyList args, IReadOnlyDictionary kwargs) + { + object?[] positional = args.ToArray(); + var named = new object?[kwargs.Count * 2]; + int i = 0; + foreach (var e in kwargs) + { + named[i++] = e.Key; + named[i++] = CheckValid(e.Value); + } + return Fastcall(thread, fn, positional, named); + } + + /// Returns the value as a StarlarkCallable, or throws if it is not callable. + internal static IStarlarkCallable GetStarlarkCallable(StarlarkThread thread, object? fn) + { + if (fn is IStarlarkCallable callable) + { + return callable; + } + if (fn != null) + { + MethodDescriptor? desc = CallUtils.GetSelfCallMethodDescriptor(fn.GetType()); + if (desc != null) + { + return BuiltinFunction.Of(fn, desc); + } + } + throw Errorf("'{0}' object is not callable", Type(fn)); + } + + // ---- Attribute access ---- + + /// + /// Returns the named field or method of value , as if by x.name / + /// getattr(x, name, defaultValue). If absent and no default is given, throws. + /// + public static object? GetAttr(StarlarkThread thread, object? x, string name, object? defaultValue) + { + StarlarkSemantics semantics = thread.GetSemantics(); + Mutability mu = thread.Mutability; + + if (x != null) + { + ImmutableDictionary methods = + CallUtils.GetAnnotatedMethods(x.GetType()); + if (methods.TryGetValue(name, out MethodDescriptor? method)) + { + if (method.IsStructField) + { + // Invoke the field getter (no Starlark args). + object recv = x is string ? StringModule.INSTANCE : x; + object?[] vector = new object?[method.Parameters.Length]; + // String struct fields take self as first param. + if (x is string s && vector.Length > 0) + { + vector[0] = s; + } + return method.Call(recv, vector, null, null, thread); + } + return BuiltinFunction.Of(x, method); + } + } + + if (x is IStructure structure) + { + object? field = structure.GetValue(semantics, name); + if (field != null) + { + return CheckValid(field); + } + if (defaultValue != null) + { + return defaultValue; + } + string? error = structure.GetErrorMessageForUnknownField(name); + if (error != null) + { + throw Errorf("{0}", error); + } + } + else if (defaultValue != null) + { + return defaultValue; + } + + throw Errorf("'{0}' value has no field or method '{1}'", Type(x), name); + } + + /// Reports whether value has a field or method named . + public static bool HasAttr(StarlarkThread thread, object? x, string name) + { + if (x is IStructure structure && structure.GetFieldNames().Contains(name)) + { + return true; + } + return x != null && CallUtils.GetAnnotatedMethods(x.GetType()).ContainsKey(name); + } + + /// Returns a sorted list of the field and method names of . + public static StarlarkList Dir(StarlarkThread thread, object? x) + { + var fields = new SortedSet(StringComparer.Ordinal); + if (x is IStructure structure) + { + foreach (string f in structure.GetFieldNames()) + { + fields.Add(f); + } + } + if (x != null) + { + foreach (string m in CallUtils.GetAnnotatedMethods(x.GetType()).Keys) + { + fields.Add(m); + } + } + return StarlarkList.CopyOf(thread.Mutability, fields.Cast()); + } + + // ---- File / expression execution ---- + + /// + /// Parses, resolves, compiles, and executes a Starlark file in the given module and thread. + /// Returns the value of the file's final expression statement, if any, else None. + /// + public static object? ExecFile( + ParserInput input, FileOptions options, Module module, StarlarkThread thread) + { + StarlarkFile file = StarlarkFile.Parse(input, options); + Program prog = Program.CompileFile(file, ModuleAsResolverModule(module)); + return ExecFileProgram(prog, module, thread); + } + + /// Executes a previously compiled Program in the given module and thread. + public static object? ExecFileProgram(Program prog, Module module, StarlarkThread thread) + { + Resolver.Function rfn = prog.GetResolvedFunction(); + int[] globalIndex = module.GetIndicesOfGlobals(rfn.GetGlobals()); + + if (module.GetDocumentation() == null) + { + string? documentation = rfn.GetDocumentation(); + if (documentation != null) + { + module.SetDocumentation(documentation); + } + } + + var toplevel = new StarlarkFunction(rfn, module, globalIndex, Tuple.Empty(), Tuple.Empty()); + return Fastcall(thread, toplevel, Array.Empty(), Array.Empty()); + } + + /// Parses, resolves, compiles, and evaluates an expression, returning its value. + public static object? Eval( + ParserInput input, FileOptions options, Module module, StarlarkThread thread) + { + Expression expr = Expression.Parse(input, options); + Program prog = Program.CompileExpr(expr, ModuleAsResolverModule(module), options); + Resolver.Function rfn = prog.GetResolvedFunction(); + int[] globalIndex = module.GetIndicesOfGlobals(rfn.GetGlobals()); + var fn = new StarlarkFunction(rfn, module, globalIndex, Tuple.Empty(), Tuple.Empty()); + return Fastcall(thread, fn, Array.Empty(), Array.Empty()); + } + + /// Parses and executes a series of statements (discarding the result). + public static void Exec( + ParserInput input, FileOptions options, Module module, StarlarkThread thread) + { + ExecFile(input, options, module, thread); + } + + /// Wraps a Module so it can serve as the resolver's static module environment. + public static Resolver.IModule ModuleAsResolverModule(Module module) => new ModuleResolverAdapter(module); + + /// Adapts a to for resolution. + private sealed class ModuleResolverAdapter : Resolver.IModule + { + private readonly Module module; + + internal ModuleResolverAdapter(Module module) => this.module = module; + + public Resolver.Scope Resolve(string name) + { + if (module.GetGlobal(name) != null) + { + return Resolver.Scope.GLOBAL; + } + object? v = module.GetPredeclared(name); + if (v != null) + { + if (v is IGuardedValue gv) + { + throw new Resolver.Undefined(gv.GetErrorFromAttemptingAccess(name)); + } + return Resolver.Scope.PREDECLARED; + } + if (UNIVERSE.ContainsKey(name)) + { + return Resolver.Scope.UNIVERSAL; + } + var candidates = new HashSet(); + candidates.UnionWith(module.GetGlobals().Keys); + candidates.UnionWith(module.GetPredeclaredBindings().Keys); + candidates.UnionWith(UNIVERSE.Keys); + throw new Resolver.Undefined(string.Format("name '{0}' is not defined", name), candidates); + } + + public TypeConstructor? GetTypeConstructor(string name) => null; + + public StarlarkType? GetListFieldType(string name) => null; + + public StarlarkType? GetDictFieldType(string name) => null; + + public StarlarkType? GetSetFieldType(string name) => null; + } +} diff --git a/src/Starlark/Eval/StarlarkThread.cs b/src/Starlark/Eval/StarlarkThread.cs index 848acdb6c..601bd14c3 100644 --- a/src/Starlark/Eval/StarlarkThread.cs +++ b/src/Starlark/Eval/StarlarkThread.cs @@ -131,6 +131,9 @@ internal void SetErrorLocation(Location loc) internal Frame FrameAt(int depth) => callstack[callstack.Count - 1 - depth]; + /// Returns the callable of the frame at the given depth (0 == innermost). + internal IStarlarkCallable FrameFnAt(int depth) => FrameAt(depth).Fn; + private bool Toplevel => callstack.Count < 2; /// Returns the location of the program counter in the enclosing call frame. diff --git a/src/Starlark/Eval/StringModule.cs b/src/Starlark/Eval/StringModule.cs new file mode 100644 index 000000000..88960a04a --- /dev/null +++ b/src/Starlark/Eval/StringModule.cs @@ -0,0 +1,602 @@ +// Copyright 2014 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text; +using Starlark.Annot; + +namespace Starlark.Eval; + +/// +/// The "string" module, defining the methods of the Starlark string type. Port of +/// net.starlark.java.eval.StringModule. Methods take the receiver string as their first +/// (self) parameter. +/// +[StarlarkBuiltin("string")] +public sealed class StringModule : IStarlarkValue +{ + /// The singleton receiver used for all string method dispatch. + public static readonly StringModule INSTANCE = new(); + + private StringModule() { } + + [StarlarkMethod("join", UseStarlarkThread = true)] + public string Join( + [Param(Name = "self")] string self, + [Param(Name = "elements")] object elements, + StarlarkThread thread) + { + var sb = new StringBuilder(); + int i = 0; + bool first = true; + foreach (object? item in Starlark.ToIterable(elements)) + { + if (item is not string s) + { + throw Starlark.Errorf( + "expected string for sequence element {0}, got '{1}' of type {2}", + i, Starlark.Str(item, thread.GetSemantics()), Starlark.Type(item)); + } + if (!first) + { + sb.Append(self); + } + sb.Append(s); + first = false; + i++; + } + return sb.ToString(); + } + + [StarlarkMethod("lower")] + public string Lower([Param(Name = "self")] string self) => AsciiToLower(self); + + [StarlarkMethod("upper")] + public string Upper([Param(Name = "self")] string self) => AsciiToUpper(self); + + [StarlarkMethod("lstrip")] + public string Lstrip( + [Param(Name = "self")] string self, + [Param(Name = "chars", DefaultValue = "None", Noneable = true)] object? chars) => + StringLStrip(self, Matcher(chars)); + + [StarlarkMethod("rstrip")] + public string Rstrip( + [Param(Name = "self")] string self, + [Param(Name = "chars", DefaultValue = "None", Noneable = true)] object? chars) => + StringRStrip(self, Matcher(chars)); + + [StarlarkMethod("strip")] + public string Strip( + [Param(Name = "self")] string self, + [Param(Name = "chars", DefaultValue = "None", Noneable = true)] object? chars) + { + Func matcher = Matcher(chars); + return StringLStrip(StringRStrip(self, matcher), matcher); + } + + [StarlarkMethod("replace", UseStarlarkThread = true)] + public string Replace( + [Param(Name = "self")] string self, + [Param(Name = "old")] string oldString, + [Param(Name = "new")] string newString, + [Param(Name = "count", DefaultValue = "-1")] StarlarkInt countI, + StarlarkThread thread) + { + int count = countI.ToInt("count"); + if (count < 0) + { + count = int.MaxValue; + } + var sb = new StringBuilder(); + int start = 0; + for (int i = 0; i < count; i++) + { + if (oldString.Length == 0) + { + sb.Append(newString); + if (start < self.Length) + { + sb.Append(self[start++]); + } + else + { + break; + } + } + else + { + int end = self.IndexOf(oldString, start, StringComparison.Ordinal); + if (end < 0) + { + break; + } + sb.Append(self, start, end - start).Append(newString); + start = end + oldString.Length; + } + } + sb.Append(self, start, self.Length - start); + return sb.ToString(); + } + + [StarlarkMethod("split", UseStarlarkThread = true)] + public StarlarkList Split( + [Param(Name = "self")] string self, + [Param(Name = "sep", Named = true)] string sep, + [Param(Name = "maxsplit", DefaultValue = "unbound", Named = true)] object? maxSplitO, + StarlarkThread thread) + { + if (sep.Length == 0) + { + throw Starlark.Errorf("Empty separator"); + } + int maxSplit = int.MaxValue; + if (!ReferenceEquals(maxSplitO, Starlark.UNBOUND)) + { + maxSplit = Starlark.ToInt(maxSplitO, "maxsplit"); + } + var res = new List(); + int start = 0; + while (true) + { + int end = self.IndexOf(sep, start, StringComparison.Ordinal); + if (end < 0 || maxSplit-- == 0) + { + res.Add(self.Substring(start)); + break; + } + res.Add(self.Substring(start, end - start)); + start = end + sep.Length; + } + return StarlarkList.CopyOf(thread.Mutability, res); + } + + [StarlarkMethod("rsplit", UseStarlarkThread = true)] + public StarlarkList Rsplit( + [Param(Name = "self")] string self, + [Param(Name = "sep", Named = true)] string sep, + [Param(Name = "maxsplit", DefaultValue = "unbound", Named = true)] object? maxSplitO, + StarlarkThread thread) + { + if (sep.Length == 0) + { + throw Starlark.Errorf("Empty separator"); + } + int maxSplit = int.MaxValue; + if (!ReferenceEquals(maxSplitO, Starlark.UNBOUND)) + { + maxSplit = Starlark.ToInt(maxSplitO, "maxsplit"); + } + var res = new List(); + int end = self.Length; + while (true) + { + int start = end == 0 ? -1 : self.LastIndexOf(sep, end - 1, StringComparison.Ordinal); + if (start < 0 || maxSplit-- == 0) + { + res.Add(self.Substring(0, end)); + break; + } + res.Add(self.Substring(start + sep.Length, end - (start + sep.Length))); + end = start; + } + res.Reverse(); + return StarlarkList.CopyOf(thread.Mutability, res); + } + + [StarlarkMethod("partition")] + public Tuple Partition( + [Param(Name = "self")] string self, + [Param(Name = "sep")] string sep) => PartitionCommon(self, sep, true); + + [StarlarkMethod("rpartition")] + public Tuple Rpartition( + [Param(Name = "self")] string self, + [Param(Name = "sep")] string sep) => PartitionCommon(self, sep, false); + + [StarlarkMethod("capitalize")] + public string Capitalize([Param(Name = "self")] string self) + { + if (self.Length == 0) + { + return self; + } + return char.ToUpperInvariant(self[0]) + AsciiToLower(self.Substring(1)); + } + + [StarlarkMethod("title")] + public string Title([Param(Name = "self")] string self) + { + char[] data = self.ToCharArray(); + bool previousWasLetter = false; + for (int pos = 0; pos < data.Length; pos++) + { + char current = data[pos]; + bool currentIsLetter = char.IsLetter(current); + if (currentIsLetter) + { + if (previousWasLetter && char.IsUpper(current)) + { + data[pos] = char.ToLowerInvariant(current); + } + else if (!previousWasLetter && char.IsLower(current)) + { + data[pos] = char.ToUpperInvariant(current); + } + } + previousWasLetter = currentIsLetter; + } + return new string(data); + } + + [StarlarkMethod("find")] + public StarlarkInt Find( + [Param(Name = "self")] string self, + [Param(Name = "sub")] string sub, + [Param(Name = "start", DefaultValue = "0", Noneable = true)] object? start, + [Param(Name = "end", DefaultValue = "None", Noneable = true)] object? end) => + StarlarkInt.Of(StringFind(true, self, sub, start, end)); + + [StarlarkMethod("rfind")] + public StarlarkInt Rfind( + [Param(Name = "self")] string self, + [Param(Name = "sub")] string sub, + [Param(Name = "start", DefaultValue = "0", Noneable = true)] object? start, + [Param(Name = "end", DefaultValue = "None", Noneable = true)] object? end) => + StarlarkInt.Of(StringFind(false, self, sub, start, end)); + + [StarlarkMethod("index")] + public StarlarkInt Index( + [Param(Name = "self")] string self, + [Param(Name = "sub")] string sub, + [Param(Name = "start", DefaultValue = "0", Noneable = true)] object? start, + [Param(Name = "end", DefaultValue = "None", Noneable = true)] object? end) + { + int res = StringFind(true, self, sub, start, end); + if (res < 0) + { + throw Starlark.Errorf("substring not found"); + } + return StarlarkInt.Of(res); + } + + [StarlarkMethod("rindex")] + public StarlarkInt Rindex( + [Param(Name = "self")] string self, + [Param(Name = "sub")] string sub, + [Param(Name = "start", DefaultValue = "0", Noneable = true)] object? start, + [Param(Name = "end", DefaultValue = "None", Noneable = true)] object? end) + { + int res = StringFind(false, self, sub, start, end); + if (res < 0) + { + throw Starlark.Errorf("substring not found"); + } + return StarlarkInt.Of(res); + } + + [StarlarkMethod("count")] + public StarlarkInt Count( + [Param(Name = "self")] string self, + [Param(Name = "sub")] string sub, + [Param(Name = "start", DefaultValue = "0", Noneable = true)] object? start, + [Param(Name = "end", DefaultValue = "None", Noneable = true)] object? end) + { + (int lo, int hi) = SubstringIndices(self, start, end); + if (sub.Length == 0) + { + return StarlarkInt.Of(hi - lo + 1); + } + string str = self.Substring(lo, hi - lo); + int count = 0; + int index = 0; + while ((index = str.IndexOf(sub, index, StringComparison.Ordinal)) >= 0) + { + count++; + index += sub.Length; + } + return StarlarkInt.Of(count); + } + + [StarlarkMethod("startswith")] + public bool Startswith( + [Param(Name = "self")] string self, + [Param(Name = "sub")] object sub, + [Param(Name = "start", DefaultValue = "0", Noneable = true)] object? start, + [Param(Name = "end", DefaultValue = "None", Noneable = true)] object? end) + { + (int lo, int hi) = SubstringIndices(self, start, end); + if (sub is string prefix) + { + return SubstringStartsWith(self, lo, hi, prefix); + } + foreach (object? s in Starlark.ToIterable(sub)) + { + if (SubstringStartsWith(self, lo, hi, (string)s!)) + { + return true; + } + } + return false; + } + + [StarlarkMethod("endswith")] + public bool Endswith( + [Param(Name = "self")] string self, + [Param(Name = "sub")] object sub, + [Param(Name = "start", DefaultValue = "0", Noneable = true)] object? start, + [Param(Name = "end", DefaultValue = "None", Noneable = true)] object? end) + { + (int lo, int hi) = SubstringIndices(self, start, end); + if (sub is string suffix) + { + return SubstringEndsWith(self, lo, hi, suffix); + } + foreach (object? s in Starlark.ToIterable(sub)) + { + if (SubstringEndsWith(self, lo, hi, (string)s!)) + { + return true; + } + } + return false; + } + + [StarlarkMethod("format", UseStarlarkThread = true)] + public string Format( + [Param(Name = "self")] string self, + Tuple args, + Dict kwargs, + StarlarkThread thread) => + FormatParser.Format(self, args, kwargs, thread.GetSemantics()); + + [StarlarkMethod("removeprefix")] + public string Removeprefix( + [Param(Name = "self")] string self, + [Param(Name = "prefix")] string prefix) => + self.StartsWith(prefix, StringComparison.Ordinal) ? self.Substring(prefix.Length) : self; + + [StarlarkMethod("removesuffix")] + public string Removesuffix( + [Param(Name = "self")] string self, + [Param(Name = "suffix")] string suffix) => + self.EndsWith(suffix, StringComparison.Ordinal) + ? self.Substring(0, self.Length - suffix.Length) + : self; + + [StarlarkMethod("elems", UseStarlarkThread = true)] + public StarlarkList Elems([Param(Name = "self")] string self, StarlarkThread thread) + { + var strings = new object?[self.Length]; + for (int i = 0; i < self.Length; i++) + { + strings[i] = self[i].ToString(); + } + return StarlarkList.Wrap(thread.Mutability, strings); + } + + [StarlarkMethod("isalpha")] + public bool Isalpha([Param(Name = "self")] string self) => AllMatch(self, IsAlpha); + + [StarlarkMethod("isdigit")] + public bool Isdigit([Param(Name = "self")] string self) => AllMatch(self, c => c is >= '0' and <= '9'); + + [StarlarkMethod("isalnum")] + public bool Isalnum([Param(Name = "self")] string self) => + AllMatch(self, c => IsAlpha(c) || c is >= '0' and <= '9'); + + [StarlarkMethod("isspace")] + public bool Isspace([Param(Name = "self")] string self) => AllMatch(self, char.IsWhiteSpace); + + [StarlarkMethod("islower")] + public bool Islower([Param(Name = "self")] string self) => CasedMatch(self, isUpper: false); + + [StarlarkMethod("isupper")] + public bool Isupper([Param(Name = "self")] string self) => CasedMatch(self, isUpper: true); + + // ---- helpers ---- + + private static bool IsAlpha(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z'; + + private static bool AllMatch(string s, Func pred) + { + if (s.Length == 0) + { + return false; + } + foreach (char c in s) + { + if (!pred(c)) + { + return false; + } + } + return true; + } + + private static bool CasedMatch(string s, bool isUpper) + { + if (s.Length == 0) + { + return false; + } + int cased = 0; + foreach (char c in s) + { + bool lower = c is >= 'a' and <= 'z'; + bool upper = c is >= 'A' and <= 'Z'; + if (isUpper && lower) + { + return false; + } + if (!isUpper && upper) + { + return false; + } + if (lower || upper) + { + cased++; + } + } + return cased > 0; + } + + private static string AsciiToLower(string s) + { + var sb = new StringBuilder(s.Length); + foreach (char c in s) + { + sb.Append(c is >= 'A' and <= 'Z' ? (char)(c + 32) : c); + } + return sb.ToString(); + } + + private static string AsciiToUpper(string s) + { + var sb = new StringBuilder(s.Length); + foreach (char c in s) + { + sb.Append(c is >= 'a' and <= 'z' ? (char)(c - 32) : c); + } + return sb.ToString(); + } + + private static Func Matcher(object? charsOrNone) + { + if (charsOrNone is string chars) + { + return c => chars.IndexOf(c) >= 0; + } + return char.IsWhiteSpace; + } + + private static string StringLStrip(string self, Func matcher) + { + for (int i = 0; i < self.Length; i++) + { + if (!matcher(self[i])) + { + return self.Substring(i); + } + } + return ""; + } + + private static string StringRStrip(string self, Func matcher) + { + for (int i = self.Length - 1; i >= 0; i--) + { + if (!matcher(self[i])) + { + return self.Substring(0, i + 1); + } + } + return ""; + } + + private static Tuple PartitionCommon(string input, string separator, bool first) + { + if (separator.Length == 0) + { + throw Starlark.Errorf("empty separator"); + } + string a = ""; + string b = ""; + string c = ""; + int pos = first + ? input.IndexOf(separator, StringComparison.Ordinal) + : input.LastIndexOf(separator, StringComparison.Ordinal); + if (pos < 0) + { + if (first) + { + a = input; + } + else + { + c = input; + } + } + else + { + a = input.Substring(0, pos); + b = separator; + c = input.Substring(pos + separator.Length); + } + return Tuple.Triple(a, b, c); + } + + private static (int lo, int hi) SubstringIndices(string str, object? start, object? end) + { + int n = str.Length; + int istart = 0; + if (!ReferenceEquals(start, Starlark.None)) + { + istart = Starlark.ToSliceBound(Starlark.ToInt(start, "start"), n); + } + int iend = n; + if (!ReferenceEquals(end, Starlark.None)) + { + iend = Starlark.ToSliceBound(Starlark.ToInt(end, "end"), n); + } + if (iend < istart) + { + iend = istart; + } + return (istart, iend); + } + + private static int StringFind(bool forward, string self, string sub, object? start, object? end) + { + (int lo, int hi) = SubstringIndices(self, start, end); + if (forward) + { + if (lo > hi) + { + return -1; + } + int idx = self.IndexOf(sub, lo, StringComparison.Ordinal); + if (idx < 0 || idx + sub.Length > hi) + { + // Ensure the match fits within [lo, hi). + int i = lo; + while (i <= hi - sub.Length) + { + if (string.CompareOrdinal(self, i, sub, 0, sub.Length) == 0) + { + return i; + } + i++; + } + if (sub.Length == 0 && lo <= hi) + { + return lo; + } + return -1; + } + return idx; + } + string window = self.Substring(lo, hi - lo); + int subpos = window.LastIndexOf(sub, StringComparison.Ordinal); + return subpos < 0 ? subpos : subpos + lo; + } + + private static bool SubstringStartsWith(string str, int start, int end, string prefix) => + start + prefix.Length <= end && string.CompareOrdinal(str, start, prefix, 0, prefix.Length) == 0; + + private static bool SubstringEndsWith(string str, int start, int end, string suffix) + { + int nn = suffix.Length; + return start + nn <= end && string.CompareOrdinal(str, end - nn, suffix, 0, nn) == 0; + } +} diff --git a/tests/Copybara.Tests/StarlarkEvalTests.cs b/tests/Copybara.Tests/StarlarkEvalTests.cs new file mode 100644 index 000000000..9ff9dc5f3 --- /dev/null +++ b/tests/Copybara.Tests/StarlarkEvalTests.cs @@ -0,0 +1,111 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Starlark.Eval; +using Starlark.Syntax; +using Xunit; +using SL = Starlark.Eval.Starlark; +using FileOptions = Starlark.Syntax.FileOptions; + +namespace Copybara.Tests; + +public class StarlarkEvalTests +{ + private static Module Exec(string program, out StarlarkThread thread) + { + Mutability mu = Mutability.Create("test"); + thread = StarlarkThread.Create(mu, StarlarkSemantics.DEFAULT); + Module module = Module.WithPredeclared(StarlarkSemantics.DEFAULT, new Dictionary()); + ParserInput input = ParserInput.FromString(program, "test.star"); + SL.ExecFile(input, FileOptions.DEFAULT, module, thread); + return module; + } + + private static object? EvalExpr(string expr) + { + Mutability mu = Mutability.Create("test"); + StarlarkThread thread = StarlarkThread.Create(mu, StarlarkSemantics.DEFAULT); + Module module = Module.WithPredeclared(StarlarkSemantics.DEFAULT, new Dictionary()); + ParserInput input = ParserInput.FromString(expr, "expr.star"); + return SL.Eval(input, FileOptions.DEFAULT, module, thread); + } + + [Fact] + public void ListAndLen() + { + Module m = Exec("x = [1, 2, 3]\ny = len(x)\n", out _); + object? y = m.GetGlobal("y"); + Assert.Equal(StarlarkInt.Of(3), y); + } + + [Fact] + public void DefAndCall() + { + Module m = Exec("def add(a, b):\n return a + b\nz = add(2, 40)\n", out _); + Assert.Equal(StarlarkInt.Of(42), m.GetGlobal("z")); + } + + [Fact] + public void DictAndStringMethods() + { + Module m = Exec( + "d = {\"a\": 1, \"b\": 2}\n" + + "keys = sorted(d.keys())\n" + + "joined = \",\".join(keys)\n" + + "up = joined.upper()\n", + out _); + Assert.Equal("A,B", m.GetGlobal("up")); + } + + [Fact] + public void ComprehensionAndBuiltins() + { + Module m = Exec("sq = [i * i for i in range(4)]\ntotal = sq\n", out _); + var list = Assert.IsAssignableFrom(m.GetGlobal("sq")); + Assert.Equal(4, list.Count); + Assert.Equal(StarlarkInt.Of(9), list[3]); + } + + [Fact] + public void EvalExpressionArithmetic() + { + Assert.Equal(StarlarkInt.Of(7), EvalExpr("3 + 4")); + Assert.Equal("hello world", EvalExpr("\"hello \" + \"world\"")); + Assert.Equal(true, EvalExpr("1 < 2 and 2 < 3")); + } + + [Fact] + public void StringFormat() + { + Assert.Equal("x=5", EvalExpr("\"x={}\".format(5)")); + } + + [Fact] + public void RecursiveClosureAndConditional() + { + Module m = Exec( + "def make_counter():\n" + + " state = [0]\n" + + " def inc():\n" + + " state[0] = state[0] + 1\n" + + " return state[0]\n" + + " return inc\n" + + "c = make_counter()\n" + + "a = c()\n" + + "b = c()\n", + out _); + Assert.Equal(StarlarkInt.Of(1), m.GetGlobal("a")); + Assert.Equal(StarlarkInt.Of(2), m.GetGlobal("b")); + } +} From 52ab1caea49d1b0e4b6ce22ed3e34a2e0a5f8241 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 07:54:37 +0000 Subject: [PATCH 09/15] Add capybara logo (README image + package icon asset) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- README.md | 4 ++++ assets/icon-256.png | Bin 0 -> 43312 bytes assets/icon.png | Bin 0 -> 1495967 bytes 3 files changed, 4 insertions(+) create mode 100644 assets/icon-256.png create mode 100644 assets/icon.png diff --git a/README.md b/README.md index 8282eef68..28326a8e2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ Copybara logo +

+ # Copybara for .NET *A tool for transforming and moving code between repositories — a C# / .NET 10 diff --git a/assets/icon-256.png b/assets/icon-256.png new file mode 100644 index 0000000000000000000000000000000000000000..9a068e0cb4d9f71243550807f17d5786b3a816e8 GIT binary patch literal 43312 zcmdpcWm6pe6YVbUy12VrAh^4`Ltt@uaCaxTyF0<1AOV8Ay9al7hs*!oZ*i-pYUbT< zX1Y(G?sGaqSy2iZ0UrSX03gdqi>m?vz^_Lj01oEsrtef{4gi!r%7}}od#s+lyJ!E< zautjgvcB{kWl}3qY1}iQ!V%_NhKd&tP*%u+@9YQ9AwS6>P~ss6PD3{Lv>JQOK#9^0 zqIDRBc?rcpv_Ao-**Rwh%b= z4EQ*y{r~kEdM^B+I;u5gQ&6-k{=T+2y?jzH)yZPsTzyzw*`uZg4rukCc&Y6A=tm7u z*v;DerNH>PDd;dqtw=KTE>@feyj#kf*O`(h;Id0qQzO_pv$m-1?DsrTTOy=y;t8eH z#Eb&&bJu{)A;m=_JdM`}$UFQ`V?G@>=Tpny>X+mViWcqlYa?ECM1%l$OD68S!B$?F z{ryl@0PQf>6XeUM={&8DQ8~h6el=N(ltXp;nj6xP0JM>FGU;wsu`FB_LMZXpsk#qa z!jR4PW2UsXz1~U|BtQUH{d%h_g_43o#>c`1Y`}*XW64Vv_vTZc&$(7ABc8yHD^|

a?t7_hylwA#`-8}J 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

=yShc$6Y5PhzCOk3eJ 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ξ+4ccTw|6tD~@QPrkQ0*5fW?RJPBSGcrG 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%0D70TFNYEsWBDyN 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�VnULpWmfc8u;%Kh1Rz_Q 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

2()6lG0&}s`UQ2vs451Tr<>esH9bFL=P-eK zXYJuyv(p_Ess>=01aikdovkp9VbS+)#WAkK8kZ2(5X&wlHqi1`YzQjJWwSK2YWAf2 zXYHv`XkYj6%*TKR@N_dJwS}eCsjydiqlifJOxfL}>24`{~oC_~`wY{Oq$Y^w)pp@9>v@`Rjc7;sri@{{wBH9!y@;|M%$%{PS0Be%F0u}VRAV20;bxRs@#;17E-61b?p;dF|?jzFzBU27RPAR z*5-Mwt|E?LQ&?#*^XtsWx0FN>e>AT%<2*$>NL-wG!cpP)w6AQmBD5D?KYpqo`3^$#>?=R4)&=0gjFwI}D4m8&-Q-1r+Z^;aK5N{n`KSNL3w-UHevfk! zyA3l^L*_BP<7uWEpRko;L6~t$_E(wmyI+?!@cdb=ts{>{!$r@itAtYX9kU7`!@7!l zawdZ@qo#grynOFL4^_IhM6PNOw_Ogd2LAZIOHNvuMiJ4IMH6%JHYoj}?^iDi_OGmi4uy8u}#RR=%B9fzpG16MxA z=Qy#;HSDVB0%>mV<}VJ(+i? zQ*59-`Kpg6i}!CI)oYbdvL7hs^N>Qp8co1`d*=M8qCpy2)$ozU208--1X;;Ic`P;Q z;sV8@&v$A*9nDd+`wcsxBh-_4L(me08+JvzM+Dn-5=zeoN97cxFk_zlxD!Mw1(sxk zQo3vq7onzmRg=WiWhhS{D;~QkVEnNdwHJ>A$N>$gzE=V7HkcDJyAVwqb3Q%xmNdtJ$(^Ibxwo^h!Fv91q|rHDt7Z*RtaJo2$1|)Wk`E`dY7XKl z`D1o@Iw!hTtUWc{Sc;!h;UBUfKrW0vSA1osdaLUshjy?i?*m6a@6wzxMC9x_51n7< zd7u2$Akz1{T$v7z8Bn@;RAf^W_R`4@81ym#w$2`9K(Yp(B}24=Yhr!Utuo>@un%~z zX*CJ%tHG0nmGwSN5;3(*+`KCuIL|IJdVfo zM(2JD!0Ed$ATwnkjORe8q2SfBR2zZ&V^9(&5pUCQk5rTAj1?oi6IO834RbO z@qd7%l4^NXr`a0QBXp%!*9V5VO zJf5^@S}+uT!5d{J`b|LbL*pa}=i0O06KIfkWa~CR2VYAAo?MWBPo2THQ-n05;ee`b za-}B-ARW20`JEj5FYHulPlS6LMMId;KyxFfGS&B_D&RBFczc6$7%;#Lv?&!1Il&lN z<`O!|Wak--nkB=X{H&sM5N+s)!Wqlr&%9z$V3h&T(*}j=+)>Cs$O+JKMWTPNPeU&z zdiepi4+K+GHHrd4y_pR$ljo!QAT;LEW|b(hX^RQ2f~NZ&0{Vm_D7~(rV$9>rU`sXz z4|9lROi_+0Mjj$*~MxcYYB+`?r4)-K~#5ex)Z5Ph8BS+bXzP;<`iQ9oI(BAr62Oqw#|H+^E zr}YD`p4E$|ANAf>`Ji(TFq6ShpfB_q{s_zZJpmw)wHoNg)s2Un^zfu2r@hB^qbaGQE%5i}mg62laT%0q zLNbcQH&^4m_ny|iwjgmd@!O~%v4t-0$_rbfSc(bJIb@@E4cJb^Zd&j$u2*i1hdN}) z%)_(0-Nverf~>?(AD&Y^RU^}NmK$$2&|P*bcLkjb8>oc>a5b^pg${5MaY?vjNBuq% zQ}P(|bvbWxrtN4L#f91vlZT`)7!4;i5!>J_N2B8T=avsQ^$?_%=UBINb<(5Vxjx8V zV4js>yB{@HquRdEmVo=a(fOPL}w9O-S~>2owC^Xl>L!W#c~N3}-X^DF*XaXSM56_$TJ8*4$uHy~a4jpx zI!%}X+pg$tv58ue&7l*6DQPLe0Ew-Bwr)5Vs&=IYl7JjL7ZkJ%k`Ri*UFUFvif%y( zX$E6@vZ9<&+Fr~IwM%$;wB}ioz?ioZ*xKA6ZtWy!B~C7%_gfOHwV^Ejb)Zj+OC_M*sz#ugu~U~!iWBXZe9A-sK#o{BDQyO@{E@D{0b3sM>kt@Q}3^ov&S z6ttNyS)7V-+fm7VV|^(pBa#4TAk!#zbypwoWi()n+ z=a@O4rg%0>G>LiS;B#H~Jo@yk=Z)p40$8TsQ|z7gOs&abmq#ws=lR4A3|_#%4S%)^ zqKE7@c^$N>@_FNEJ2VG?6d!v?rI6)HkubWQ6vqOP7?Sy5Py^jXV^kq3fx)vF*27mk z9s-&_o}=9HDl3eW{}~VD*$_aNTiL1z#^kV?AyroJtb6$no(7EgQivRmVvGa)L_7y6 zMZEZqxs61@&K$p65o9m&d; zugk;+!k#NIQ*pZS3K$m2>VW%$2J+!OLBN}Bs?dHrVJ^=mmfIPai}Wbx9L7iBa=>&R z(%h1J>ZcDrB#C>U{9J~3nBJ5|`>-C+9P_;4Ga|W2`kBCx=R2I&P*>yB>SwFGTd#w8 z_HK~ulPt0x>B z8jd3vXdL0E7sJn11KW<{%wC4Kftgd5rxyxf6(2#5@e=F#vuAkr^Z{0h^%2D)=@=yR z7Id|4BwoGu9-cis#n*oISNS*o=FiuspM4J>e(=6tJb&Ju@qbAK>)^lbWLX&Sq-4{H zM$TH?0V&6yc*ePmxTB|VML;n`SOrSUqu)WD80GLO4>+;a5W|q_Sd&82P~|5Pzu8pYHaAL7@#DYiAMAha z|L{-o?dtVAKKes=vp>ZUiWP%ZK^{9+L3j@DSZnc@f9t~U|I$vPW{-I!hfKcH!A|M;Nn}W7O z&8%EWCynkkp<9Rhil3W<4H|CM_hV7s$L=Vha}+UkHS3Gh6)(99osHr`(om6L^_=~R zzZNGypMgTOUyhh*CZ*m~#83jjcDtj|P9d6MuIxdNF%veXH6iC@#`dza2B8tC1T=4# zbyKL|xJwWyqFVkJ?S_IYoEMi}@>7`6N`flCkQy1gzuQ#x7D!mv?|j8R=AjTVxdugI zhA2wj7ctk4q}Pfvp~5RuV5XRw)A1jo#Cd9>H-CRwPDfrf|a; zQnYg}KQceY>1MS@1CcqHa4REBtdQVZBzBpQW{mTdgWGc4MWeat%YOSsb5-F-fAEPu z{och6$YrPD{1Q}M#fI7a<(MOvCsZV?BZGA{$A8C78`@X}T)kmeOSx#T2GB0;>{%R{=rjcsU!J#2*xdn9-aIK}X;39OLd!`bokltF zNoy4*b8v1&m~_84uajL#dUWDX=Ix@bM`egJx9XLjr4l7W5kcMPw)29uOO#MJz$(M!&bHpeMX() z7$^VtYg(RilH5AyK6wciOrsxXbe6R0mULifo0q{d~bU@BsSF9(|Q5aJx z8?94E&4Jl-t&K^#=kG@#U?95#D)$Kl>G(aHJ(*YjE^G-C{Ur2E7xk%pz;6$02%cvz zUAQx_4zlhZ%7Ty+Ht9u-TcH8;;NQO8Z$HjBJh;Pc!@KCv5d^!1vh z1G3T=)m%|3cnOUOU@C7}pLfd_WE~UR2gy6n0_B~TS?D;v*CF7x8jZL6(j!o}Zi%~9 z9#+@IO*MDBZs}|81RqTyK4)dJDslwMssVUN_hVAIbXbZiEn=EQ~b4` z`CIt<*T04L-+P4*UVea;#_{5mFz?8*v4P{IX@ap0 z{CaYOj#E~;ki%j*;NIN5=SuQ+qMKwMWLLdF?=SF&fApW`zxP-E9sa+6>EG=?{QbXQ z8*d>!l<3GnCbER!L~=G6v*68j@mrr0|FxfdPv87ZdcE7PMR#QlN*u3^!723UZ47H% zgpb;cQ_xvh2&tDgP!{Up#(oq))^fM(m|;Mv5p@VRH>fB%nd(Aequ%M&6?Zp;)~jbv z@c8JFe{p2v5SZb1v6vZMbxQ}g6C|5k3yN&M>|w%|SY@e}5_ferSG$Z-gfsog+7RYe zlgi|T6V#<^ize-+SR=TOmgLK_g;UX=GvS=iX(e{$*lTZkFHgd-nCYlK$EpQgxSu8Q zTXWz3JIpnN$IIKmwL@ninO}yC|D0x&lX3N!E3&YA3k!-rnY;I0rqumB@FCL$d8|2i z28Q?w36z=~28j-dS#w0p*_sq9lP6Fq_yw_jVg3P2S6om6Y!`I47|#|QCuV(Z<0{^RTVO$&x=c* zxagQu07GjH7)Wxz<|dZIjYm9|I)vRafS6RZygY_&H5aRpnyx6qq}=Iszr(ogfSVoH zy3JOIP{6slSt#Z~I-}(%^ipr2+-nr&B$*Kh%R6i-2~q`$+brwY?Jhr$D#g{WRF>+X zm}JZ>9h7}EQtI9ftVQ_*;99~e1Ih-_wNyp*wab17t=Dg@Hu~YO-1zCE6=Sic^p-5O zyS50XBIF`p`V}CL@kH>R9~QF!P)$5t3vXUO_NMs28(J#jn&{G#dhW+6u=GebLptsA zzTZW@P4ir`KL@J)!SJ+SG5IBuXJ>IiANjTQ!o_T;5fV}}szVP%Jq*nS4)U0P~f>RPuVOW)n6 zb~^c{^Vu%L#Bs7ugz-5Nc6RU&%Ec_#dL8v$kZFw6%)<-e92V`-w1a2z@BVuy6dB{1 zpyP8{`5AR`b!Yfd`b_>?iP89Y(Oo|<8)(jKP2tW`9WOo>NLd3RIXBt`lqx>>{^xE;|J;w<8>S+n;-w z7|p}1Nq8`SGK|NN4&&`U*5O7}2RCzH;0TH>o7h#U;EV?jleexE} zws+>c-g!<=I1jl5g6Ww*0qK6L0sSd4VUmUb*b+*%vRgxSj=~}VC;Ks|N|t_3N-lrj z(4-Y{{E%BG$(68TYFV>1@;}TU_(5r+tP;wdq~jPp#&hOfWrDwJ-Pc?gRCO1ts7VVc zNIL~&qC6uDP`QVUm?~5nh`oZWFZOv^PCQlU-s1q1m@80Fn+%W6MF~1CbP2sx#Zvp^ zv<@S|du#{xwY#sahuedmJ%3iWhlOUl*+68WTwly!L4oMrdh&3?`>)=^>(5{7Z~d)b z&@cY--@}uqPw??a??boJ#vUIhA!fhZa~|&S$s4P;i~yZ|hR=}s3{OP$N>7XZEKCnM z3LjRIY))zsY}zi^^I$Fcv`8f>yll~6j@}t3v!B)OXA@jaF!jYPs;^65+Mo{o##CXC-n4imcbKzs(=`;18gltN$1O*Z&HC_{9@0zQlg~ z!U7m~eGfa0PSPP4#1(Cbekk+ab z$8ai#iL1=wJYE3JONG7dp2fu=1&BuCz=aVK2dL1yl`J+2twzUKRB~_CZZN_C!~t^3 z5AIDCRKH2|=6Nr8SbYiX%PtggVMct{lyNRqmGO<#WH# zs^$8fTO1ry%mq3*p5j5FzyaV>(s6zp@`c(x2e}jNYdc3mD+-~O)ek;Y56RJyubmwz zK-=9-4v;mkute|%nMT_g*f^)9&$X0ewP59hApl$DP1khn@kF3eiE7KatkcUP_$XJ4 ztq@XrYTW8pr2qvUmU;4!ELF$Sg4iBy;u5}Dk0Q`zAxy%sx5;D*oiH&_5N1`?)768c zic2;C1E!o(KzDZr$=3M(c(bOZ(UV=lOJbp^?oqGYN2q~ZO?aeE-ze;Ey?NdE?VmgW zyRqG&k;J6ogp<8Nds610mD-Ewek44;W31sRM_cPm)Di8 z79r|t6}mmb#GykwVplIyNow|%SUxc}G9EYbc!q}RL^%JOi`+X{?rI-;>qHapoQ7s# zP1^8C6omJIs*?C zQ14t37|5)_ta(nDP#N`xvwY|G#bDsRW4CDG+(e-yJcDJNfFIwj8l0u%NI4^-(rEbe zOcv&tNV*pZImSC7b1)D7D_+P;7^Wq^m^V?GtH?1o8`Bh;5Livb{lw309wKpM50GDw z6149tE^$ygpstZajmp@TTx`SAxAu9TuoH$k|1{AZlPM(Goc{YJde^z0VYI*jlztr! zwZcrhKGOey=s6SbG7=u(tl0*g9vhiSTRFH4eEkfqJDi2&m)KeOmEG@G!~oAB5t4Cu z7aTBudIrnD)~unAe5)P35Dw$0#+fp67T3HG-=FHv<-3$p`Cgx%GXV(-_=g9mI4{&$ ztLN}ex-jcwl+g|ME+PrnNSSeHBo0~#fLKR5Gz3S_I?DaU(&Zj_U}At{hL#G~u)gQ! zdz1w@zjyvtPTc&EdHy_YykP!!I>@}?3I=RQ%gVsNnvJd#z?HS1QcMUdVrnQ&w8tM| z@?|?iZ_zoB-<)dPK20lBd-j*F!cp=p$w=az3Sxk$-)UxGTHj_{Dq<<#ca)@M2{S#O z1vM=I0->whz1pjuzI>*qPalq3M29S@DoyjAOYXqB39nu~=WRXUmwxeA@UuVvRlezs zk3W2crw`9@rBA3(o8yeP7R6O%7!KOZ8>tEutJ2A^;A&v}pIDIkt`JU>hpS7yg+i@q z%)21oOqX~Iw!OxRpy`*P9_CmagpT?S-M)4I-6zS z>t>z;DK5)AVD zb|zcHB8vwoev;{K<%(?wH>(`<>*HR1ZdF&F)7* zPj2jIPfL%Mz_%VSyE`l-({OOFWT|Zj;k-$^A*}FV>D{#2-S;xUE8#7E{gwQ0ITZm1 zU<14H#WABi z*tAM&Du#AZXapCsoyTk+4tD;^i7i)&TnXCS{jNo!tbwEFmH6nI;8F;O1DtY3pKcLt%~T2r=h{-x;uF)Xk34?u7^ zJU3&Et_-wFEok{kBKySECb}BgE*;sFih)R=JNX51{G}|d679ZFva%yq(Mf^O-QwN= zYT4ezFSE^WJHN<@;=vGXw@zhwnRsM-7jGu*vXvGwfi zw2}I$JN_mr2>>&+aHxvYU3c)9Ac*B7f+0Bv zl*}5K1ze`Clt0Xwj{)z)ehnAp95cDS8oJ?(PIn|U@I6IL-PfJlI6^8jz6N=s&=3R8 z9tqVX?alH3L#P%H#lB6d8lb)4W?vUIph+Dv0=_Q0Y?=MIK&lJo@Jg= zVh!oG_9l;lx+k$pp6=O7lh5NFOzW6>o@G9}BkNj@U-m`~=r?XNQ>RJ7$8de8tzn1{ zE{qouWDL#};ydrdqF0x-@+z|KfSJ=i@3L@{d%HB(ALF%bK;J0AWg`$A%^vcFJ}wO+ z*I}`g12a-7oU#u3fFouh6SRUPpgOb0y%FMg9E%r>*~P-hM#7?vse%*X95&uQMR{Iq z`ofbe0J5yjfp~*c5k9i=q@%-A==u!Xo>52g#l^CK*n6{9JYJu{2bv_ejQRXH)xq z+L79|mB2nP^q~V2$l?|h+?!w)vZFd=%+9lFyF8qA%d#lC`gU&1ncoDsi1InN znidVZr(J0%t0APwbo=_B2|mu~9B}6K-U~2(2iOkAi4kTYJJ(x{85{_biZbfWyS~UINuAq!ZF`Nu$3R#+aIi9TT(tX+<=UW0C z?*Z3tgJx?8;)W`-^F9r((|h1xd3$oJ7tfyQ>60g-u=?69S6&tCqd`d#_7KmXJ=crp z@98(c@hN}mXa5er_1Smu@%ta*)yrp@C3oRA?U}%Gp4C8i{D{i+SnnL7OKn4us!fk< zABx5C!5)w?XD}RBz=9UhvfK=i+?H>KdRY)Ag4HQPX5$e);320{1aP_gz=aiS^3>bz zpM-=>RBdCX(lQ!~^H>_J+BvsZj-MX()C)MQAMP8GBPhak1lJCRy}GV9U(`?j-am=| z+rRt2)!+EdpV~k11HT_{-hPj(Jo8Sdgq&FH-JTcO0g;YBG)> z__j308&aFDg?pHILFR*H!0Rz-l|sjmg;Omksol=AD5A+RyEPgm0^Hys;$5Vr*@{j$ z_Kh$m%GlQudT*+iWxEUV#`ZdPqmzm#F*08bkq*zM1d0IKhoyt-zh~B|g*gFNca&9@fF*`6D znc^O3=E9}@Tew_x4S^l8^P~sZJr~-?Y%1G$A<4OmnrAdhPgzJ~>yUH9lwm<+Z?)Y~ zl5D7|APe8U?#4aPLIg5Jm1YB;HodLLKZkD;TN6fZ>@=BGZI(6Y&q3A-kz)Gz(R*rk zj+@!*$D*qnqb?MUoGS^VCeDY@>{K04f;@P}tJ^{yJ4Tq$bgUJIOQ!n~)Yu6HoMoUI zg^c-a)uceQVglmI*E{K{$4B73mp44HbcJ#CQ?~%@cJfyy-*hMd*`rh$AW+>!>6SF4$i#0-vz|BLS(bQuzs`6)ImyU;uFf+e9jk1B7D zyWP!~wW*A)ZLnKcJ012p+s=UdWG~?!1>iBdBuIAEQb1*RUFX(J54YRCk}J=cIUdY| z$j*}ipQ>>9FBb)u|9)r^=ZiePRxCQt61d0L3`A)+&6BC@j zFXx9wgdrI~o8pDs=Sb*>$7iZAbIpgy;!y`U`W;oM1)1 z3`}v>0jIoZYbx8E!(BjG9>00YVVIN7E%>_s0T;S~GYgs>wRFHpD-v!$eiv9mE*uk> zus&j(Acr)-0n`SB1h8g=jg5X<=hgb?b);CD4Bs1hhzSSHoyz_FIXVHfIhh&HdsP{5 zIvLfala)3Ytn}dca4fhc>A=gW4q^J-{aqEXzGZeaLN~wko~mM4CT%bNe2!bc9ap>H z-uaU{_&!c=Y9-*n0j2@hPZB6B4G+jwuIE5(|16+)S|Sq3%Zn+9$e%L{I^XXkdFq6D z4+d@+`I?ABL`^T_Aw?#r#w{d$X8v9cadj4+TaO(r#i{l&6@0y9Y2&pvygT z?1unM2cy)HUf~4bNwCuWGr+Y}L}Tl4>PDcfxVe z1}b7}cV9LUoh9aX*R%pMGE>MMfChoxY7|u32?4jt$Re`U++Mp?MA?_90HS(7qFZ?K z^Z~Eld#^tK{0si<-}-s{-CzCne);?<-h1zTX$kw9L$b7GnZNx!JV)6C2g1dVc)^Ym zLkVoQs>n(@;V0y#ObWW3V==FsGq}%~DIIX(cUaq8X=zJP8J)R7G^S%th=&lh>ei6l z-MLzX7h=Pp5R1udJglk4!U6H9@ZEZ#ia~7F9rkuUVb^j)v=G@np+HmgRloV{+xqd} z^`FzP{Kntb|K`vAi~0kfd{T~q`|P&cauYSYEe}Ce~p5d3iv2j~@ zWMfDZ%iz{MnL%On*4_)8o@B)cY^SoCTDRxWg4@`53wpRk{h(@(JPm{}-s+wJAKGBd z)QF9Wbptu~r6vp4b>aE*1qpj^*E37h?&^uTvzO!{f-!ha7;3gbPQX@C0q6vM1YT1y zbZUWyITAzIPLgrJ7@B*kl-S!lk2ymf-iZy5bTf^mAOeZKe59n})#?IlH*Z4!owyej z9G0LBN;jY1+hkz?zcI};mwNCxiK23T_=qWC8PiTx%jt#hoihZGvY$ZQp~yC4AkjuD zsl>ZY2;}&{`;wO2Duh(0uE|xiX5DY=*CsY}Z3m1n!m80ygzjtS0~QVXEUfL_u9}0y z@!5uV1#e)RnbjOfDXC)i=mjr0xnH}qJ$mRk-_s8#?IV&p&^} zCF14@6y?T*PT3CToSIIio1(ii>@7B3neV>jQj~noC5@j;qw7ioc_pi>>CXlt!1PgB zDMguzsY2lEvAz9UTp@8d-3~}v`31yENW;41>VDbU~;AE!^Hj6&UKg&8r`Z+RscS2D1 z0W-&*2CiY`9B)CuW)Qd^;e)BBIC?tQ4_@#;PIw65n03?l;kVcR{1eKfyr4y7HU{?9 zSZI}F82NtpJYejqIff)iTSuGZNKWiEpPv14LV3q5A;TQd>pU9_zLm@OHOC1@FyUvC zv(5J%trTw2j6$kFUPFHhpPO(pd2Q23xTr0TQMGi$5>^@ zFz=@s~ zQ==zTVb26NMljR10=B*r41*IKNEH0`?yUPi!f*MV>MDkXa*R5&%c8H3f1)n{?LER;p z0&NhBJ?Xx0=hnHZk$M-jN?oX~5@TPL$q|^+3<1>ksa+MvaAYKalRnzs-U;-{eL9EU)1aEZ!jE{~+ zl!I-FE$!@{EZd2WmljZoD4UDI9>IdEK?C{)f#x1uM6gQHd4A^voWw|{{3RQEcQdcs zNg6{Tuc5nJPT4daV=Oov)Xb@p4>HvZ@D#wRB1K(9PU3cz(+M|xZaGP_*8oT?r#92A z=WE@h9kfZ=PVf*0@q)M?i72sHRn8k~xjVU4xIs$I6`rD#n36F{(^fU-J_~3r4v;`w zd2w2mQ{`mE2x||L`TtV&?=ict+jST;#(duITbF$~_mktxi7&AoAKP(k;zUhTk_rc5 z09Qb$zkv``0#&GLg`lcHLI{aS}$LUFKEBea!(buDq>f?dA;j=vUI-66!$y6kW!eZaRHEgB$u#RiB7r8>m> zHrd{35iC(acB!}hK}BuS6gl$Nf#~SHD66%2>7+R`B5@sQPX3tMX9 z{;v9?PhUIiC>o5K&M+|U65Pz!mUyr)xo%2(Jf^wAbrBPQeg5s#xMSI{U}#nau=*k= zdDg*5kUusi6&+dRT4_fWsC^{NdfT(CTa2y<7kJGVhDB@uUv$XhEIez5cDbS~t0ec= zrIC75lG7flJ-^F^V1v~X;VT!|zpLme$Tp9Oy{iO^Ym6&6ZV%H19*VL}4+EITo-_Aj z*gK;~MI_D09C;-HcDS3~a!g!#hr{@Ru)`FPmhA_lmJ2~`TDEyE*`mA;(~j)gTo~DL<70mY*~H9g77%`- zPhB-oS0<6kg~td2P6CzFnM&*I%wSsxn(Hce%F%)ctT|iVV_m>USI0;=DlsHPAS<7wn0^{05 zuY&UbE6?FRBFmFHg~{FjL(h`S=;61y1hce$%T91t<@umKy#o|dZvZpm}19M}-hQ=$w~9RumJ)VE3)1?{|@ob*{0b&RCWCKQ2j(aOc+IU~Z3 z4uywTrH@ki|EHI@j1UJs^WHnI#w4;%;xuSF(Mruv<>HSk+o9YVFK{_1K@4-vU8X|< zaW#sXe1_ahEQA*__HhiI5jNt#M(BxW6uZi`fe)%VV zrT@zR_8;qaKl=dR_}b5LUtig|56U`4!3L-cfTc^BfGa{FX|upnK$s0Ut0L? z$5&m7>I{t+PM5N+eo1cOzZE_$_>i0^6X|35Kqj6|%%NH%QIwzpP;WFlih@oQcxEK| z3Kxn#o52>gkAZrQW}w_Q*nEF~!S;Zb@~d`=lTPNeyWHK=z#h}41S6(X#R!@p5Y%J?H6cX??uEy7} z%2sHlg5j@R0?4$8m_aahFyvy=oENPHQ0aMhb+1ifGFVzbb60}MQ*W9vZQUN>ZtS7q zpRBZsHHnMgRu%dtN0Q6&KttY;WKCPDw7@AodRNPZhHFnmFBLoKr-HQ^d^R_v!^p9F zu{7SUcrpU4<;`EZMU{gNJE}Y&vxj+Gx_dc?k1l)_+wyZdCfUxDTCE)lVg0U zkfcikZaIQ=jHp!mORX(zvvolE`g^Jy+U*%4hYCy^1ZHMkiNZBLblbLu58#AIf;QNr z=}w@b$_$CL0(y#`c9+dCI*gNY;aAs=jb)KsRG2Pol;GhFVRYV7{_^w?#ErYV-!6JH zzPob=ukfS*rl4eIopklwfT#BrJ%g&DWE&7+YSc7LqU5xsA({qUe{V}Q7-7V+mMAW3 z)r-$w_E6aSShq??;k2jv#8#yLWNNSR={$}4)Ji8+Jc7nIz0Sm%m4m9Zc(G8MntCAx=0D+RBqbM%#|S4@w03Su>gUL~=f{Bz>IZB#WfG0JZH^j7sfu0;uGN5cZ!=%KgZ9xN$})*Rvk$Jaeh3l+zbn? zhIKFigD@$bCLyOkciK-mx#=mWYQE7qgX5tj>bT`P2?E@vJjKhj#Oc=dJMlhpCNYV4 z0*O;-B)}j-u%g8YG?;isQ=?;Y05Cwo6hXX#6Vp#2^%PKjSRnBfO3iri^HoThqq{I8 z7KM#9`Fm}gQGw$I<{eW=n!=^O{!~nFG;nh_v$#3o{!s#I>_FZNld!p8Z)hw+lHnY3 z)2s%)Q4orJ-jvz46ET56FSkBYQFwB|9S`(q=OWhE=|AaPD{S2FIb@9bCMN+@l0M!l zqfCF*93b<)Y(5L(l_2%*E0OZsdSqlF&Ye{D#&~@{3fM{LgDl zL4u6Rd$^x&*7FtyOwKY&`8)!0aG!su@pb_Id*>%MR*lKS>qfOv1soY0$7xAJ4RY3DpoG-ejZ`24JZ1f|lelH98CSmIljJ+mC(J@1^sr<& z&Lut2cEbYpj8rwwwF!7=$f1JSc35Ly7GV~XrZ|2qP`+G(7vABO~g*{^v@f?C4Xjt-HqHzPK)H4~d%#Y;v^-EVtIUGtC#MB^z~6v@%)I zom%Zo!;uK0C4!i)Yth4kbsVDGl2FRfZ-FwOkO?v44wM`>R#I^ciLJoyvg6NGaw|%c z$J4jRI+EyKVARFtoHc8uUp2|N+4Y3@`t`^9Q-AC~g}?DP{{{Rjzx{u$pa0TV2|ZC` z%Qk8a2mnmWv(K<^s=zxI-~056D~SKXpLwotfA}Dq^mG@7a7yv;0I)q~&&NyT5?0Cp z3H-<-EiHnEHL6KfaS>H=I~kQ~ZB>`qF8;$#@F~&_<#2bj#X>mfyJPsKd8_t}8xZ=6 zXHW0Yd$Lqn#W} z$aJG7$SbTCCzLiE{-&|NaHg6`WbbKoJ0V9}Y#roUuOi!xim(IIe&HyhqXGevyXQj0 zXNTrr5U^4TCa`vniPg#n(^Q7ldhl&;U=!sW&z{F|fF@{e4dZ?OC)(j0sy8N*!?+cf z=}P5(ZZrW)@KsVkCgB&>6a)!b31Q_EHU60-tA=)#XIn$7qR zV|&ZmK03jNj8m!wPrwU%-dj%DZ1>gPQe}Dibx=Ma8a{f&uYnN92~28kOe6`%JepxL6gtLI zGs>)UmB#-Ow7ge2Rsv8f+!rzk0)?w{-F>kD#R=jWP%Y4Es#av7+Rz3UMD>lt${l4v z01lw@JE4rowA3XbBFM^Lm^ak2dg9xk(_3Gt=9?ZJJ6qHzQOQ=O%7LkU& zmaY?AP>m9{hx3{Ghk)IVR@V|f{^S)4mk{Ej4dccMgvpO^rh?}R z(nA8VkY(XwlK^Z62^=t}ag2qw9krePh!9LVz*qGYX{LCaU#WD-00z9G^=}C zWK+KWT_D{jfN885uyJy`!j>`tLuvJuqof#=7)C~gFn}r4xEfwjxJ1_Dk;g0XkdmZg zFSo_+kPwoLYQj$cb05AjJcR|rcy)~H8li&%>w^waJ|JRx$U&Uk&3}}{GhzBIJ(ow+ zBICYK>|3`f0rQ+)r&^u+oX@@W)A#&-K!T(#=by7o$#FQB${7hKPUp!Z>R>y-5iw6< zuM|lK7eA}vFaa}lVH#{FxyFD2eOnK5?gtZdpG5KCrGV19$xJ@64$fJN#4I=~3xfvA zMN8s8>*2eTsCs_aZ8}+pd=}^Z9H*QqEYiu*CTg52ftJwSVAUB#IF56!bPVy0DVIDm zok)2&Ma)iq1Sft$HnMnwD0QWpofp{Q?72d_a&WR4rmK2$>@321R@m&5;>|)#g|!E} z&4mtCroH*4lYx4HBg8eR*tklHL8F<)^VYF}k<)^Jh$LYc2|2$9Re}QIWkcESAMMjb zv7K;sJ0ioz=oP3KU~UeVH`Q-j2XrBkAgto#ZijA6*plCggJ4Cg!Bq|?@0v9u&a*23 z$uSzqIQeJ|I~hYh3__((6agnD>Gpqumiu=qKm+7}_0gf_1n1cl0;dN*DI9LtYJvm_ zJ6Uexn20~QcYgYhtj6K@OBUzTa+-mB*mr{W0fi1YB!F1h@mrc8xMN5n(82&NV;D^F zN-q+T{mATcRX{B!AkH0f1_5k50$6gw1KKcO#%y~5P&cl7gIe2~`71^0P6PY7yJ|yn zhzA4GPF}0c&2}!E6*lDIer*3RHDMa5-lBPxq1%$>nyAB49wgjFp*|%NTl;4WvX zaR1~Pp1twBKm70|e(QICSKs;m4{*8M>kDr^q&BenWT+0Rj9fqysB0x3vwN)kU;nF+G7aina1Hr4C zrN#a5fFJ+LpWz3uKht0R*Z(Q}0{+1d zuly%}?F)Fa!1q4()b7_^(3Y7EjAj+F8Ep$yeF2Uq=z?) zpcdJ+3S4Ed*EY<=VJIBDjjA&7H`yE~a02BqZ6z>(Rf|w_cj^9)N28|OBQQ~_%Va?^ z7l-4-p<3yU+cDN6z{?tj4SDlk=yo4F5H2+tWUI&s;V@2`C#q}oN&kF8Tfnt*9i-HE zQ_U%2CY$()(&%M-)QqOXwvj2;<|t@2(AJ>YaaUWbGFlC(>QH7c%I7!YhY4ve z04pLRCIO_lqB&gztif5EmF7m*E_&Ru1+t|ze6p2N2xA*rWv=;>S6;0VUvl8;`4SLP z0Wv&*4Q^7hzCvj^79+>;%NDg$Mhp5}VuNLAL#QCOoS4vn zNR)#nLpN{Wii;Qtk*&hTOU-M-O1Z+;Vs~4kGPGg}(}^zBrgn0;kRF_rdjNdq1WT2K z6#(nv=VGOSBIs?x3nJ|ZEVZsag6?TIlW7%SzI?#LXX4$e1m&sNvam|LzP>h!+>KI| zgR#gDI^a)Mc04vfbyrB03dMI{8@o3ejkRnC-udta-+1y@ z!h=H8^l%dlaSmu}EYZU&XC!@1%EjT0c!f9tbmf&f_;pWR#sPr>Li%Npn~kJ~7|^v? zRPREocK5w>fdyy7#O*O=H$ZK=$1#a&Gh@{!$4I`QBq9gEyXJpWn3|YYaPHNJCY9qz zd)masT#0-Ab4uTjzh+@Hk0$@IcFe-k@Hlh#z*}B0pAEp1TzuQ@;cn_4w#Q?$lEB)$ z_{?vg`)LVabkU|z%66f%#-k=DvOHI#XU{%(jh1!!c74ev0Mt)E6F`+G{2innv0VDGyE9b|V zUa`zrEkZ&E>Z$K6h%kAM_(p{24|xm0zYA}E?yIZ2ymTuLV} zjN=4`Pd*^S^zwAg<@Cud0kvlv@_nM)1s*ASx;l^^-fW6RCjx}e<-qVrlbUV6fooY# z7}J#=L~M{bS9b%`_T=5waM`i2AwPzYFK6N20F zoPkNe=OE7aWepOQovMgu3OsL2+d#zpMlt2j!U0o(mhwjsGHXbbF7-=sL%P zw9lm*0Ob!E-|Y7R#qumQXLaYGS*AYaoPc11HjWS^bbuj0Zcx^4GbYrj!T^JRB70+G z$X&7y>d7k_BDH!54UNplfM*cU|4OnuVK%=$8>sh>L zBvgDdN~8rJ6#%1z+7DN*TK&e;XLxnJ>UVzk59*uW{x-CWZ@u{@p4?sdpq8$?ogCT; z?n!q9U`quh2ZX~ejVu_j#AZkSJx+G#AN(7Y{5XegGJ=_nq}~A9BuyiF13f@sYbEfP zvl-i*o1Um-)6vb=@JRcGA8G4>C)1^Rd zknN7acrdsLl&bU=w1Dnc{p88#@Do4&ll?#V=YNS8pnl}@KdV>!mA~H_f)HESp6tx- zaCMSjW8>+i@y!o5e&P#H^y}Yvif{hlv!nlVDF7D(Haivg(zP=OU1WnrCySEEjk+cX zhpB;O#3W1(q@`wQ<)TypHGx}XJ%qHP+6t7{8;Pm_XsZpGg;MM^gMdRx$Lat#FHZ`+ zcLXjAoeoP7%>_qO@juRHWSW7@Ss7C?lOqBXn$!%Y3r)waH+M~sI=0#siXnqDl;f0Y z1#Sw|g_Y`eRLqWz%5F{V3&28D>Sd#{O|6J{p-oY;0)zI%Hmu^S;e>$#bIf2 zv9BAD^QX-=*b+NbE}c8)6Ws7<8PNERl~VV`m)ih^M)#)Zdu8@ap(I|}$BK$+YHhS0 z?Bta+n3~{cU}{G?gvL<85OW*)wt`0jVo}=u83EfC5@556#Vg?hLbgSFrK_DraW%V# znrS@0XHvY0&lY|1)BwA(>gCTjl+$AK3eeykNvt#`i^(UHpu|E4j*klEzKx-iu7J<2 zO0yVY@iZIPLSzFM&;3x(>7g(YM_9n1A1c>_J^Om($l&C@|!IbstqPN20-r( zvZ@Nzt~V9Bn;WVCd5WpI9a^{n zE80>>t3e80wpq@9Jlfs=i!xWv@&ie<6ZElgVqo8nPCR=n*j8d(7qx^L!|gLsa2ssI z`&x-_{7xb5oJ=+he*3s`X`}L45WrG&BmI6;jFc?iUV#DZGv-Am$#!e-V8?^jp zbT-Uh@J&gDevEHeAXz)c!OsQX3ZB{23wBX7d3?*xd|# zu82J*2rUI52r_xBiVd@Y2c*iSv!~3Za*e`KdiA^=FzTy`U9H2vEb#DEkj2O)9 z^(;briX4&%1B5OXL1HHYOQ?o!LfuW57)?qo!Hjf|79cHS1HKfSRV^2P#eH=c zep#1Zmva7VbO8k=Lx?*PTs-U>jmBt~u>v z@GxOOdgOdLcWC1tXpU}|pBw>ODLF`mNNT5TB-Ev=L}|CQs{rg)x@J?&f%%->?daGr z08xu>CqPphEpU06o#}8g!02R9sqY$;G|To+{rsQTzwodAW&GU_epkQrwO>^4Pal;b z^1fH6B*s733u)t#6NVSB*zbLM#Xs{8y}|E)EWLhBR4q+v-E9s#$7XV!8+QoKc6|Ob zqlj)*#l=P3P$I8w7Du}&cf6eMx(S&L!p-_%rwK`=t}UsGT?L5ClfW#Efh=_z=^HB5 z-qPZw?=P!+r?hU{pwmv3@O-R;&4P6iI-U$gRJmp!K!8_>M$H9-4S5UzYsadm**7Tw z-7Mq&b=CM)!fmt$%@@n@p$bDuU9qQ!xMm?N|3(2WgtaYM2eP&eia|08ZE$_kJ5aS` zz6_mmn$D*c#DA@DuhIg@GVR^jbcKdeAX-QHn%j_t_41Us!^jl@g90)tz&0o}*(wC| zrdisCl}uAmO;@p#NC$u$#3my`ZIF&wf)?D!+9m)A?GC4vrUCaUw9Jxksk;PRL@rY= zr)~nYoZF_xA_b%HXhSuTQ4f>1FbwSQ2uU7Iqy?;&ixfB-!+}K7SUaR_9Plol0!QEQNU(ReV*JMAisQDQqiN#g7Vb@YYHh2_U3St5Nuc@;Zn{q zjDQUu8n~tp)j+g?1h@w7$+ajqHa4tRb`V*`7 z&H-`ywfCDTa?e4&_44_V3IMNAcA5$-GIY_R5-%g+}=J)}O+dX1?2?7qRYLOQQG%M#SXcYxqa;b4U&=f3EsfK&IXAKPc zl+|8uKnt>;0>ot&Omgfo4u0@X7L5bobkNC?-IPI{xgS#joCV=Th%%BG_Ni@B)3kvo%a?=kx#&i(S3)R0fAdbttXQa2^d?CgOKDYO@qOX4!pV)sWOVhciVjIGrmfh6;6c zxCU$K&v6-_7W%DbUll%~lV#^8oQ!29VWjB%P5}E7X3?bUR;G3^9y96pmnmNHB5)`M z=PVtONk3zeuA#+{DkAJaTWAI)ExfvZh+q84e*}N$cmBWpU;SJEZ2jpk{y2-^)dlCk z6#|unDHVnw9S4MZvZUX6|MmLypLve2fAtQ(|AELNRs;?WPZduF7KG0>pC$+jTow+RPw(3I9x+;#WPljya3MB_|-MdZ5=I%~)A&vQ|Qs7MO zVOv+K?rKV;N}Ent71o}EfO17OBrIXHu5&z~eeh<2#7YOW+1EZgr+8gV$q60R0CabB zZ%p3yYR&|42mGFlWgFTfJmC47sXY1bYAbe3mbEhk zZj`nW1)E|Qt&%@Q)pPfAi;`{c!pIDp@g7E?6sC7On|<1+~XrXV@1{JW3jau zf(5!P8s6;@hV;O=6hwS%E2=;Yk5BoiUWwuqaz8h$1vvZPF z+ya;TG1{eb?PEg`Ur`3Eh4#>c0n9CIvYYCnnJ0;uWM<|v#ZgSFUK`d#m3lW&%d|E_ zX5(b+HCJuPO4*L7Wwfah@H@*(#_LLy4=I5O%gu>X2TgRo5KfN&b_7JiPa$kUkk|o@ zwP!Ap+i3!TL10xA+79cv=N|bCTfEpKZ^cT>rO$uTPIvq*H@lF9PhUJ>)uNlmo!xX3 zRfWA#TW#@e3U?5QZHl90Fv;rkiK8?ui6T`O0gEahiUrkfHF$ridU)0P_~mQd-z{x5 zYMJcfT7n}z7d)EJ@7_W;(0%2CrRd@n`kY^%k%{s~Q2-?sdF3!^t3$UypFG!|eCprL z0f&4>ikpeBjPV&U!illM#!DzCs@s8BY2F|O$VY$K3D&Au7J_^bpw+Dw7u)R9a*&oO zEiP^*BI}`QM7Z8r<}&zp5s5_*+`wgWHv^H5(L9l?RWtwGD;FXb)==7~%H80^_eww^ z6AEpeZWYNXZjCLOdSo%l=!BuEyq&7P3mIU}!(d}IiDU$uy;UtQ|yDro!oao7QnhyoGQ9bsBdGJ?+QTS^P2Uld}nfL?-BeRJQu<5QR80v ze23tRHX@G2DNZg7HtF+YJ87ksC}3$LlkrZrhCWYDEK5J$1QZYrk{TyFDIzgF+zPU! z&>;p(Y#RWm2V zK`(SNLYc~^p+1H=k>{T{?PyvS&=9PGj)Np6w15tR7U${s$7?MN``844AW$h2qI>($ z<==r0{r9O#&Au!J#T)nRh!cpw)1YrBNzVF+PG<*98UT{*U_#tC^ltaL%-lL+Ac}%$ z3T>{CxQPhq+{OxyiF4_9J6Bw4qYlqRF}vYk<%Brot8AgQ>aH7Y zh{EQpA6#rZ_7)d0_il0lh$Uod1~D7vyFI~p*2`zL!79CgOYb{AN6(rPU~#)Uw4v_S$T?WXO`Ok>GK?kG+PVy*NT zZjtS=)ns9Z5-0_XkBv$!4EO2?+j92yORG|yEzgWs6KQ#J&}3$E132TtqO9hB%P3E5 z_FhQAjF@|KXpx(VCvf1gga{Canh3OtC34kJ(CKLqPG0Pv6B};^*K`8MV1KmsOK}*8 zs(^HAi(2($`8F7B!sCsQ7C)R;lic6%@wUA8DsZ{Cu;s=p$A|IREojRvEufNKh6&cN zB7D-L3cIg$x*qcjicMfkBS3h>o5HJ(O35_V$d*fjl&NK@f@bt$8XXd-ld@7@x}oW| z0(YRg<;?iW;F#r`5tbOP4PpDElCKCIUdJS1!nOc`dFhLwr|F@=Vo)V_Pz95Xz3KJ= z7{yffHf;rs?M8glv)Wh%sFr!o#3-itgc}v{Tbqej;O6d>fb=oii#7MIjVKdZ8tKF*mN8gn;Qkz)-_f! z^(4ydpTnoFwBRH{a?(e2Ydg@YOlwXnM$k zfC@J{KWEawSdkJeqtU1u9$i4c6Nkhz<~p6krBceH^GD@B`2#S~-+)cykVfmk6(*ke zdRP#4KS8)5s5b_22t@6DbT!oM76~&mZ}fihig1zHIg2j#yNqkfQX=tD-uISAF`}pJ z*oS~0lh_gS{5s!to-^km3NT}zFd+G@_8XLAgyY1RGe@+hk*FpX@?irX*AY5nAOffS@o+tS$p?C*n&H#$!;U00Jl36|kA_Fb%QhGft4; z8#v*m*VgTvMiJ+vrQ_Wod(c0pk}W}Lk8t78hhdN*OP|dK;v~)ng%Ci+DV43GfoR)% z8d2)$I;|Yuw((Q;o#p+QcfxoFoHcjdPti~aaywwhL>@hfy$}CQOOQU3N`fBsZ2_j@ zpa2f!fEga75{^d1aai8RW+nj)fu@I{5XOw$SEmV3ajVYD6(Xk#gJ9-! zTmx-%RnDlUS_x>4g0(F-a|mtKf>8yPjYH|96vH!GMq{CD49dUji&=!0NSU{csw%n0 zeT*%jj8dZc_#0YWdiLg1?A`i`EgMYmJ7p3t?Nr*iw4|!DH6XN*Iqk|kbGcQkqG4oD zLu`$-QN9KREuX$u{Swcgy{&Kj+|T2`|3Ci=KsA2qtA7G7U%oKy2%>4{W-4Etw547M zsKf0ENcrs#U*jMDwKoBPAAPC;2Te5YxERLg?DRJa+Vji_Ox!e)GtJm;)UoC6bhGoW z61CveEUo=vy+=i%Jpe|7pnEagKA#XNm(R9qu$$KYV3tpaSk_=VGSpVbrxckuq!5k+ z#6m45#ce}Yb7tu|FXwu&@hM zU#kLF&Ry1_n}}SR-(jghm32Q%4;e29iEVA6nTW&55u%rn;)eU5!ATO_!<38Lz-hqT zXs|}elL>k&K}IKT(DEYZ#Vu0I3i#l-(Q^h-cbx~c6+zX`BDVUC44VsBGp$I-#`J8s6+1U8pLB%U& z8ogT6_UF4Tq1oc9Rj*%N_4?rfRV!TrA~S&H?!MVq>D6bAySu{f9u_hI%vfoT9%08u z12oF9+T4FQJGcH(ag%uS*&W~c;1e|1_v;RuAiJ>WezTGdsQ|p(=+C|NW%nIjZ`ojJ zqfwpq8oIaExQsL~+gVLc-NV|BYtBhTi0ngbAf4^q5*2Trd0vLmG3P}yi3#l9qM&UW zgb8_#f~E&sw9uL-;Vy?etqJu(u$H5T^h!83o`iQAMr;%cuqxpA@{>Fw8k76PlRi0P zYg_{7NP*Qi?^at4yDO^%WI(I?+GCR z&p<+wK09UhttyOiWxfVIaEyvQ0y{~Bc}g&}8njLzc?=|LgahZn;847B0hWWKRSLPz zwUG#IU}$f%fSDCIW;@@J7w?lnxaTf`kLen^QF?Q?{d;~N2f;gJ9~h}hGOHYrWP?bt z0Jj!^n6<0feFbDFSg@Z-EuHqV)szq-8M_=+4Co=yW$Vtd7r=k z-S6||>nlI^=G%C7e+NPGyn7BRNDm8GGP_~K`D`Z88V$WcvAmri;j;4Mi{=(ekH_Qi z^}UotZ&{JbLleB5X;lxkv^q5mr{5u&Idw0s7iE z!O0DBmV)lk-Q|6|xk+K!OipGb!iAzyTVX@Jb@Hlqz~$(?w~`r6#cv#~GIdk{3murX zAl0I|54vjm8G8U>f5t*HM9!kP%I?cSN1gglK-Dot&O<1}6Si4fI6ln@zmVE~D$oYS zPl#be31$}$E73QgkEM&(;}DH+a<=w{JGRNd`&Qd3hy2pJ#VugF))qg$Nn=Rs!r!pG&K%J5D;Wx&zB&*8R`zzU&m6 zY=C4LQ0E=Z+caVZ01Sd)*APWj_QG%h{WC&&Zl?;w+K~T-@GLF+@p6|P0{bUc?S^oL zg&sSxkVis#N(^>C2HOQt(M!<-0K^Szu5r#W4tk$xZ~Q1uv0#$c zN-`Ja_~c7tr>2j7{%b;^ljKg@G5}r@WK?08V3io%Up;e@^7oYxIX=rRj>5@g$m!5U zMr-00iz!a!_KuFP(>7f=iTxCP$!U89h?Ro7u%=2@0cUu!@AIyc>}UV|A9{S}P(fkNTTk0k+Z;365|WfDniKBe^E=Q$1yFNh8p@iRR> zaKaU-F2;E|%*$caRFMhnsJQC}9L#hn=W-j63F_zblSo_j*9+4KhPaT5bz`nnkH^xq z1emts)GL*oIo#^K%KJ(O5yH0AN327E}T%2j>$?rPS4R!aj7Ebh8dh0lew& z)A*d7z0GBDB<*v_0Vw7&&zbFno34Gh-6ZK39>23AWUCkDl5QO8o`~C8VOkapxYnFW z0Zh==gHuL1K4Zr6zMZ|XVO%kY4ypZMRg9k~bt?mn1(tieYxzX79r;vh1{}pF7dPla zeyh@|LRBr4jk4`VEsnD>Q9j~#h=HNe`#`urkl{oG&O`>U71~Al%r}p_msmxu1*Qn8iSmK6LMwus z3+k|l?P$j$n=K?_)db!RkZWqL76IyJ{m^bqVoizwGf)GZ)12!lnH zaMud@5pUVZ$o2&f+wzODRgPH``C@^ezWM-v`Wycl{lWX+!vFYR`%C!cuY6H=ch7ab zz6RX~jG`}zb}}bqiHoc%Xh9bcpI(8FKHB;x|J?v{@NbCH3!7yPG(yAO61~3;2v?+}%$6XK8W@{h z)#*)2@3gy7lt3{LX*8i=VNEs+esQ;xH~LZN24)SRS%7sXlASM)#?89ski_WF!d#`D z=rfbWQ#9xfZ!O5Na|^*({p5XdfG96!6IjJwl(_^9wZ`Yo?OMy>p#Q|MWVd-BRpoAA z(H;lAWkA(B)=-CEPu~O6(vzBTCrh(k^!XD|WyoFeYmn`Sc*`Z_Y#E&-AJ#ogUl2`m zkByE{m&ZoViDJUUDEO6H9l3&Hr7fWy#{q~zbs@@NisM2@DJwiT40URpvorE&ZYLv?6-EeZs z*drw_Bj#wVX>^Aw{y!{Jn;Hhdi>7Aw%*9A*r%JjT+PG5EniGzKD06nNQCVsA@!woI zp4luB&{fRXXyXB@GWzC<#l&R3aH+zvh0si(6)D4uUuejblQm% zI3(GMMu-B`cVhm6(K#G-rxQ04bnC1W5=a4vI#utftZx{*yJZ$PMlh5AIN@{tHGhYJ z-6z%d*Nc;bxwpR??(rvYX;~6&vkeUJ*%sD$E|bW2^1Z_B+U<9f*G;LUlHbY6^o2LN ze3V*}cz5&&R@TbTnpz%GWIB0Y@4Vl9U;dg1zl{S7AcitIA;TVktWVc|)}%iE?u+-G zISZ*8<)3drjZ<_zTP&*{oqEBd?hWVsSCL_^ivNlIc|Z3`XY`j7KH0az+t_(pnC z_9mN@%5$r*LUZpGH_cQK=pZLMo~Ps7QTOa)zUxNAQF1GKL68W*4m0SVCqTGrUKu?S zi6o@MISGCYMGkqE!-qOWr8V5&yjn;vq2bSx^{KA91;KlLE*a=Ck(8Hr){gNj39wSx ziA9ZRIMfDB=8e%v$ZnlIAc$!%FpDL|M;W058;27ZZQP6va8SqtMli-HdoveNF+_=~ z{I>x<%=WCD`vqFX(&s)kuWJK$r3ZoN?(W@n?Ka}g#f(`a9oy5AVMFKCu?wc=iOBD)mmMe@E@K zK`dChgVUrlHyC093XeK2c!1cFn@qg}l#FX6@yg}$`po)c(rZE}Cc3wO9KGrIsa`(O zz7{5*UP;&IV+$K$M{`HBGnckwW)#hWF3x;tmt6{7lEXGwiy@ZR8C@zN4kl3qd#Ak- zG(e<1QHENLt<8cTMUwg59kuASeK}OcT6q2NNM#DL{}A1OCSns!NBbaCV;vCzxn;o@EgDMoL~R)9lrhkb=L)WXkybjiX>n&_QaWB z(EN*Dg15yA{1Vb@1_P4xl1w$z|}9RSx3s(w6+{DtIn)L2Cmf9nASick(_H zKE$=0&e@Q@XDCddHY72MZ}^#{F$lRoW&K&L$_1?<(sDdTM~HD;4o6 zr`J|gOM51!{lWBW6xNU|(k8f0ZVME#!IFvMNwmk0YxIk-k`U5x43b88Ms@8J3+&I` ziozV6H2ueMh~xzYY4f^Zf*X{zXlJRDl+et6B=qzhnv#!{fvZD9ahqnYR`M2m9s#O7m zRjD14CgLUV(ju^|iBSUE0xv{W^b$l4O%#BZ=@}xh%4el&LGl5j>_&EDU`#u+CQO}58t9)HAXr1NF7H{-|C&ydv=gqM9kn zB$J4M&4_0R*e<6Ss{s^mYE@77K&Z`lHOr1z+D!LrIR1=JF9SXVJy|OCk#uu%b|e6@ z1%+!uFe49U4-^Gl3Pnnb6%Rq|Ev1pg9Z13q1(8#x=5|vexZK200fRV>i}A5yGf)rwEfwjgzu59^83sl08Bu$zfb-+ z&BF?y>O@zWKIV5w0~)3cd8nF5e#ETNOgyuF_cN^&1KR3_$A;Ti8G@fSZdcxiW$r=O zxAv!%9N1Fa)>6R18@p9qy#OYQ=!ce|4GYYuKh|sXDOojrmkb0E+y=U`~E%26 zBdL~8lF*k&)DtJJp#mpy_lX%CAg#{6kTpqDf`!4Pj*En)AeT_-V9h7+eH3)3p`7kmb<%MC7XI=fXrQ5lo zCJ{#x@a&@(1dU1$zm3_cH^CRLJ4if%0jPA=j2Lu-kekiy7JWZb#TmrVhRy5WQ`g?< zAJo)p1fkszN0lcj%Z(86Ev^#gts-@X5(~>(gboF1a9te3Y7p>VnSw}X&8{mtT9SB` z?(T|DpFh=B>pS260l)jbAMoX?&+zQ|6Fs}XH&oYG;M(f+zqbK~rYmP-vIF>uJC$;# z+s1~f$DpNkoPemA*0AY-AAqN2&+$nr58ulkc*a@|5?g&p#@8}v?_c$`>8tO#)v}z= z`U@Mw@a6%Kn4^%IfHhu6Yr_4Moy?JqGylpTVg@-wuqh4eJ#m3gwH1xZuWq)l+e(@C z)^b3*UH9E=bbqENmwWx(AN`g7AN?PHqh9P+_=O+))qeHqXMG`YLBl^8gp=bYE#pL} zr42Q7De(IrG(Pw2(*OBC`CQ+6_Z3j9R@Yqe84)T{TntX`b0#elDq?aGmA?uSg zLZ@qi7ZA0bfO8i=Ec{960Z=)70s7@15$pLJGmFk;==mR-teG^D0t8N;iAgt5S zt#vE%q*7OKG?p>r37nMk5T78c;Ui#=eOjk#+r6Z1%Y0OwjW)|k{CF6lCwSZ>2KfdZ z2ij*T74?%6OvK1t=|UOM)bZCxG|2!>QYl+7Q-T~RT9p=q6&E8l zV6u_>my2$3w;NzjlatkLgOyXho{jF1U_jh@KIthIX|mu(yEq;d2Y;6r`>>Ak0&x-T(Q z0bekJdYwS+>wn}|`0M z&C-$@*Rq)su?+Ckh~KP}!Qk$3?r$Gg0ablBCrX=yu8J6ebJs5%PkRw^-<4{@#Na)P zQ75*}VFOXAxKBV~I~EErB43BYgp1>ew7Nlt@qbCQt$g^X+Q$L!I>1Ac(@fbqbME2y zca1c@SQ9bx{0z(}$|*6!>yiQ?aF^-5A2v#zN~h1TPg9(_?(Q@`2g* zsK+PB^`>=5u~CwPX59MvbLfbqMy~!lhV;J5^1GO|qZk{gnujtkptXjvj{_nE{cHg=fBfuG zPfroY*yT(@(Z_JKcaTiYca;^NL0N{+;hV&;`iYq(CIB^MO2<@@3g~uO4@d`l9Kc3# z(?2*)oKjyQ6z`ut!QEPV=e-Z{tv~n9dPdetGE)D?QtKHBat_ZB`BuHH~DF_HvQMJs%8(5l1 zoP=bqsYl7xZKrEQvr5+UA}0E?@pA2FK1s9t^sHmLaOYyGgIQOf2#>>*i-J0Ofpdj@ z1ec6j?{u0k&b4z7KR{?DvMp9quOD9PSAY7~>tFtV|8wP@yR|OVCnAr81kih(uLIc2hk-e<-Bngf%|=Vgsv3 zRN17sWoy$=^gD|rRm_ARJ6UOG3zqW)2Oxq_Y!r&#GdA2G;tRsOfhHdC(khH@qBjNh zbkBK*sPrRjsG!?audBx_27PD%?*E{sqXajg%3o*VRIodr*EJe?zMt*`#?pvMrOCrK zoVL{IE3uuA88vge(`o3woo(q%Q@qn26fH9VjaAMXTt%-530buz3CkJTRI*NhWtBbU znG6Z5t>M~OSx}@SG4@TQnOO!Vce#Av%`YDVH9dSb8P6twi|gnFAi*>HD$fxci-nf4 z1sCF(Odi-7cd~s2g|ZNRuS{|=c4PFEq3|xFI~EQ$@m{K$nv3TDY-fF5_K=GfSlb~8 zfu+ENb4Ge}HZ@f>5}+}@YOdIp+f)Jjq3Pt|^fV+vcBR>IbphJa-TfUt{p2OC*Q@UC zGH<-Q3|k7%?pN2H@Xc?1pwGRreAfFa3wcJ_fF3mnp;UA~uTaxwY5zg0*=y88cJqzr zPw~$CFYw93E57mczI5&r$?A}0L{{^R%-?ED-Av|F;UFl^=|TOT4q85J9+C+Gu^+n`%K=g>d#JVGgwC5U)~e zv%I1Iv>~Y`aqzfU7~KJ8+~5VNMHaSW0I`}QIFf|jfD%yMmM#KlkxfRFRtCw@bFzR_ zEv?B{kRlKQ4!Fd7`u6Kq#hz-Z&3^iyh< z*))kqXQCH4tU5f>_L$u+u)^e=XN(PWZ@>R{Ieo;*p(oGuPx{}NiT~rjkvPBFh14LT z2AT`rZD2;HGlO?by&5h@a*HA`S`|ICy~aFli%!%h&lpQE&*mT|c~zpN=S3o9w4|8>S><^d(bbc?_+M@TDL@Naqa9#Zrm6se=aEi}i6fy${)j1hwncS) zSdo6C_{U&yN#^YVE5UO?X`1o34SmA&lnKRk(63vD%+Rxaf|?EsKw#7jZvHG_!Q5ib zO%CHAi!;c0a^{nmyDZgK?ag9O#n(^LC}cQLp1JFWrn&Grhr%fGWZ#eWBk!DFm^%s!QCWLW-_A0KLF0%Rk91H-p3Zr727#btZGlobas-iO; zc#uQwuDlDf**O~=WOJ2OapM3^_mZ<-4D-QVy@C`J4+6JQu(+-OH@S+roi0`m(Lc-~TQ@`S=Cj zc>cVfJ$nWKg>HF_r&zlBsNaAqg1~l9AuHqAA*j(nca@EYOE}w?-x>bV>5g4;Pj)Y! zHo=UEMummsE@fAhh*HnAbzdKsom^v!g)qxIF53DAa*G6e94>&`;57hanvLjtkkap( zZQr5;G{@exw8&H$qc9ZShhUi1m8c*jbn&Ao25=RG<;n#1i1JEmrA`NnF?{?|<(9@!#lgy!n;2>JGcF&?*+Ur=K()7xh0WrVRrvM~U*X3; ze}{kgr=Q?=-rarYmSH;R^v)CmZKLfOBGOML)dJp&6cY%Qxe(D>@=PH$W##}@fW;Xp^^PjFXs4ZkeO<}IWRvG=-5 z%B+w|e{Q04Qc2be-2-CzkT~^pSAbRpHfAckE{hsEHKf?C_}g&W-x+CSQ-$v4p%xp9 z-vT|WvBc>AyCygcm|&c4<}QNe&iu3RJ>XJexLfj}oAYzPn8~t^jtWvofgZx$RPRjV zQ*p|fg^g`P-IV~KfT%Fw)X>2??6Ua?vT_(ktbfBkCiXVUYD*o6GUdujxU!gbNxaZ4{8sC2R72keq=?EfV ztGE4VkQxEI*`|puHMUxAAb}l9b%8{&aMk7)-+YE|{oq3q(%ofY_lQgx)U|~ZF1*9b zS0CfeTJ`y-U(~0sJ_Hs~0{cEBcaFOwB~#qHKrJ5;zbtUV<*<`eh%yz~g{=V;r?`kD@Sc3* zzvrVoY>HRB7zh}EVHHCQpPq_G+5sZ4(sRKw$j8{=ax9wz6I*EZg6+?|=!BOo%wOns^W<-nP9;Ec-k7k^&*<-KO`bMu zKdlJ>v9L}7SMZq0b# zjLth2bx|R`CVdman98}Ko&z3iw>SAfDlWTct;Ux6olP_DfEt7t0X>D;B-*OG6qifU zx9=MWYnM?^AJG^wZ;sOMOHa^7N-Q95`<2)l)3+j;{_m|9hRWLt`p=+gfZcfKVnrZD z!T~Lq5f|!fm;%w4@edhITtOH+lM-}0hi2FtMM+xk^C{enf#Tg`MCTq%%IIzPgJt!H z+6jvdZxGjjO`bbV5~C9D;8YNI1!mVs_v`@{mvKP%?)!TR&ZmR%0uE?H_)L(7ptsvN zxodnIt4hzGKgH|MUg106{%*bZ!}sg{a>3K*Pp}B?wiAiYhx#N;fF_X`XT4nml1oXk zyAxtXKg{AeJX_6dA1nk-HMj-twyH0M(D;y6eELMHj5s8AbGVrF-pSjNS;D4fB}G#W z0D4CD#?YI?eL%X=QcRF)WiM_=uZsfrm7acS!w|_jmliGX2$IWXP$$NxrU|%6*&Yo5 zt1bcM+_9{tRK|dO$9wUDm#;s?m*4!U{^A$Eg#XU}Qo%Gjc7aXafywDFxB(P`|8$9iGqsS?iHPhwW(5ERokhcQ?QKb z8t@WGYnQ)h%SVWtSW{)0Sb!*L4?J&>)WN;&`rcL&a?}A{?8{mfd)vw5l6=nxCZH`K zasWs}wL$%EHp@!!1|h0_*cxJWV}$o&UL3urOM%1;ZMVC-0@|4(5MQ{6!iCk^vEdct zWXV}+S@UC0J?~7~nq$ZD$x6`MtXezaTgQTW0z@dC5un(quaVaB)h+%B5f;;GMJNuz z2eBrY)>Myt;2eT9Y7>?jZ5B!Gy>PrPJpd1yDwa zTH>xwt%33L1S00)goAKi)|@0Jfuw9jc?G*O$T@KsBGBM+Y|IJ?ww3(bdOg&3W7yozd3FjrpUL}BCjAm`S26n zKe+>s6Y=+6cjHT6c!IzGtq=RfXIH%a{GPqH)S|Y4zRHPXt96CC)BQ;H!aOZ0YH664 z=Ai$9FVnK#Tz!Qas8v$Mi;Kw5u6h?Ods;4A{75X89jg{o@9JJd= zL9CO{NBqvo?{LdPY_Aw!V+`7>R{K^Ej*WDl3QYW=$$v1ho=)D=qqV@~VuS7t@HF|~ z6bk)IvD5xIO^5l#qg zKVZKvpu=ssF_kS8VN?pC=iDlIv)Kmqlms$+n2g^uWS!gwM>jCi2o$rN*zKX31VE54 z1R|VgX(3l&Y_G<~mlY!`^KVT`fna)Ah2`MS?P1*e-GjbK#L~F>1R;oFY466Gc7-03 zR$<;66{~UrY{|a_Gx^^EPMoZ`3fzOX%^s5{%F#(^g)K=d-2r|^^kE`OhhFFe+(|JJ z5I@EuTK26hGangU#L)ZJc>)9hU4kn(f>U@@12|-IF>3qiI!qD-gfqBMx9XrdkgkjY z;=cH__qQ;5!2`=(Se?E!%2jm34Havi))+VY*-z5iSVhvDF}Q9>FCoi6 zgFZgUtLZAMMVk7`b|(R*Tem$znd7`0FsVJxmd+C#_c49QEU?@W%cbq6?DsvfYxsU8QPn1g^0O8Ng8EI;Ckgues{6Q^rUeV$mZz7qpzC zmIT(kyHi$U3AO>s<%vXK85*+zE0%ZL7?SsRffuT-SK;a)?I^?(pn4Hy9!Ia`a3U3t zqJUL%N))zST#?Xgn9QjxtR){8pVqe1@Cgg;FX4qZUe=`{5y_5)>FkX>$M{bl#+74} z%7Qq78(DlnG9@%E%!powO}w*ALl+J+K{fnonlN5~)Xg?&pal_J^xZdu(aqwT;90S~ zi(ZEfgVNnnml?d}DoB8j9wcN4gE#5Wa zA4N|0m=H?7xS~V^dV(Pi#26G69xa-tZUBlnK9vEa_#GyCSY_ zEvf*XEeFk#c>m;%pS^t0Cm+44yZe>YH+a^T>O#K#=7s;>-+d3y?hBVH?cQ=6+Suso z>xQ(wPm;j^PEpA>cB9ecPTWLo;Mx71u!+C@yWhoE-+HF&VKMsJFXmxWW&UV4e&$F2 z4EEl9)h`%QR-*Gm5lU@gCNI(D@`b{H^flwBi{{ij=H1YvLSEU=I#G})H5FoA0dzBA z!#a-bilxus%EcT;a!`L;%!V>v!;~1OKJ*rO39rpz97|2@0$0WnBJB(`L5MXd{mh|E zOd)V-b<>Q*Q*=URG61I!+%2RNS1;!(NGphhp#x9arD3jOow3)691>%ulu|4EQHhqF zllZ1jMcCEzTk%Rb#im-RtTV}~ZOTO7%Ds#)g&7EmBEO~9;MKYRIpGJobmqWz$|)ev8u956Z3)M)+Sq5KDDAJztx zKSuyF!2+j~E{GXhT;2X1-NDhZGn0zkP*&r@ZD!Df8iM|{CsN&iOI;YJ~V)15Bx&*0{ zc{7GEYiYFZ=;>erCjV~<;C>9;4fKR5X+m&mNKqXQZO4D-_j}yKamfRA)~s^~I*UEY ztlH|5Ho1ra@k0+DRSj5q$YK{$ftkM_3NJ?6fI63u!@8Jp9Dv&}@}O)-@5Src>j~Uy zYHy2E;Q(`?h6dxgx^+X~LaR}JABW`PLq3LsY6BFx$bjrz?5IMQ%I^uOmc`&{E=Pi+ zn)B^dE4XK8F5^K#gWNt|U5+5-xW^!SL-Y0D319@~NZ%RUkXms?hrs71Kjy0&&d<4@ zaHlV@J*dDzkXzq_^g43#uUR0Vt#d-MHMCRPNfp@IT2;7va*rqX7kvE5NBH(1d{-a8 zc!6hcJj2twJ4oBBy*5&INlCx#89V%m5`<-LpuF%tvcy4vieQF>*E9`s8x+x2qQ-W6 zj;kLhP=csTQI9r)^$<~x3L*E^kkOJ#!--(H-}}MCu0V1rFxp=Pph!pv-LgiROB}%l zDshDJiv-}Jslkh>$ZG0S&}jo9fEC0681E-Qi27Tbmpv78SrJeE3IXag2bzUZudb~> z{l|X;|A)W%SMYED_TR)WeC20#-B;h%%z4sEYb6(=ITD<>r6sp0OL$ViJ1^9#&Hvh; zeH*|3&Zo8&q~%Z?^vAn#GItJ;YJn)W*$EXG(Pz^4YPs8FL8vB|gDGvJ**^AeuOD)< znY`^o!L20%)v+9}{C_$h#1ZU9j{7O~U% zWd?oSTB6cxVc0r7-eadaI7cXg;bJ7%fl2KSKz-FK5n=^}Rs&HycHMb-f}t)5sG+q; zBNxn!f^8;gmZ<^=qiH2CC-X)iM6ifuDMmz{k=sW!B|~I7 z+6`e7aC>Fezta|@2^PfRL}SXH;ae-Xn-5hR@HJjvhE@U8RBUV}b8{xjGm=%(tsv#& zO@p0E>=Vqy*hk2@ZOVk5r~u@>02$h?@xATOPvB(zO)W>s5e924&j@kSu9$&*dbB>s zGG1BUd!obrk{o?7LZmt-ol8WyMT8_3;g!}t34k1cp+H;(@PhHjf+lGNJNc&(le}ul zu2_bOr=G$|kR9t#BJQ7D^vV05LJ}_P;@Wu*)N7G%zjeWu^#A#-_v@=)x<~Ie%CQA% z+s+s~Drtk(w+ewVcpa0UC|$x!=)USJUwFpvyz?=>_vt74;#<$veKqRja<6?k46MF> zc%?1qSHAI|!}~8k3TtA)N6gqRf`P$%rNEWvK5G)x{Kq(^Kc8vNGoBnuMo-o>Wzzir z1nDs#fZ!wm}nX0)cceRKr^aT#=6Ti@_!zIiSO4`x7 z#`QHD%}%Y17eom34Ru+JbB#2ACsGulk>?2OrbDPy=M7Of!1GKNdG=2$@U1pE}tK9MDGOANjX?`ZfO7 zNx1!N(}o?3hio3KKHFJdCf4+241#UpNSNJT_Ko0$snAcscD_>ssyQs2yf?>l0FD6) z8XHmn@quMRbX!7z8{VpNTXwvBjli<~2w)1Zo$;W2n+!@pmu5q^F9Ozf^`oEr+p}Ro zgCH#2bTZAiQN(e4xQ9+I>3o+Q4Cl}09{XqFaZY&2Y;d};kdagVLMfCN2XL*64^#TT zTaBW7Ulg=z%K~pJ@tG!J5YT&$nlFZSs`5Uun8xZn(I5I@X&uy<^LMxx_oHu}RuNaIuyANM zW|%YvIP$v+*nRv>TypNfA=O}zV7FHI&wHN07bj=g*R*>sHIjYK`(d6sXBIHGbFOaI zAGwS0K4hlo?wv1VsVqQbAnW$@1tD!R|*_4Jl2P9E&&k-FDc6QilpBe(h)f zllnW~`dj*+{p){8Kl!;Y;_mXaHa2>k{<70@7G{f7_JxMYwDPpn?!q@ecvb(zFMk2J zEWP&;aCbr8dCo9vWW5j{716l>z-=|bcWgo2O;J&SGTbYTr8&^O@G-{UFRZ!dh z6~coRDY&{ph1Vf^)K>Mh?Ny;w)Tm8!1zkDuv9!9iJa$H17l-n_5NlwRJppTQ8=+MUE5^F?$t?ahRtbQ zv9HB)$fgWW$r?q|+>P7veN^;&3+2ljYvSvlfHTx>K`gemM!t=`3v!{nG@Q_^U;-ZJ zFoKktmo19DVf$um(_~M#>97P%2uf7e0x3!nkVzJh$Ry;fnjA%r>XL#?{GHsN8H`~y zZ7)Q*0?q@z+2nf8U=bg_`w`cZa^jP70ZRMfiXZ)vH~RngTR-Fve)xbdfBq>R9ya#F zR$s5!I|?w@W^Ypu<2BG$I|;N12&yXfswi=?@RcvUiGT0!eFp;g#@!R9{am#Kb2_CwYNfBBF7I==VbcWqhOSV}oEVWUn&?#kqhjW#aR>PB?k?KG}HKv~e8LauEF z$o4`zi1Ht{piMPJ9jb+$mOY|B+y6APRs_0bqRJ5pBz6T^One!Qm{=}t+ za=i1-`E&P{q5n(wq)7=SC3V-D+l2`q8q=RJMbk}yn*pJm$E+zdO&o|61_^)$&__(L zO@49yAt!`UGfAj9@wVT<744en2h-llZG5* zW0n4P{dqodQnLKC?M_WX+kV={^!mqrRPHlY;^T=YZt?e|djt^;n#A-zYBj8#2{oF* zF3re|fJgB7*}qwzj^FpbWGoNn(^dn6u1{{j*#AP0P-pfwYb58ZJlx{!L$62m054YJ zc~F%W1Zy}%t_E{4ZFbzgH7hI`&%(5aqr*UPJB9@m%t!&!ZfVOOTEisZl_b~pB2@mw zZY4~oanwXHMK9$*3c#(WiwB9?$Nc69<=_l6KtyQY7f6?ElCz5e3o+U52~HB)bC&n< zLiz_`cI`A-mrf3-5@Z1%HDfhvZ32&LG!%69cn~<%xF}qWdR`5I?xV0T~o+Q=JF>E z0ZqbnD+3JR%tcIl+^Mp=!jQ+#T}qxm-{Hw`IX9I<@ru|C|e&lV?+umNI&kS!yDHI*-#-j?afd>1# z+?9@_P`0fB+yU5F7uU%oR<~%1Eg+l`@07YrxQO;Bn|ZJsbr7NQzyV+vHv?OEacydccOrOdSx0PPJG=?GYW7)_3j4Yyasbuc zYLHh~grVLj5qJISv(NCOU;DX@wfNurU;oGJD^KqEwak>BRK;4nu64M_Fv{ zAna6W`U4ZI1t_Xv+c}Zo_>!D$)S&T_q_wI){^*qhuj`Z|oe9*~Ro9yUSWL)9w)*Dz zSOxcwGc2fujD~Bv7*P%x*zR+#h!b3DS%VW77Ka_+TjlW{LrYbrY$t1HCYCSQhsSD*z*eUa0IPH++X zYIj+tDX8EetrD`tk+Q@qH!A`}Imr{?5LIQIw+PZ$3yJEI+zAh2=29EXXiGJq-3X(p zGICkLr0FBpA!(cVadCZot;~A|GH%kYwa6+3NG}u%Tkt#zL^;J`1V-S(Zzrv0RY48xAeo+qkLOsw$6q2BJuieOTY6KQ*1n>|yKG8Q=m zeyg#qDhu?%W|{JlPv9>A%(Sxr-BNL-8-rjGef6b@SeJ2Db@UCKLA86wkkU!~*#x-` z)Zsp-5upa*b=xTG1vq17tJxuTCI9OwqBcApl7rp@c<6R9c;zLQ0--uYxAss>!r_QY zG6;aqKL&qs4pd=QrN`4ZTgf`@M0!H0R-vga2HOR-8A(~p`N6ftySr7Ne)K6n`S`Pb z{_Gjlxz7ZySMhQOy#4uS^?&(S|AD^z`8(ZTfX3EJCrQ!5sals{-WQ@SSUk_grP5;Q zQVXRj7F2!hx1NztS^BsB?lAw&Y#iqJ?Fw61a$rQe;A`KApL0Gf=A)r<7erF)=B&i z(wd)h$Pgf*^Nd+{%@YQNt2vKR&v(U3w?y3$qQL-7qFxEM21=R7O_4M|?tGc7(Z+e@yHm_U_K6!c%N%;P|@8~-}ct_ZcXHTEv?&)21 zU6FbZN}EM_YZ{ZtBbf`E;BGV{C@}Y}=Sl{^?@EJ$V=9~^kV10_mD*G04HcZ_eyZ*bae*hM$XdaLoV4GONR$87#v}^A!MOS0PVP0E` z2TH&&NfIeqDO{thTWrhxJET|9sLRGy5$qPKorXL9aW#jREw3gZ7wW|hA_4MJ#Ov!P zeCz3#`Ex)1C)a=Xul*|5-PX^1^%wcs_0{Cb2}6q_)z=Ut(3uoA(StB5*snM6gO4BZ zPye~k;l*bgAHVd#Iu><;7x_-Jjd%FT1q4#bjMm8D|#U2ZHnfFts5Aj5Lm|UWEr8@HZ z3aFNGhH7l{15|YYY(b}G$IE!g+T`Til}I^#2BPOSI^0sKmGthCWw9Akyo!_TvCxi> z6%`GP0A3MVnOc~e3QUX4&5D9)au=-=g_g-D5KxWG6$LPTj4>D^G!?2>MS7nExvAPN zIl5TSWa+kr?f#O^v`Fl$4PrT6^feFNyK$AMu4tlQ*)zoE3tQZvs0&ZeI|zsR=P~7$ z2mk>_AVxqi(!sJvztW>e+v>7rnJjY+qTxMtzK8_Kb%1FE4pgMXt|*M|14D$eFN*D0 z4I;vR;uK~qED%vavM!2h#G{t_i~C*?opDghV7?J39cBJHZ%4wN>L^?_i(0>BAMqdRRL^hKr2)^j3I-MJo{*icnfCDPFNJ_roQ zGt53osCJn?9sc&5ZogCa?qUvG+Rq(HlHxTe7vp=a(2DdgWYb(eNdCi%CUfx=xJFl$ znVogFvsE9)45*%XhH&d}+TD@3t%BP7iu;UR)*U{0@1+P;70_2-@+#tbz2fVC4&Q9(RuNlglT0PBx@;Vt|}zxy7( z{qaZm^5@>_hu(DW9bt^NoJ)3-)%x)D#$Wj5|B@cidhy~tE@exsnOU}aN1327Z=pv>e_L=X$Y4a&2WPDz7mt9 z$&eFkQ`eH`*rRO92$~XNy%0mbnuHBLkP7qbLaX|d#zkFT!5?1M)FSr?M&ZpF7j!il`yPOVxh7| z1x9SNAHBPCDEqEy35FO2*2K?`K$mf=eew@K*8D(0KrJtRPa8o`ZQZW1oIT2kmfsVz z56+9)LKV|JaAOE%GhD7(sB^iq-lH6w!kPIo)9ziE7-X`I#c@KwFe9K`2Nc%WqI?4| zMP3)P33yaE1nfb5sN^%ZfG-vs>^dLiWcd%%7Z_B^LS()yrb6fUG-e+eVF29ZH?QY8 z2^^L5f}yY8<1vf{AYxOC-ADty{7;& zE%Sy z{Twv|bma~k6^($&z9|a3i4w!;l(Sx+E#n1N{|>9vqW=koQ#~78HSt%H2yrW>*Y1fB zw4w!U>$MhcFn`pb*bUXLwnBGe7o~umvZAfKv>aMFH%b-J5nzMWuv}O$hFrHfpkQ~A zR8@JLcCLH5Rd0C^bGr~@GI(kzDp{d8M|&5Vbz$M6hy9uEc)_p!(w~3*SO53_Rs6kA zzo}pR>NoJ}`Vw=ov`It`{JasQLTNi{^gDR`BrgU0{qMcTpZ%#f@T)(5hkx+H2i!mP z14tw;J<*Bv(umk0pI_Fhm6*UtSv!9nDMg{-34VHX;Cra`#&!}&&>ip`MFwcfcxr9j z74hQJ*Z9Kcp6aK6<_rD&$>OIkuFlM?kVxQqq8755p-DA?!#^Z z$Eh1CkcRF(2A#R7$%BwimwfL6E&#zu*dvJ})jWN{uDms{tkL0RIu*5MdFfP1APk3j zNC{T6e11`DgBQaOzUr1(5FdOVw`V32YEP3fFyQ^SzR*(fN>lq+5rY60RNOOylX7qd zdUSBCtWeer0nmZw8qzAV&{9L2jtgCZW2so>Qpa*ER9Z3Il1foA%n2ij9bZrO5b?OH z6Et8TYt?q|5300)R!fa5QHsh)M{8p$T#d|~;ML6Pm_x(c(g=-lTiX!fQ&`)O8?rwP zNmqzm5y2uCAB733@N?Q+8Jd*ieT90vg0?jK&NhX%(uGi(HhEcs6%l8S^ChS)gJ^(~ zf6--ON!HAm$jPa`?Vm`AMAZ=AqQhbMz*uZiR1}lwE1P~|w{roi9-&&|3=NHMOG}q2 zm|WZN9s>Nscm`|6r3$`#Zfq_JvW?*Yjs>PN$L^~>{^2LMyT5mMur|TMbvL+)ul@L& z^)LS4en(&Y{Cz)rwy?Vk+jDif)zEHobGHdHr4cED%0enmvA16~RKqrdcD;rHMFUaz%JS1ZMCmMHn)R0&nw zV1eA;dss1KpnNt^*J0rzR^*-MO z9~+MtR*6g8i*lE|!5z9F6K%k$7&yFfO=Y=(Xc1Bs784@-d=vnbMMo1hpg~FLWvCtw zm;uQi>f9vN({f^N%EX{boX^C!k20FLgq0q4=UF_OWjPK?LcQAmsJ20jzc3DC9k40T zY89vmvRQ~E`H{;g`^2g7AC4YUUH5;?+MDMn=tz)Gu`BrvPb%u)Yn?>7cRp7@RYoOF z&J-V0<$~YfzDXAxiKs#{u;^?V2{{R~UqPty`*ZRqaCB$FK5()P0JR5tzvZyQN?<;3 zhnb8?ygQ?3Q@kGOE1RVYPV-7K>BBfc+N{~u&?%oC=NHaPhUyQdMxT8Na-HX54ru<2 z7(JD9ros}w<==%J!-yPOZ+>xXN;Z)>ibhxwtRS?LBv_L=iQq!$2L9lcSbjcLbX;yB zv`P30oAFB$R-sS746;BRj(?E;+eJW)bvx%Zn{TP?+WsH_2A|Wb#}Q z0_v-O#aP z2u?c&lbCka&IxJhc6p5-23KQ^iS%WOsk$&%F5f*JpGdbdfl-shO_0OS&vRaM4nE{V z3daNq`jb%0z!ETwA=6&ctbze)r%dGYs`(0+xen@;M+D#@>SHh_jb%y$Az@b5V(~j|tXGuhkHOO{foz<2z?-R4&)J*Y2mo>){jN58cPplx7nXa_ zO_r^zqa_0$WTQ!Up)2ZfPcDm3?(g{N$1m{R?|#2O{^W(;eB)_7dv*_97PQ4Hxi4s1 z*@%R;@y?ESbi~J-f)8BrG;_u@Pk82wI;pf*0;YJ%9TPniv+4aMT5z?z;k)estCS!j z@Kffrw|#O?QCA?Aqd!>G3Unb#$NktlV?$bJ+g)dJMPR8+;)*Lc9>prlikWNd?j{SR zF0geORYlz9p{OI^B$z(Yz;4$<&IM?ti&T$rI^_uFJ_*38b&*J~| zZ~oQ$fBEnH_5S5A{}{zf?zb`zt_e?rvi-7_4q*Q`#*z?lzX~6`*ir%i^gr}A{{DNf zp?$$cvTR~?w3^CMKjc7w;K+SR*Gx=#=FxAE80#BwX!vtk{28XU@JD# zZ&|Odo0WkrFspM~-$)QEn2d#vs)iMXZV!{Lgo6d3;{_<=Ay*eJ9n+M6CYdfi10v}4 zD2#VuIr=otwA;m~7%=C8nXQ?ydzeGR#F+46p#2^v?R-#fJd37%cw-4P5`?ZL>>SA% zS?LM~-MG!6{z~+;%HG#9dt*nkuvE^pg$?zJQ#K^BVTB2c?~%c9kX)>MMq+}w7$9E@ zsC_g|3y_IMz(oLd%Ibw_#aD}1&eIX}5UhswH6c=@A0Vg$^=L;RXgX3CsB%E-c_6X* zP68#TfeWj8$C9X_y}4H}L7_?15{lJ?+6A7VA1)&K-r4AFIR{=wqQ(ge!bPIcYkF5b z5YgbK=!O((hP+8E-$R{AK}IllVtY9VktAjw1~))nI_O_vdbzrZswT7%IJ3M4indRm zn5$DjND&)pGPKdpiFR92Ok@f-&i1X)x?ALu2zE^?p#X-^zY{H`Lhol`ySOvS7G?Ke z3Rawkvz$oNB17=Aod=^0!fsez)mNniVFekp52$u1XpR%0r%&$i$w!~^)$3QfyT8=j zFrfMH*~U-&t}!b7x4S< z{?-DXD&I^e&D*zp3r6c4$4FY+hqtUO>{;tBa+o8aPFHXow0)ga%hH)!k1QW#Ej_Bs zDu=6kII^C@2haNfESoEjOhO}!!IZhTw#)PdNJ_7lY}4&5l{i$|kx!AJY8FKmw|PXy z+Dbv@-D@HY^Ehc(4GYDp{b3wWF)(=$0Nc#vBr)h;3QPU~GgXhX&hIt$o+{h%H>oME zc82%&HNd2U;&Ez3W5MK5lnD$@*sce^X5jmHb zHfv`SVc7D(1cxEudWCE6B$3Tce4i_t@0s^^>xdcS!%`on?+Uw&kJr8@=W+Zv$+#lq zmzXe&hsiq+RUzWH1`?{r#)aib=W=KOYpVZ~RP7vr9P@*Z_`fwdRQ}90&J-tk`$-qH zeI8tN%^nNqoH-hv!gBJ*_Qda1u@v)F2>^9KioZjr&&>!1>>-tsq!NbDf)50FJoBJX zv!TgmI?mqXPIYgXNftq@Ol?| ztaB*pKENR;!}a$$R>TAYyMT@z+;h6JsMCTYdFczh*#U5`oh#?*#qjFIm`<0JYZAS*L4zK#M2?9(BvpY zcMVXeF*mf!#rEAfhlI4DHjU{qs03!T2$G&?-sOfOHYO<1k3M^4yB^6DOnrFv<79msSUQ())pmRE{jj@p5Wnn)%U;uuHO6L{kpr{@r@@> zg0oyz=INul*OdUYo!Ie$lS2<%spUNF2pqZ~19>FDI1rroj@uD|#i;~vQ$blv$Sgy{ zR0axo%4RN6FsILVx6D~g=;(ar3a4Gyq!0r#x?;jGCs6Zkv9+UO2tm8Bb4&BF5Yek6 zRV&r4wiE-ajH61b0%#;t?EH8Nw5vw7s^QEzK_>?e?(mM5RS|M8Wp-4tcjNl-F@N=E z{;~R7zxjXX|MdU)-_kF9`73zy*%z^|ub9gSicTywFxODKb#fH5j{?tKrGN15EB;e| z<}KjT_~D1GCznv(Koyo^Qv|K_=Xb8pgtI>FStcdQMbztwE}P#52`5RkEs+5QzrR40 zc)+Yf%akA$*D8GY@k77));)guk9`?G_}-`bkw5Zgef`J2pdY^fX)1lTDJG%O;ot^q zQ9V#~S=$CpXe)w))b+uMhS}|wn|Hsa>zIffWCyC)<)mitB1%-#1To+3PP-p)NqrNI zYd}>UuBc5^22YKi9I8zch%3LDQE;}D&^mksp*_h)O>TqEZTyMYw<^KSr#Gxi(U6*A zTL(>wTtX&HYZ*j16~k+QEt6w}cQAPP{a=@bwf;kn{qvVIw#4q0?HBJ#ESCX zQ}z-g%huZ3nyKHBwSB z?R{nz_|+H+rSn5u4-Er`ZqqR7%xWM;<#X-jw%_SC#pT%NGoZoaA#@V!qT@aw~kEQ1-tyb06c#MgP#0qasc@;LVj6qeW>`R?jR`CA@> zg!|ts@$`9Tovc{!wpJ4OTC|n`08WC;`F);Pb@JdzASJs|=6j9iC4jLfJUOF_0mC{D z@`rLvfXdi+-X@(VpY-=mCJ`Wg9%i<3e0tVp?itTvsPR-lZQCI}J_Fu|$re?~eflu) zhmJ552Uj;cfyz!zz~kne#E!_zSN3^eAY?ATlCLkW0UjaVRR`pBTc$q2@U6wNtA*P;iOBp)LWX{FU;jJP0qW zQ`W82+sb~_RWmD|7zfGmOVxAb z9`&G!r~GYU?HU-WX*#8~q=?pD17NIGtP8tqKU_cLFaPv!=-WU1*7bk*Fa4+Wwdc?H zrMJI=SJ&6z8sLdZmODE#w{*^(?~@30tDi37x8HxwU;goX{G;D^1Hb*2_?=#hY@09M)DRmArO zWKeo**sO)zFITidOmBt>HWS!v)Fpw*@~hhg0(`FyoG)7V*t|k-V zs7%gqS5G844G<=z_Wf*4s_HYl-Fb5N^bVhW{0gsLJmBtfrxW%Q{qWHEg`fYD{@%a; zUHrTM!Mpg0AA6hEhfS_Sr?!7rcrtc4SZJ;)frrlko^e$mxp2Mi>rZ_3^LqD#&-6F` z_HW^5zx)-vZgFS?h;iL;E`7J|@yY8C^qRn5{I&nOzVn0ci&gPaGL1ZObz@svL4XDf zQ6d($baC~nJ@OAHqp6^g?9_?!=TQ-Gh%8{n-Pj|V0ZFLUt#(t=#M0oV=r;PpgTlCE zCcHEF7C1|4;eyr4dz|;Gdel>N!EtxnEq_UlXev?n5h_(P=64CaIeWmKvU{PLVU!X_ zp@rcZHWCT*v=94$o|X4h+Zr7a$HgkQkx7YLF^@@8~e%57;gDLFPjYV*3n>9WCO29-wW`Ph4NE8@#KoJg@P^e0N#U#tg?}l-}iM$iq zRhD`u&i0De$yw}9{qv<|Fa(n`jfKmR&ZhAQ*_tppLVg5LydVl7w&yu?4oZn_syim; zGYBe}e0jd&Ky)4!WxzfT+2Vn(V%Xf>Oe77d| zON>@EJ*U_pL7{3Mwm=0k3rqG2~&d4LHC~wSI0N;7Wa}O0ELO zDPa(h{PFfX3DDrn#!Km~&8r1@|Q7h;!(X zJ9rcT1RD*v@~C>&oWHBXyF7-40ppQSU}%4%?O@QL^UM{TU@nrgO=v^1GhaclCUF;l z?tMexl=xc0Xc?Hi?fn=ESm(E$G0A1fPT+;{ zNp)nxY)2hfx-TRJIMi-(N;ZMzML8ba7Cfj<|3V^S8!-ndF3CFOZa3NG zCg{)Xp?| z?kkv%3MvhIB@!8{jFNbZY@AON1Ad@xNW4p_MwMi7s007gQWq6=It8rD!iyK5 z;mKXqFMZ?7eCG$B2nD`;(Ry;f@GHOg6}P}luE@#}F`5Lz1ZI?Es zcVNn{fJCX$7z!3abg8(t0kK&&mqit-*IdF;IUu`)QRcVna&crQdaFqe+6$2UV_bu5 zpd9{(K;Xs+xB^YFiz~>aD7Qes`J2Y{N`AF9yS1Uv?-VQT!n&( z_Q9{0MMiwyfoam;?RI|kjF@quIXO2bMJP9ab>>X_y0&bEdcrl|c31F0-v zc}QAMKrJK34b)QVy~jAZnM1N_>uwdvt4@0^(bP73rqHNnwPp$~X!QpZc_g__#CI`3 zhUB7G7I+oeJn|bniEkXKx4Y&M!N#h>ZA zum5fS^pC%(=TFkZtj%rlPdjUaSXvV{)O1TNtC-Ndl;VYT@%ejlzb-%Zqi^GX^RN7! zdcwuG-+U@{iz@(az)PzEZr;@$e*dHI^GBY20YCR6zf!;V!~YPM%l#C+;d!A(KaerQ z6T%ov*l0A>i*}8RR7;s(O|li6u#~XPaHXU1g1YVAU5p98laySN0V$I08_-VVw_$zQ zV@<1ZYDKo025tb$4-|}MYFB4|CEeFBq%}dwOCUg{-O3 ze%?Ttnd>yjfaq9;h*r3mG{I^mK0Waz&Yvg#I;rBLyXbi16h^~Hg&}CpXZvQj5EmGw zj-@suc`Wzf!6z_G(Jdfh@)p3DA`-2hiUAI*x~!4H$;^!DfgR=k_^GAFD7;O=e1ho& z!AMDFP8qIpn+LvVV{uncgL zRrh=#vEM%ElE2^>Pt5jtz_}5?l9RuA+pb|>O!g^Eg(%%r4(bp^$6NsI@+1e^ORxp4 zB*NLwax^#@tuJc&+9u&Y2xTrMc^>JVCq!pzHRGbg!WwrA+tFM;=vBY>cp0&FK!{Qovz|Ohso;+^tCr zeVKU#0XUD83(?+BIl)oKW##x3+ zl4+&36?314)83jkeAKo|fH$Bi(nF8N$f8(Q0m>DkYN4P7OS7e`xr9qjoGD^0OKfqO zZQ1QyGKonLa%*~+n!E^7=Gc1f9~BwtB%Kkui&dyZ>g>E?xUC&L-Am9_6AqUR$PMrr zAK*?ohfiULIHFK5u{s(a-T@Ny^)qne+|0!_o`eR@D zMZWG&sJ&^msXb2w$RoVn`{4RyT(&bEBo4o<{>|^d;(znce-592B7XZr>2l|5XI-0S z{r{=@^JwkU>?#b~`#it*-usO;k6qQ1)atIDBs38TL4*@#0!G5gdx~r5yx^I12z~)glJg<8d?H%x4K*1QV&%LANis{Hpo_XNBAw!*9)FX%*_T z8(ooEHC!_3fecVAGUm36J5A*_oeqq28YXWVO30$BYh_hMqJ2551qXh}9jnwPn@KA! z+JcW%8PTG=0|S|p7?rN3lI_bAG0?{v1c=JLoLYd+ zGz|xwpNK@1Ej!T?X&ERKkQ|)iJH|*OBwcZq8lzDeUIv5-b~%v~G{r*(hL!h?he)Sj z=F*UqQo}hIQe!Z=f{GdaV6yju08la#)5VN9q-M^c5@bb~{!!AFnMu1jL`Yl2NTvJA z1uCjcJ43H93K^rS5|LFytud@BBGvd)wBTA6ti>WZ5o|2mhyVcq07*naRCYSl8CmeF zu1-bRu@sJ{rUAUa3&+PtSkG77dHWuYcDv#jqGZ?NZUNu(!c+DC`rDs~+jsVO$J57L z*Q%O8q;_BgFmp<+LEkrlQSzkg9)O5Ja@4>qCb(9`cYMne`IEo+a{knpU#t&4^SJy3 zMYsVd4xQr#WDYDfnRVlIUqAdi{&#qAdWN(8J+KzU&^9!)ojyjG2skCEF2WH=hH|Z{ zRts?pZnDy7L9^=*xs=~3qo(v)Gu4x*l36oKs-yKu9RP!=dfRkNG2&CVB3merd&y#;R1iDFjJ#7Jc4;`RstY%TQc12%%PR9V-bn#+}up;T~~GM%-4w{-V^5-bYWy?doZQE(jlOk-9hvihPkh&#?0#-dNG?G2 zbM^~?(O)zaYI&Ogd(zks*nAW=B>@fTTJ=!`s^y^GLBK|MsL*)8SAp*Ru2d!7S+6T? za95@QuB*BYP|^Qe*sAkriktu?d-w7)3^M?&3C-zM2W_QUDX9-J(z$o(pJs+xB6Jaw6F_wT9qzhwx44Bz0AWGwdsPCCx z;40nbmO~T1AGFG$!8Z_7_ZC&I;c4Bf%6_30V(ooeCIb%t+MmCDuaoFr?pXr$t6rcg z{7AELdKU18N7v51FgD{%*f?eC9W|jwKXX`dQxjPC6FCbb#;i37a;$5PtTlp59Pf_e z($VF3aCXkuU%!C|_wRE#TDaRC8w*U&k{XgyC$mD}e;|4qJVz}SG17EeF|!zM+fYwi zGHX8%umug_$5H5Ni=rTA2VcSH^n3-uc*f=F!m+|M*{dmJ4Tns0HVy&3K z>DeALQ6K!k6L{^7dq{bx7m++apSX4ZG(P&#r{);I-Ftgke#|UO|5+ic*^&!z*gz;% ztuG!O4c04$e7m#da^T_G76B&JCw;!^(`~2FlvCD#`#1hX&xL!H=QJ=6e?m^ymnEmp+%vL zySrkpf10HsC?4)kP^NMHXu;g;TuqVpjt9b4W&uI9Nh1Uu3l!?N!JLYR| zz09is|Ap`R*YgXnd@6DhwyOseYN`(Nitm2(KWEvr|4SFrd%3aetxiP;-HVHGkqKnxg5IG1*{ zNx5eKOy{OB#j)Lt)0s3_-;7E6`ssMsx_PhXV{f3LPcYl(S#w zww9mcPE2e}S{FV|0Icj(-G%7R*|Fg!joH>QTqd3MmgNgME`7 z@Iy1x2>*S-e1T_Qlp~(3ES!K@T~fxv68ZHD7#30M{tHrcpp6FzK;|8UDIIJqN|p&g zK#$Tjg6$&R@8c4+uu*Og5@2_{>H*pvv#Di5)z5y|ZJa@t7ZlKyEZZb)ER8mhH8xQI zhD5>rti3SRf3#hVt0>{4NxMrP&G5s~LmlNlFSd-r^hzBzWmoadsyI!fH;(s~Wg*QR zb)Q{yEOjER^D@MG^^1>wZZ3p0e`obX%jFHupe_~xO0Hf+UcPD4L5c?gHoSAK0_eUi zPDq&nOG~{O2!ZMF_N}mL0m=)8NQe<)zkq@)ZPL$&NY4z7?9HtYR>07Da0oyr$~nt~ zL}vqqh=LrBwa9Zcc%cKA%{dV0nq7JEt3pK}dqa3%JGgCN-lRk(z25cvg9<>*K(?&e z0kQqyEpYcIc0dQDU%&93l~CCvog9S&cmZ56bB~$D7{RQS2-MNo#qn;zT(e$%{k3`X z_N`qB^##vJixZZR*6JypX{)OZk2^&Z6`E z;>HW4EW2m(lv*9jAEqYGM0M#t+RUOnd|k?BgeRR zXOG=7R2@Ph7;_hEgme2m>^aVaTK*b1MZ)# zh%qX;+IG|{rwZNqL7)w+s-|sGt&)Y7e`1sYl_*j7C_8Fwg=(F=(q)2{qlo$rtd4o2 zq^2muj`<*_1@R`RAhH57s54RNQIPO1q-d6^P83+0w>%}Nbz)fdH1S26_bml*97fJ& zV|@t#C_{vfl4&F_>An~*)&#?&MJ9n+prIJq1U6mQB?1F#aXTcUA z;A}&UCPu(^lt@!I0<)s`sw#7`z*vkPKqMJ-%brfqBxtplOBFisTh`hhZ-sKwX}W_I z!ypN3Id{p*sS`*-^-eg3I|)xWP0;+)NL0v2 zM}S@YPJ1BLbngr)WRcq>Na>6kji5xprv)5)x9`7<;Bk3%L1i0) zm6*2cg~>d%Y4y2?Y6Yp={PhBot`~G=wct-yTA(2e1ys!nvt5Ezi6W~9UqHBDDOqJs z6<&#&>H>lK93V3^8I4A&Iw`?Wv7D?+FR-RZoRmS839LL6w2&pmg^MF;wILkj3gqa8 zTh&&xN$A*-rohWs#+y*fo?=Av=X5ft6m)>4J>oQO%fa|h}Jq)dVeeG6l z*8TOL!xJ=uny_L!9P>2j&Fqto#!Akcsq2fPIbR}dBG}4wDfR+ z9PE$hfnY$d2NOG18{|PHqi9X&Sd^^80TyNr61@fVNSg#U7}m?{nvWup`Hq6h*8>V`Fcu#<312Mk~T?s%fcrjl9QU-_ekfdl|^g?g*MM;jT*QxIxX65v} zRHa7(92X$CKWaNDzyVfGkm9Po=R=LA~n3gTe{%v8-vWgIu`kz3PP2)xUl904a?fNKhCN}(L; zaO)v|Rm0PJ3SfFHwYFR&ixJXBX9Y^O1RERU7R16@Ojg1Mbj47D45b^DE)TcU6D!nY z4ep}jn)KHIIXQ}jtO~k!Q3pwpg`8;X0~Qz=hWpv<2w(7jJB;Nn@hcVC33m}G@8>Z!6na&t85alZ5tZW4?L976dIP&$o6EEJ` z^T+?Sch$YM@XfbY97P3I$zOv>nG*H{_0@-i4okHc2(CzCX4j8GF$Y+Zb+zLIP;pGRg&;ykb9Qvx>Zm9uh>8cCa@3*PbLnOhq)0{!VH74oRGNfy6~n}tkp0jxFkIaA zISWMPBr8h(U&14xUH#i3p{kftoGOJk&dMq*sLDj`oFUH64Z3MI(#BJ0Zn-g)DZib?%~y>O^PTNC<|j`47#AT;<5HH<0*2-Ew&6E2yq&n>UY;-tFw zD>aF($jQRYsU?W*%k{1Pc%49_0tt`c#Dv-Wc3vvqew9GpBqSg(Zth2ABB~rwt+(&vxrZC|Ml1L*M8zf{y%>9<@}W|-oyu=f21D1ykJEp@~CAF3}_Uhd2O6m)ra5tSp7FY z{RRBoi(ln;zvD^0F%O0%eeh+W)Crd-V~3Y-eh~r0AO3;=0Kf6_C)b24wXR#HxR@$| z$BJ1U{~S8CYbrqIsEyuKyw{uKM5`#PI_`>oMFiFKVeTC!u-xNNZ1{;BLc0|KjtGck zRf`Tp<#|$tUcFAIuw(6^99yVNa1NzgOh!hOjU)?@rpR?wM?Ouyb!v8iqQIdD4g+wa zP{Y(b00H7Ii0U%BvcIL85LRl&dBa;K1&mP@7JTA_-@WP^*yQ5bxX&?H1hg@Zz6Grg zE6})1pwbE}L~Jlx`I=rEdhC6l9qhNP#>nlNpaFFCcGfpHAJWf3hgj)i+0UjEQ~wP7 z?=`ll2R1=qMS%m-h)&RrsaTQ#Zg0(a#}3135~Pi=11gR6yAzz);v#?q2mA!pia^WJ zHJQur#-HzIf_@~}Ft%f|SCyY-+9M3;(|3EE{T|Wvf{pw$O`zBqU5%EKTL6J|zr2)K^m;GnRjAnOR9O?)UgHi@%4I*KKT;4)56PjUP1 zecU^JP{+qdINlw@G<#&a+RYxuqzp>iB17&yXs+>*GIDU*S@YW7Q^dxilcAFPIdk|V%AXEVqHGrk36btL$i zDE^_kueO|%ij6zhIP#pl^FXkIj8!pPm7G361U_Ax1Uc!(Z1XR^?u2O#YBsW-<6rq5e+Csx-MIbbWw$#?N#IAIq9SK;3PDwgWeOxl7292{EM3}; zYAF5ztjR6C%4rr$UK&odDl2MKZS&faq30WVpege`P`;{|R6I%rxd3J#M8?8UdbTZn z2mPv2^58`|QY?P~$yMN(&R(4!Nluq2)>>68m(I0{twPbVRw!R$Xzht;(XA_A9i7sM z=e7Dc*nm~UVRa^I%n_X>Re*!=C>#G&Y^k?evkYJxXa+?xMgX0|`uy|p;Op6}P4sBE z9Dbi6S9uIS9b-9CJ_V?a%y9TZ3|GQ~ecR5ILpM^<;cJ-99aZYbAhUpg;fodl9Y&z04MGMMLw1f6|z z_IqhnJ@jXJVP(|z{F)rl+R^BCAUwsP(^}nA0G=44dJxg7h1p*n8zsJdcZ8J!6^QLZ z$^tpUR8ZXPLrYvRC`l>G+Kg@>}t-iu*2|I@wj z`u`erSXrrOrN7q!7A+txw~AIF^I*i-^7ejSYC7~*Pp^*vGDY1Ob9egbprPlYwjib$r|;78ImjKg z97w!h9hO>*!|F+A+4|U%P%o?JqPrlmhtEKfG9L^;Z}qGUawb8dInHqo~>&y^B!lC6Zv{ z7aUh9&c9h$X0n5rYwpxhNzqu&`?UP43jzSjaWGQ88d@lhPZY#SuB6vLd{{!XKM)?? zrCkt!r~6EW%kthzr=sL6HKBB0v^=tE>kU>wC6pWnt5O&)T(ioOzL%kSaYFhGl-nUB zH7Awd9*#TvoB83VKgiSbb0GG`8kBrc zyFZIX4qb=d4QHVSn{_VC41iI~0DkeS5AgdxaJ~Nc4?T+?|0l0vw=5WBG1L#O2h$xB zt|c#krhEqI837$PYqYD_){X%R|8;dE{m96v6(I$YTadWQ#)I%|O4UXY(D);z)_nW+ zS-kMRM>%$d2WPVe)d;2aqh&Sq%DG>Qx88oh@BQv~^2+67y!F;u#0bMmvee~L+f3gS zjZla@ih-MV9wa*qR*+Y+>OzT#R-z}1tQOm({yENsfcbi(C!H`>39G8GloFJb$v$^~Q~ zC&+b$0*RC7hOsdVY4#|i&^^FNWH720q(UMvHzNi#Io#6UrK#%lct)1mXGiF4N`RSJ z)d~aAl{0}Lhw`mprd4rCRFAX-SeXLHb|4dZ(9hwH)C^`G_u;1c&6p+=_Vm92GZKa81a&*Ct;a0z!?csv|U}s` zwC8cP)YIW>z%hTdpJ6| zjPnQQ_3rmP#^X!HfA>fJK|ONqD!%`_p2d?7Umf@FBmesU_oey^|L|-0XD^@D6Axd- zw>@L1<6Tc&uXZ#I8O#y0>TF;9;JY7VEUwQ3Ib!D935BYc2V)clGcuhl%tFLOd9!xV zluqbScnpV`*q(;>x=FJ#3MK?{vvUp?Dd=-0dOf;`7S+J`@I6(BrZT|<0d=()8=sj} zjS{HbKs)3(uOhC`Ac$sc5UN|TP?WkBT}J?!7!w!F;E9ctPueWW1W20b@VIi-2lV|w zZ3T zNKIf&;XHQo((BCd6JAd-J;9LS9k3d!gd=MQV?$AN|Eb7HHl#)SuK z6dhAl?~C&mN%wHrm~JB<233)?lv#te)3tgp>{bS}kpgXWCq^Ar?&qJo4PYw$D6=bd zAUt}-6E-zd^)}2pRV)ZsoWff*{QrtG;w#&zPfp#=Im}XJlP+}HN2@SU7nr{eq1sTL zw6*D<5&}ubnHG}FW~yjH0UD}n2sN1YbKGbVl!;CKS#Kb0_fpXP_8vqh_`bICJw-pW z5w2Wo8&=;1wI)avJj9lXot!K*R5EqC`5}nBaLVRIY#`+o5E>cm#7WP_lK-ywvK&aH zUGK@|TAu++4)q_pHDk&+r8efJ6L1FyTewC^GR#WU=$BqrEp)r6)fW8#!#QdZYXMj! z#nRe{EvQ5egd}7GYvJttyw2CP#!hB*RjQOU-_5PS>R?rgqB_7k)fWq(XB{l20mGM!T^h-X5EZupZ;z<_ryEvPyCf<@W$x_eDIkU zV$O3gvfSk;kT4aqGzLk3b2t>inc6)`sa!5Co~%&5>b@Y+ta-nVpe;fzinRmBxd z)D%&`xnFC)CqDGyM{)D^DNs?wqWu>g7lFw31E{)xw$|CY)^Gowr;%B8_ue_V^tV^2 z&Z!9DJybx&ZnwkzyXUOQTo!tNim}>a%f-WE<1w8AN^B~s%bqh?Dc(R*rdB;**5;bS z7;MQu+#Ap-k&2`P+P69<3w5XjsV`8}3Ta;y5KX*Dy(QDn*l=6|nb^P;T&xMpnzy~G zleT>B2ND&i;h(C_Lz;+Knx>V>gMTaA+~WJT4`#V{g*qo(Vb zAqx<7hZWFekm(I#ZVujQ03ua$Yzl%~vB9kz<@z7s9ic!Ex3UX`+HZkdksz%BBdf6W z$cl@ZM%pdH@;RFji2@8($b%5!?Kr*7sj9d}pb@rAD7kQUr7#^Rz>bY7-`NmZ-rsNM~BQ7R_j?%s)k47G+iE!7QZ0btV* zX!eU4g%S&q-LOf^C|et#)_|cDjHD0{2=LepB2roQynGavup@3YKY1Z}QUA*hD zYxVbj^&6QK;%NqXl%L1iU#+c_~Y-j!F_&hvkVntE=oAhpb3sn5#Gakrz=` zHPma<9WbKYf{ny1l0ain$R9lY3U=dSH7Uq`pqhq;@AqgHtJ_s$vHifQ3 z-mA*Ga`m!#Ut=pNutlEQ&BA$X0Mb?ICc08tjjNp8^*(_fR`!`q8CvQy`?^iTI0S74 zL7Ai~C#3Nehtx7%t7QNg)(cvR#oE+>?U>sw0V=lz#s+>v1%h@`sO7@F_`fRUC*+cg zC{;&alN((uMqg+7&NsDlU=-nzpFY>47pw}4w9W2@FZ3*X63>Q0=$yV6h=_Ys2EuEH z1z>0@98tg=E_evn>2m-M>8YlGAc`uIhYQLTDkhhTeNrk^h=~lMhLrtMaYNq$9VzMj zO{*#zEOzf-KijgDUB8#nz!(ra=)9*qj++PY-01@f9up=gLnkd+iDXKnQw_N!W$oV< z_*as~(8h5(WKh~Qg~AMjJ4;I%y8~TC1cG|#$xwiA8Gm6j#jCDrcMk`2h{4kQHogfkm;3vS_U>rK|*s2Bspb|Z2k0x#A1?7l=vnl;HHidVC zDZ;6Mc83_i))$H`c=b%GS?VWsp{UD8={ptiUSGJJ$XA(ag!f zh}|HfhD~A%6`4qPPGeRF8)=+bODBEKwEMmXsgsp$vBI{p^WqF^kX1I4@dzVKw#mRn zHe)(s4{t7!n%gR{wcS`|Rkbu)vmz={IcsuezpX+9s-(n8lVSo4PH1#Jf^O!VB};^WM%;g zwz-%YC;1WKU(GhWaHhKT_Ja)^lJ5&gAH-WeXduv#k@F)aR>_LR%uu=stH49m_ZX{S!DR%=K zZpf6;@!zz8{>*_gf$O(Wkj`$^MN0OFT|NZ7DODOR3{hOK)FH4 z;zCx+ELRL=p%GB)hWl66F6L9 z6Z=|e`C|FJmN~H|p2#Or=Xa_HDWFjA;!V2YEw+6YX(Z(5>Elt$K+(G^afK@NwRaOq zIfka>{=3Rr9Prc0gFIz)rlwtL%OXS3yPf^ohQ)~|t&}kPX=(jZo_@fX+8hk2K)Nq% z{uVcjDJIcp3YET@R&Kh!fa*p-L-lQ^h9U(^;Pm7+|t1w-saRyeg*+Vmi>qHW9#BB6mlN1~|-_58k@N*S~QC?|AN7eaH7a zRbTr2oA@{X^gphz-FOgh-9E$L`Pnb?m%i{;zIy+x-v8uveCy+vYe!;T;W5?-AY*dn zn#?CIAK`2*eEf@V*K?0vnzM@M`x%44_kQ3he&rjt>yP{=KgXx9T*CXFc^LQ4B;!!! zo9eWcA4DaI!3B)Kr@#3Ue%E_`55Dhx|8jl#)nBTk<&v`SPWlnz6j>5-wCbgj$v`6I zv!HNR8#b~AnmOS&i<19^($4I~hl;4m%tX%Wj?&N}A_0-|W3A9|Q>$P7gl$PE4mU`~ z0xX%4)Tr8;L@^S>9tD=RDHX}a5!IP>b;T-9qLi>SA-?^j zQ_h$uS8M6qqznKUVU-0Oxo~JV1RC2cqCp)Q8o*L8sPkE`qYs-#MQ2Q>uoud> zI=oqy$ESz_88`U%8Wji;DBEYb;>(tv*>RrRYw@kB(xI!?|Cgk0I_e47!7_RQ8RQl% zuQU+WS=$w6S9b|9KD9+FGf-YENOZ5`7P*SL3>YdkE4KYJbf0P)H6!PUX<~suSB1P3 zX-PW7I++|HOgj33lzO*en>Ok9ld2xd ztyFg~(2`G`3kBzogae6IpbUrwyj2U4XT{i%1Wzk)b5orgmE2!^BQ;dybs&Z0Q527ZdL1wPq(YBA<}Z6hdWy(^b#_QLF2c zTPCPy6D+KGmWesFP%g{Uq%DKKRl z4FRm2m8cn-+=)V$&B{zJXH_6I@6>6TuyR28gLjQ7?l@?c2~wmbGJ>5&xGS&VO1r+R zoOtVP&&q06T-B(gAkXk`}DlMQ?tqTi1YWL_YF`j~R55dd9C>0w+##`LM- zF)Y^pm)ZSNb}|ylX?kCSrG@=`kx1;%&hf6Ne|0;}2ctkALh2-~HGQ zr@3a!q$@@tBO9>j{Z|c!J%Ki+7FfiIsEp}$?-B?UnK-00OpwD}%x>0{hlIs&p8ph~ z0!(HKG+>UJL<|%rap&Hi?|c8FI6s?Q&n5=dAOm#~SGor%l5$`$@5_k1{q|`*`p6MJ z^3f;o=GzaLv&aY|VFQ|~NO2-(}C3~>u`eR zS16LUvXVM0rV2F4ZF)|q>9hd4NSiw6 z#5UqE;A*rveGY_1+zW@GH0DMyyUNq(+FVMNnh8q&;)6B7TAe1947amOCT23VhLCW( zr_Z=blwO4JF;PSatI4?3Yp9%6WWfe;xd)N5tW=n}_@|17>nijLMWpWf5YiH4PrHVt*K$V?si2y5un_IY#dxC09r1F%x zPR#RjjNPp3SC_baeZj4>bAINtZ`8m4zx~Jj!+-W4;xjM4g;(#K*KfXdAJ?y6!h4^* z!goD>1-k`x23OIC_xk`MSV#l#2wXqe@#CL=wJu#*M$L0v8cTlr`=72~__deo-~Rvn z3g2_>TD|Y7>vi{ZMKEJ=`N6mujA?X}xHK;FH(vQ%j%BGo^~3)Ke&eg3uCsaHFMRDz z??O$fcP1eYfHgA_3P9xmCdp+WWr`Fr^DZifwR#Q7$NrN;asR zeVtBJ1D)S0)k=Grky|E>Dz`vo`(l z3(!GOX#bS$+UHj2)4Q4%>dgc8M$=kYP}6L6L8Da67-V9WeK3%eNfT?>kfGVcFWLzJAvcvoz`}{d~h| ziUsukp-_2)&ZpVRYGpZ(~bFE2bj zux0=gs!sOE)@ZWDrhV>Pbs+Wg_aA9{*+gZAyi>fyim6r#6y$N>#F|aPjQ&BWXMAjI zqb@PQ>1Q27?+Q-K?Z=?ekGZXTi@Y)C#QAxx2}Hz6?&^#c7^LFIX2xAM71 zKZHlFU*o^^m#*`zx{L34`rE-u{YLU77K#3K-N z666Ywz>u+Yp-~$Hk;y6fgw+w5@^AK0%yK`xPA~oTpr=^y=ACmcOW}q0J&GG|-zzMU zKn}SaBsNFB0xBj@QCI=&fLx`Id-ILEeDAv-LC(rgf9AD%`l%~p?4sr#Tq>~A!yXXD z0FO=rZ@lpU&pvxSgalKkLs)Xi3wU}-$;@k7vq0gSimECOru<+dbU3jpVM$P>9QB+u zvk#!HOoB$MBs8=Tvrt2OS+Q7Hp;kgO=u)Br4$BCL!c>*l=Qj#qbT_d--d|N}8);o8 zE3%det^+lEKFj8-fC6KNMFA}RSD%Y;H7YrotQ_)vKuc#ug7upVqN+4@Psc@6WxCxa z0WNSAI3a$kJV8&NGr-JQ@R?POXq}^4V^v^QA=)TU8G$R)Dpdp8_1WnH3+d5J`ZrfN zS#zosY%d9htbw95$Vwlahw*yaJ4zJ}#3!mM`uy)QLP${&!C7zviZVy3YO0`v+eeP` z8U=HSJ+LM!BF%TR^Pig4o?L{RxfOb)#Zdk>h+3;6iWw0huiLFX%AmtQZxzOh()ABR z#90wOB9n`}`>Ieu)-u4U-BhE*6mI(Afdyi`sG%-8C#XHzWx`pkF&M)+NiN{1QS#Uj zy&k0dKutpw3UZ_YW`!C7ee4!nSBv5Tcl<|Hk&DtbtO*`3xboOFtoI+_?EZaRI z*dsWja-d~-kXfBAzy#j`rWH*Wk|T)px%bofh2Ma#(I;P?YX zV3huRG(ROGJfcJ%y15PNg|7`Djj0jZ@^dC#9Tf2G44w0$YoEBCB5DAc8va8>iNqBl zC_w0TC8C^kg2emVAt%0ne2&96dBlR%kw|P~=of zRgoOW9LqR6J6i$L#+D{6%EgFuuIZgDYTv~%vTHBO`R#R4+54AmM1wwS0uT!*WfWLb zNmDgo(DTjo6aov{K>e$Vs;iWW9N1iFYAY)W+t_%)?fxRZ;Npi1fZ)JL|2beKS{M^p zB9B8+a8@G?0h&<(`P7P3$P3ph3#xP-MdZP1{AbHaf9c`*W#8n6(%r)5?<_iY8U>_r zYmZa*1Mr_+fPMb6_UvXzJsqj^!>9yRV0fvjen(7mk4%>a?E7rLHhOS#HRrNm` zGnW5sQ%w}M!g+h3g|Xgrk=;T^#Y+H-oE`fiNm5-?Y=U|SdQV9Nk(W+aMSqV z#pg`NY>2~~nWB=97ZUQ}RS*n#qLM+blAFU~*k~Y#fT6z+sJ&N%Ws^m0^7!q3(5$3B zCP@rsktR#b8rd;84Vag%;5UF}>eL{B(nu#X6V%VL0dGovB9N8a;%l&8PH3cy9$*tc z1({2zd?P}K<;~%gs?X?N*naO!FM6avyfO-#5OMnsnw%L}o}~k*PtY|0QQ3 zLNn34mZjZHB0{}yp0TGTU!_ni`Ljoz>6FBjUYcWWLCia*dVeN;AiK6FmX$Us0A?1v zw6OT#swLI5P1oFg09sd2ESaIq>;Y1RQa^YR!HOka=+|K%p_mJKdZ1VbI1~UUauVu} zAR?H|jFNpgEkE^ARVuB=rjAEa6%!B={Z-W1cyz2=i3NHtCk?SYA`Q6N@u3=IGq706MTYXuQ?d}+aFzx;ZA`y-d? zNB`mv)c^CxemTz0SWiE()cL;oV!c(H*6^?#iNa7}WFRrgiX<&f6e(@!8U%BoVnvrO z6|J%|Bvi0ujTxc3aLUaqeGrf%2Qji8qee)DAyzlYc9gVnmXU}DcF$?8cPqbvN zieS`0R&Ix8mxb4vc;ofEc;B}^8t3~JzxldTr?gpK^=$u~i97(K9%E$17|dC=2$)Dve$^!b^bF^r?w*idh=q!fo4r6HhuFj{R7`NnsjJK4 zg;sKu8nB`Xt`Gu{B!xhE&R&VXKqMyz*oG&}l2-vNy06l?tM(*fs8JxwOw{xczvDqT`(1W^gGB_|*-2?%tGAeU2s^>^o^SF{Tp{iE#WzHRRVlSH67loKv z7&0G;DzzqHgHBbbV%XP`t?*`4%^8eC`c-6NAR^UbHY9k^k;E{AYRzPcCLX#%l;z+y zDiL^D(MklLg#rk2WbGt^X$BS;%|SFfB$E{h);k0 zHfq_`bB|xc@A$wq9v=_v_o#WEoSYK-h&(Q>BLI<=n*(>1s+nM@0|pb7mv%=y3i8u0 zzZHMz2fqz}>F<0JAN%#!@xo(I^WiJUIGt-oRpd}-JPQb_H>-RC@0K0!p1*|`Z@yjs z^WXXJ@`dNWAHVqdzmH2Nk5?afSwu{i_Xe7bgc-lC;wxRySVm?stI>i1Y^qzC zWn0>C+DKTD zQSMAWPZ63Kt-Y_%hCBK z0R3zuYrRo{+hVT+M?8}cuBI=31}ap+#3#0VT{u&ClId${h}OX(6>>MyYaGBhrwIk) z3m>a$nKdae{64a=p2588@6{oH%fbrh&WxOJ5)08*sF(bdHvrT-Vh~|8F$N!Qtr=heg=Bn1XMd zy{Wf{vdPRZfZgt?>32|Dh1tN>FE;x`r~?D!T)@eQR+ik<#Z)U5Z&^)m$O9;Jx3M00 zxMF0MKCQ(_rCbh3B3_1$ACT!B*nqtPt}UX=p9yUk6$bqHQwo5MkZH3AUl-AfU_g`G z#ky_ol-@4o?-d{qzYjf6u~q`4cGBrD*w`+;=M*qE88UQvK0`L(YHrz2IeNkSInpRV zKkBtps2Zq9yPL{J2S9 zSGwA%R}ThR6;Z>G#G;rqhY@2^Hzpe$4Ykga9N2C`989E4Vv8R6rid>Nx?&QBp^C&g zY8$lR2?a>x4D{I!`G%%DtJnm-I$NjVF(Wx;N#u&Sn-;WQl2k`n4pb&9^;%K6gF#Yf?*SRAOk$8q`(ue$F6@Ag9fJYgk>= zhj@Dx>6Di$m}XzX&npBpX{xiBRZ~;d86tTQtjYln)%V~Y-x`_wj2AxeXxzMghAgS( zkuKGSzbn0?2qH{Y>L4wk7!la7iPvx3sSkYH6Paf@zWBAb@${2dY8m7#0Ri=s5+_H2 zySMkafA;|{9S__-U%4ZuX2dd(1apPnf^2lOb5*kF2nIhh^{lI0PR&|#1xrgtFhhXA zk5t2MdCTdkLtl~)5eTJiHAuA&6zO56V_7#zwyqJYy z!-5D@j)f6rvn2EQsc4~=4E6rADl!aZoH@7FTgfcA0F|+pDovDWW6BqE(xkHus%Vl) zEl{cLm`L^Z53+JI{gO=HOqq^>Nz{pM4CM9=}#c*H5Z;QR^Fb%UF)a;9zp=d{E(A!kZp)RILyZdEWKNGO4DX zj}=Aog)Bt;4_gXyLowvdX27*eOYipuHgdK?OK z=+xFuJ#J|h_Kx24+HNkW2`ekX7v^aEd?@bGg}}y1+yGU#4XazJ#TRO0YmCxcoZ`_E zTaG(I=Sms#838DVw$Y&!2)@>qRcTM-pm>G}lwogyx#iv>48kN)Izz>VS4_j8eZ3qO zfTBGG;-I80{r!x7rXm-zhRGGBw?ty)1vP3v!#*C^jq$=`2Xj1U(2^Pmd%39Gzsv>` zvbZ{6Ouv0|lLw6J0;y37S}CShjmAoV)F0UHU5PTV<^+e-&Q)EgCOq%Qct$0oLjRuH zPyk>tjevH0_YnZt2up;L7GmmLgpLnwRazohW?m|q!9m9hWtM|7S22^bu`Ou8SgJJB z1x&;SBC)+pSNJbbHJ!P_|I?r`($T9d0q{LYX6+Zy73=MVG{e+HOIf1S3r7-_ZKH1g zt9Yntg}}EllcxqWzNAJ+7<@~WIX943wJj%oid7IuQ*k&D1Ob}-=?z_yqJ%&)OG&(~ zYFYz{Rk&vBm#3dgI8tmQC|s)0)vrhlT$F%QIUTG@RPfk{8YqU+eR`FftVbkDoBD8L zMQckr8tIqsmxmmrumztmvmI1TL#hd@Is8JK)=&_u!)Iy2%|;lp%5rraHe$0!`T?k( zK(BL?S*|~Ii%JbafU${?x`hGTx~dM+fV0)%*x}8OKwyh?ofnrULHhhUHPF4Xi z^eyCuE7f8h9bF=-ayHucLYQ{pEs<@5su+r4PMF>30x2qp;j)R$KZKh~A_bbrv1mWH z)#FxO*$N5X==-(SSvIOD5?qrQ;peLa$TnTB7^sIx88t(hTp+mM-hdk zqkKoM4vI<(CRWK}d>>M`DI-%DEx1`CPT&InCsiwpSBdsJgDBc{01gdj4Koy6g= zXa1}jafBBz$t(?IpJit z3YAgBC{%`?(hv~pLH#Y`qcX@*x%C3nl# z!GdtRgj^vqF=HDI4UOi@l&d&3pKgd@TZl;I+?>TRka7wR+hzC(gvl^r-{IK4lSIWbp~$edwg%Rvqj>Cro*jkmV5x-cp#hfL>CGO6m;MEVS_>T;B+ z1Q4zVGNQ2Rb}B&z3t@NfA~C3WS6G$CPpp{ds5B?HTLyPmj(P2&%e?&bLx@K&Cr(e} z?iatwU;lew<)=RRHGK9rzJWJy+{fKX%%h`v^*;Ho_dbh{Tn*$T&)4F9-6Pk59EBxW zu>*68xYbC@6-Yt~apiA%y*h?x05MI~D9__sg~zV!`0|Z=_4%*g#0Q^!qD~fazh6rY zG|IWu{x-3IRf$*vSZmk5ih=pxiDI6(u{(pjy6sRJgdNGoK_UvpBL*6ll^VFQhGjDdhnh1>@OI=`$wHUL<0=GMk86M#?~ zY3DEk=rTa>v2{b*nhn2{&#_3EiqM~HlUC)$CJW1FN|ZsQuN~+{*K~fX>mFPFYqdh@ zpIwIXGqg7i3C+kYltUrq2i!P%x{Ww1W;B~E0rx5HJ!?$2@&7_1^nRG zie_1=fF)uhb{gl}U`dH-5QnA=tApvp%E zZRn5m;RB;2wzNXv2V47rA13TpkP@v-9Fm{z)fc>WUS3KryCneS3HPDiirL7~V01Jn zq=RLdWig2$wFA@+%NCosm!$km{E@D}p|>4f?N6aR%lr?tzbfG>rKtw(|L$bf;6!Yn zG82Isw@z^S=K?*_p)SCU=>^z-uH?+QOmA|e`;=vf;e?sN>f%u^kbXb#;XYyi0%sgfqM~Z+7gC$YVtIXQ4q!L>JUrXF z5HPE^5Zq$xq%Qi|a0Bwf(?jgymM*pD0Jb_hQ{+?8F|V+ZAca84fY;YZv}_C9&^nUC z&zC0Mw1$iJ0w+9L#YiDn6smEtuBYi-ufYkK!i*@tK3d)khc+@1Nlu3Mz)(>snsv=u zfRNxhk=cblkVD2+x^5GJ;=8HaZ)@wWRH6?_hpA#ACuLxkg>KW2A-SV-U#d(7wu4py zgpOYnBBQKZ31rQPqKzy?@vD!z>L6+Y z!oCFJE0yQ4#+0Pnpjn`x0x^*1=QY>c{P27J0Pfb?{8NAVsd0qJ55ME1m~%y!Fu}kW z#gsQ&xx%JeTe9=ye zE^?NCQu<2t;o?Bu?lMfww2M->1IsNsMb*+r05u~s+{o|H=rf*{yTZMW)eLH74s#k? zrdi!ZPwB`qgNw*=xu8ce=GO6!R9kO$6i>I`CctH)GPpYVhy*hR5u49W)4QdX?nGgc z0ivuGPT#40TKMWGOtrJyMqc{PMCNy%2cO4R$unH^njsnGf0yPt< z!6}RpSQcVA8N70GgyrP8EGFU9!fBkN~_{(3!ul~$u;%7eg1$^c82e_ZW z?r7lpLnpZQ@Wb`c@lr=i#8WkLR&l?dT&JYBnFM#KLOM!ECaqCmFjGynSPHRLPJn34 zS7vSktVwXxJREJPu>_xd=yDZ<`?*CVe?GuinbQPy5S2@w?2h@HuYHOm$RGV9U#V}r z`kT0M=ZicZkDt;i2~spJMcKa;-<6~Sku0j@6`}?Gai^1f6oLVJNMdJi0e?8U9DUmxnUzXm+c%I z?Rz9@5o%d&b+=KJt5v{3i9qyX_wTb6-Rp*p*ib|Aa||n+)qb(M2fy6b%dH4HD=;cI z)j%N*s)Lrpvo^0V_#Fv9q~dG<+tfLp)Yy8zLR1Lb@k`cp75-}Ia?l$v!EO+2rp{7R&FKhuos-$fEYg*8uLcSa2AP4u7BxGD+ z21LERTjf`7!FnOIq{IEWT|Lz|yK>rk@T5OuiV#uy;!TDFFl}N{y%xI8!5&MPGWc*F zq(|3`UTvFf>ydZh(sfK>9b9zLUvv-THg7YsT@8RhY%fG zPW^<-B3>6d_}j?1pTlA6>h#)1X>{8FaDkokb!;11K?(ld=0ND)lr(ccNaTf0S5^UG@3+G48+MBKQ26TQk^chl34qDJR&XlzoT^x{J^24}B$C4OZ45)@RzZP6L+ z08p=!|G`**S{&ibTmgzET69*JD@trkV?zyc}? zl#0(PyG3PXh^5#LXfs-J8!G4LTCSVSpjsvw$84B8ZxN^h;>~HJmp6}@CR_~&KvmC|Mzb#?gl^b zjt|AUKdq$nt3w_coLPtkShE6&Z8WCOYg~}U)Nryiju-4J@$om_4paa@)b6Or4 zh{{@_693}0tOn>})~qE<@^5O&MKUU~$XLPJ)&2Wt{J{Gk$E{nZAgJPN0jbfDMUc~> zUsZ&WjFjC6gj+cfV(Ss^0FOO(84-vnB+cVv=38%{ z;r_jeacN-Q-&c)pFi%a{9gsc41=G{W6}jb-UjOnKX;HJ-gw5rrB5BYpgEbC0R%k^G z_rW@&bC+u(JboeF8>xC(dDLG4w)Jq#)kSse_F3+2l~tiZ?L?oRTOY2IE^9Zy>T{7r z*$=wa_E3p%HKkJ=+i5|8PW+LsPwIn;V9eB#v;l9mik;;q8bICUnX;UqlH8e5sDZ&r z#}81-16#w8+uIrPbK{gMpHcFbE&0*30o5R7=G2M^p$lecG)1@{x;(o|y|zlnV=JO7 zJ<)6vVldg{cA{7`x=npSXtgRAXM$79sqBqXh>DS{8i=-MMX(3uLAnPFfo0XJ3Wc=t zL0T|kKxPE76l!X&=bB*5EpTR6L?^9uE@&DebwB9 z$cb6RI#)&2=}_C0mJu%4dt z?2UW;#wWjmFMj3~{Q9qd9bbLME&Jp z5FYW&LezqY#Gc5gz{$~Ne(AN(;O1H4Z~d{a*6VlQ;7d3Dd0jestdL0#GYOB(>JTPjH1Q0=2q2b zD^;w54r;gvaY6OL#XZJtw!O`#)@pLsbZn-%UQU? zxo$Y+BLL&T1{#394GLnOtR;ahc` zPA(2N4_BxiD9glUVVfY(ee^W)%qW?rHUH3{4y}V|MSv@E#U?}1rV@!V8p47OcW zpH2^0?vU`Bu5#k!YH_s`*_$KGQu*f#v^2cb_m~3sb}>M9zOK6Cwz<-awB;;YH9aYr z=@jFm;l!J$6WG`^u{ys+_D?ihF1DWf1Ls`wLh2m}wOQy$PbX)tULi~*$V4Q)PUzH9 z`M#1ym&JT-0?OS*(@Zi@VzzYl!4sKHaA-FvlXv)H8=lD9p+>sW9pYK;SpT%Mff)1y5YX!9r`Z>xBu?`)%^7F2pq z2e*wmrIRx-s4T=7YV}X`z@^i12L>tdwdN{{YAERHQk4!0oi^k^s0mgqoLNeD#|3jx z7FPZV>-oBO4{q&9S^_)_K1g+?!+{e$(=H$IL(_h0@19=^20`=0(#p6}0sDt<6p z=S%5JYHbIN*yQwCV0a=iyf@dXiWoeJ6+ijn>-E3I1G;TMz>1OLr;2`@I%u?W zht&CFN1UCniCd@fk&ir$Pks8;x_RSXJ@WXqSOzg?%^>4w4BonV4^KRKt?m#NmF#3? zX49Mn@w@~vxgsz+P*9a3vN$W%*D`Gm<-XTuf<uYMbk_7^E2pYq8u0?>O6MLXx$pf*~ZyKtkrK+Q+fQ{x(Je$jn(IsKL>x{9uAk zFjZ9Z2GZ9$luJk7Ihd8a&S0eCV6-1KG3DSURS&m*ODPBCE4R&m(&=85Rch^%v4m47 zu;qtx;wxm*YTHTvmz|o~8c1K6Ja`mXLTs`LQ(h;6`_Tf4{Ij)nF9hb{X9GxtAbaWKUj<~#> z)N-^SMlgorb0H7|QqKXk?8ux|2%y%%pxDcdm+A)`MA?RW-(l<#@puqn>^DBlYg%qj)e2H}CE7(pxuB=ULaUpTuL= zk8%Cd3D=o8J)2lV#q|(y*6b7cX79iZ8tUX}-B%>qq~{=MilHp`_OA zj96BkvLPZ0YaxAD$AbeH#1?f~7v(iJ^R7Dd*=Og0i|~rKZ$R>W#SmWyM3_&5Q}bU2 zgPW8@n4|%I6);rD;;?GZ7>6AyR4G*cuX6#w+I|`aq=C^rv^|lav@VcS__5@M2ScmhaUB& znUn6-C%kx!lYIenGEv*j)YjjgAt`#a5k!NTA}ZX=T<{{%%Qpnx*hbZy-7DfnZDU0x zytUce3K!AIjAh-^fjNrE9+?ckkoM#$NV-#j%;}BQ3#{QZTmc8f&|#DRj8rYXva=zF z!YX%Jn1$;ln*=36cI@qMitQ;7W&0aP8)Do12~`7-_9SuO5YN!bpMSr!;E7EigYIwd zjo!UV>EP8t$7ha785nPAzBvJV3OIkt{`>N~_9m=(Ys7AYXclG0Jr>ErmkWY z9p)ry2m9Bxe%lmGC=M-6Z=k<%6d)|_f#R2g;6_fs&#No+CX%$1^+t%!cQ>}sVr^^; zs~(It@AM*VkAz-x1cC$20|W;O$jl3NFGNkNGrVOn5mXz&&>2^S*rX~@inm>7rwxkY zWJ@jqWFR@Jm^EfW)#-vKCF@x%>b~@hJQggoZz6+U?s^>C=z)=i$)ec+A*8a?m6#LA zr8+7KoHZqo4Nwv_RmH(d-JTnl5=u})&Db6P=M)MHrzY^)9^}l-LDe)b=(e3}3aqHD zA&G@rZ&eSIzWa$6a6WGX zSh`a_2e#`DB+`~BI%7)fyGIwQJvM5s8OyTNr6urFUw*Z|{prW~pZ>+~<4eE%Mt$n)azOPI5tw!Q@2Lm?Z~l z?+=EZezU8zF0p5Ew*=45CdNqow(oqVe(hIZsW;!cU5`F;mCIm&#p6qXn{S`hlh0fO zutuwNh>%>FQAv0{y8`0EG!Y&ElcS*ORA`lTf|?Zw0q26|Y$%2ioHZ3M*j06@8fFH9 zA}Xs~T`AQO8heO%O$74d>yih1#t+ zJ$QSB$(0g?p)&MvGL))k((dXkXzqbY#|wPy#_5QBL`BFW*WPWStp->_fLqyyNII7) z%+O;jZTFHDlns=tKg1yH{_biGRWsadrF{yO=;h(uMWGIaNk+w57Hu=OLg$9=BUomM zXMu~8V>y~Rvr+}CZA*0GgnHk%I-KCW#+q(}P6~s;?1L7Nk4Jce#@KQnAbn;I3#ymgAQ}22N%kBuZUomIm{A|VfdGh|Nx3NE;_~NI(NmMd2k+Ib1=HdkvLz40_ zM(5}pX1ZXIyuZJ%b-(AD#JXRRb7H?wu9Y=s5tWrzN+uT^pDa8%Il{F^AF9Wnd7PK7 zExfcFz+j!9POi1`{C@K6Y+{ml{pJ~tc1yi^^Ne48;~swf>$hlQ)PMk` z8(~tfbfQ>(2zv$zsubA$iDb8e4?FZYe0l@QxdK2a1L@Y0PF2MPCaOY|Eu{Y<$iZD! z<^34IOmvCRg+V1j=GIK?G=9QH{0T^N`(Jf!DJ1dG04V%pKwOzN{tg#7^i5Hlib>bq zW8S+vNLiy{1%aAqx;&#cyjI2uOpoF=L9As#=R8C~%4Yx@$ef;NwOJ&4jW`8Rt**+Z zdHTHep&kTsQfEyx>uG!u(FX+@jOlqgh3@+Upx&U^aN4F8hI0~QC1r&o+|=+{CPDN& z4*MI}-nKQegwFHam0d!N5Jdz;q@fJ zg5SPzE+BlOx0JApUw;r5`XvL0oAAHr7D5bNvB7JZ`Pigqt`KehT*|m6u?8+wn_Ug( z^n&stwmfE*PMK)gRQJAbFYy9c)o#(+B{0k^HOBEfM_CSzO*K$D49${tJFK2_HfR!{ z4zn2On*u5|t2TU(iNy?+ozcB0BMMNKH5&eI_cP2bC{o83;h7s3B+^EW z!oEM+v?L6il%QQC6LcG9{i0T)FU zLXV4shIJYh=)R=h)lxlZeru)puS$~?)xp7j0>0Kh(WLBYHH1l;ltfhDGhNNpnyOTR z64`st*Z}Z>ktpWObw#OjeNN_PNB5wg|EJPC!c)kYV3BzE(TtE?LMD-_ZhBSIl`UW zd&EvL01=o|E}5GvRFhq7VB2W`5Edoa^dQDEn&tI*goOK%__w^4W*#=xB%2`>SFes>T9V zhWfOrre4gMHAXK83 z_24%>r>u#Qk-X3>la;Fc-1~aw{DJC+bxD+^22Bj5Ml_~3UvSD*crukcrY;j1{gy71ZOAE{-D+F@5?mvMY$Cy|@gE&)`sFlT8j zd1cL+oU@p-P*p(|rYT7-&Nh#LaeRa&lo;0`4*m*ZJ6lJ60q-0e5kk5kv9uhU| z#8GY`FuKd2Oe?A)3%(A0Rb=z6hDVAy(LJL@k1SQzrC_Ly>t^kuxNfuN4b+n9R+@0ak*VaB~s8l;e_Hte2;#*0gMa63hh^tKYS__(1sQftHuY zh0rk!{JO9gKB!u;JXy{;MAjsGwg!vPu>DWqBs&;12-^GwuWeCM{#$W(df=PcD&5O&?$+n><%8FT^Fw1 z{#p<|;bv?vpx-Ui{olXE^PoSAL+lcpR}smxwP&!~xjTnYx*+b~<+uqcBkg5{#>F%lav)(Auo(m&0CYf$zep5^KFng27mjYAOi1uT z>Zl5}D^nY@!iEW6iCcklTK2BFTsj?1LbG!s^gLkSuCE1ImP{9z?&qcfXt}?Fp+qwZ zOBL4C0~MQtgLzT8BJP}j>jNnUT1uXONCIErF4Nq}wgJ`hB2-Hzuthz<{1C*@_dGXmZ;@w+p1# zSy$J!HiyBpB0-y-1)sJccuRxJ&BV|C z(oMYd^;G(a6_Ge}9^(aZZs}gkSDGI=D#Qg_*EW4;@o;=}OH&2NXSqO5r_Y7dMNhn0HVr5}b zGuc!=T@KOaB|VGThkB~diDL<#-Crx}l;8R7PuFLTUW>1N<8D3m_!aJsiQVynSHAI} zKJbAP++EIqjMk0YgF1-nlAU=bB{>ZCzEE(e){~UY*qkj_r)Nysr$OnksvatAsgl^s zK_^U+g~5rcK~>h3XM`7&&GsZCqf-Q(3)681>1~|aKz$w_1+G1G319g28+`uV57lFj zUqP;k`wv#;KEaT)NXh0RV>l%)hImY|Evbj4FNw@d*hRmKoGNswSqTIg-9N9iqFS@m zaHy*TQG`z$7BxDF$qN4!Y&RAYoGdq<(imath|N(&gq#GS;ij=g2gS+3MYXhv08-k4 zNid2siegnJCj(g04q|LyD^>QTfi@SaI3^wwME9gss^{1wW&r9lMo&8H^)l)}m-oD# zK6_;0&ska`$!3mJa`l84-R>+-?F*6VO*99pQk;#5W0zP4v!>o32BYu7LH`~F8C%#Zx{-V;Cb4?l-5fBscI`uLT4^zmyk2C?poZH^^TmHM`qMP{Ag zLa$-iYC;bpMLLz#8;yHRvyk5Hm6=tgmZ^C@v(6J$70s`61!Dn#khesUvkH|NHAlrP zKBzP2f1{3e;PtCJJn_(RefV1*<}%3B2YY+d){UlvOXHB3Yp#VN`$@)K4f)$dVt1drgWir`c|!Ff_-?Gg9HS2+-01U&e%$w2AZbRb08 zK$XtThO%WnzLxJDFj`cgYPRyZua;Bi-~FjEp~A3P3uSGbwW0&L6Zx-uxN7Y_lVyD z8bgU-R-cFHj-@s>v@QvoI>W#&Y!adBhVIACLPk`67S)TyER~;$iCm!}o4VzMs{L+$ z^6Tbo7XFtT)a)ON=klSnrfR0h3t~>KC ze_xs^Rc~!#CPgA`#mQCM2PfYz)<&-vA%SBwlvgHgr7iez(ukxV5j zAP-%kbdWE#B4mpNh|bS#6XRYwm@N_j$pXTC%q1d>rm98E14%Zs0n-5ewV(9=% zD$~gbRX}Q2bG|*UUwS@2^ld+oKlcCr*O$Nl8-D{Ie(Xs+bm^HoThE{(>4*w^d=;VbyXfBH&&`75`u->>=j z!^fArPnq;Eg+Hc;d+`xOU~J-n@B-9I}MSfz6TFLRqaEhM5)f$QrWD0IA$bATJk5q2c0rqD)jMT zIA0~*?v6@+R4%h7YlN1vPf^h}H0)FtT){?igTrK&$J_1IHa@zC{4S(UtZdd}Q;Gk>~**3$%q@-^OE3IL6oaE&M@i@^&cqAcmnW8t>=!XqP#MGa9@B2pzqk}$XyTZITfmek1QwP-?n#26)y7+KHunNf~R zBiaOX)NC6py0;!l58Bo!$FjO5kXeB+N=a9D!qR=+U8m)0f*PViqg>xGI1qEbIZ%Lu z6SD$RNCb=%!cR)B&p!9#T+ZcBq3d@0?f{NO06%*0vEPGL>P|)J7!KWc+{Dc>bvf$ zH^HyL3IP%eQ8Rh(6nXD#-H5BI#Fa}&_1l<%=f+r^@tT`%K_S|wYj3yKnm&!+~ zuw2-ClEtcsbk1Utd8jB5M65)HLb;mt3jk+oUZa;A!JG*Vg%nWJX=a_hS%8BSZ^KMs zuLHPKN8Fjx7)I6B94k(Gqyn&$w=K8j96F3Pxaw4oYNOK8-gRP=(K=$X0z0uF@&T&%c<9kmJxysjM5(OyW_7~#CXsyuF(PAV=`&)Y}A`Myt) z%^xBAKAoQ<6Bx=1J8y4)04G&)XgI{^-;|Kn5MiN|)dk_y`*n-V zh86V+VpDK5F(6!Im|%=m2=pk54Se}mG=+c%#`!|Aww&J;MC$4D3k#d!u}nHQ_aNu+ ztPAU8d({HThf<8IOal|yUgTOX7jAMhgsYC;X4z*yn68jIv8#&})B~7nb-pvX?IzV$ zCm_{FB@6Ncm;e{QJeTz=d!D?$S6q67xC_^+_yAfWuVkv9K06PDCn_UA*FP09Q7JWe z>o!;$P}I!{ux_H;@}e<_h5gv<++uR%!h2{X#Tcqmr=A~BcK=s(Tz;x$xlI{Nm8>1g z?|7z^gEd)@&N8V0`xOeMIi^6q4cC{$+8-0 z=`&0?qf??@7fno7i&&~8PhzunW}NLhwQZ;-3lb3QvqYs;1m)Epi-{yXw+lRmqfe6) z0vd?Y^9m*BJwg$E>tVGDnrMRC8|t?xX3!*aKbuMnNz*Hml{TvqPcxCFru1NrG$F#Y zgEp&=TGf#ly^K)uN2o3KMg~@)dU^jRK((?W0+SVkZkBheonm2v1CdkJg3y7^W;Br% zG_C}c7R42fr$A z%8-fRV3lQR{cIx89C~GBvCWaKY7DUU?&k`i5OaMqAAaclc>jC8hkx~N|Iqk}&;Pyr z=o8P!)k}}@Y<-}lh9jmK<4xLCq}^I1i3VVsbaYA+Nlp-%y^F^myBb8w(a4R~igj)7H~_AFWVw2<9q1MarC-&s)K7l( zA!D)-6BUS2B6Yl*fGNf*R8@$iNa+6DB!Cc-GcV2v(cXX6zW}=fstqJy3SVA_PKIff5y|%7(qz zl1&)sgg05+nV4ge55hz-#2CTd;VTM@3!!O_j$H)^rPqUg*;$%ls5j%aKd6LhV1K&j z)6ZPPoNK-M%3a*MpEx%7Fn6D`--BbJYgiYR01SdPky; z6+UUL3Mv%sr=eC{nzkQAtu7mF8B^HgO5;s!aLJlv441#D0`e$#)k|_JjxseQ7gd}? zGmX|t43NRJN!s?HmGqP8ifd4*BTZ6L?3Y12M4qy`1KOe3e%le041Fj~uTuW)2@qu` zEuSXl-pQ=#SPmj4cE<|<)LSxq@a%gY;UD^A-=ClU_*dgsf8iy(`Q|-5^~`l1 z?*>xrRTq2%1QrdbqEvJ`C>U-OUdnw+1uVdDYZxmW@e>@Oq45hz0S2OG%~JDeDiN3c z(y&HN_RR`!dSo@$WQ~ENN>=Qc0~+WUf%6so`i(o_jl$)V<9Ppb58-=1@Dy&{Im=)A z+#B&zzxD=x>5FgTwD$Sf@uhhFkt=cS$_|%y#O(>J6N>qqwJO6l1)2Z{D_2yFf;Bn- ztOk-HSq0-tT*mo&hF|~YXX^e0|Fz%w7y0|X@87Q9_{uNi=DnAA6c44uayo#M#Bi3X zP^9FbVDN5WCE_>L;-g%e?%9%XSuS~CjH*FKwve9fgG-4PW@%C>?Pw!%uevK@O?Bsx zkUuO$-_(Vat7LAfDzuU~a@kanC|U4nP%h9+ud37C^^_5v$tvHGd9l%{VnB0mVum=2 zxo8|x%%~cLl3t_65@X?5lgYbx-{#x*-umVTU--~}ce>xJXauT?Fwji3%(knmw~Zg{ z<{ttop(!0+-gKc#SSPwABb;>>F>@l8WiC77=xAVF&!0QHfA9N}`~c3zy9dWtiTVFq zF6}-YV|Vu8^i=9po9%5#LCmc{C=xOQu2^aP$~VBa#6(qwJYem(wL;Z59ozd5#UrlQ_<+izz_jMtGfYB zDJwLDafjmwxYj_nzaQveV1x+TXP!tgWvsw==qABo0nR0xAc6{y_$`Q&CIZGg9cuV z{sI3D#-=V51rszAM{=!b5WN>w(XZU3}T*svNKS5eh;IeH|sr1WNx8ft9U zJ2rO5^dvShVL!CeV(lsg50F~lPcP67Od4Q^79{<1aHX@sy4Wj4CsoS0-FpKqc+Bi?LBKDTMf}n zVRTJ>faoMb&p_{i+ky=fK;ky(;2zt3u%CmXhO@`9g&q{`@!-Xs)O{KzG76kS#2la` zDd>2!Agm|>5ATyW+@J8h1GndWbd_>vik-SolL(Vz)S)tVIxu#NA4 z0khD!NshqD(GfoWiGT9O@BGd`@aTiH2Xn1;C-P->snJIpHWcTYVaZA+idoT#oF=0b zRbO|5jC9LNgJC6JW->z4tuE{Yc~z9f#nA`1`(UfJt;j@1RFrF)+W#!Ib&ye+))aDr zelNXc$TTB&kqA+g6-%F6T3l+H1VM}hJvu?fgxC&VM*=$jKv}pdvnCRnvMm-CP^pts zY?Zq-Zc0ih&d3@CV1IrePd)Ym-t*K)^FRDM-xI&|(kJj;&%CEj#$}x5IY*9el!CTF zQeNuSeG&kxqEI;yF*}+T8Kc1D+-F@q9=yNb<7Z!f1AqAsetZ4MANv4)`4?Z|-CJk* zk3RNFJap{{m#-{YnNe9e2scJoM-#2{wj^ zvp7a*kfaXN3eUumeF6Z6P`m)VY*7Q`nyeA{+ShN_5BfA|u= z<0DVt?)@{alde8H{%gEl!H-z&0ct2AQruaU%-u2p4o3nX=30!*)=)6ypT3N%4_~gFiPN(^N|VXdvJ?c>$RnkW!ejy?K+(SktuD81w=rVSSo>Dv z2=8d;cm*HMO(z5Cbz5zAI`sWL{?pR&&R|ptq|-;$umRZB^1>Ukf?l7gDqRUti?gUs zJ_A@PP-G?5H!lv@!)pl*RcUfJd_uUa=_H2XGal#(J+UQzMap!Ql-=a!4(AspNbcuS zUL;oI9g_DdHdd3FI^as9c`Q{?S|jK(;RfJ9`3+1LVf0Fk*yv@XYtIBSYLCgQR|Xz` z_F>$+d!C>C`LD$1KKV^tzjlm=pSWDH49@-9MWL$DovqojuiA13MQ>$i1y_%>G8|s1 z7BQEwkw7{d43}>@q0?Cg6;RnVLvC%YBF%NYLcF-LCkJd5^9%dTm=iT3_{jAmJpRxz zu3b3-X4OCY!VUh^Ctj=1e(g5je6YvO)3u&|_%h!6#5LZyy`RgGL}zO0(O#S&B)Phf zVG&dh&8~LKj^M;M-~ML4a_?5W`=Mv?r+?^Y@bL9#>oYI@W2||fyYYzg+Rl=wXXHXi z1X(e&YDf*$8FE=&7(Of6&SRqm zI(kv|@Kc8B1ZPz5-dU_2Jco!>5pDxca5*2+6kv4T>a-|OEafK&>u|I%;!v=eksMU3 z5He9myW<)o=B?W|mQ&n{e2{Ox`&-`o-r%@h)AKufGK!V7>}{s~qx=%eA$o6j-rbO( zAOe6@j^T~96e3CD`a>r;Kixld^X<3(%$+-T{#Qri4vA9)0LfuCvZ^zekJ(sEM_v@Jty80g!7vL3)p4qDQZ12W~&$%!C^dE|zkfqpwg9 z)O}XP;h#xVt=nr)MJ9c6rI)F^aJo2}?HvHv6t#;zT0b)VHU8YHT4}((g%S|baYanb zM7OF)9bkX9uEb};-UE#yp|cG{>*L$Y0X+QkAc&=At|l;a zM!RJKTV4-o+x+)L^c*`L?u8q15~y1B{Z;_AF9(0ucl~ZBd9D;xAEf@S&6unX{T!Pk zqZxm8(5!9}Ms8&!2+$xiC<-LroA2UFsRfBSfaN$zGN|a_jU$`!M9VLeQx|BMJFv_7Wz3m-=zh!+z1w8Ws z@G2fw3sm~~`BAD*NvlE3pPt3>@iBh+SAX*DANcMce)z%Jo%76f0ha(~i*VR#Be*|T zXSd$FG?dB_LCOyuywJSt13eE1B!fsfiKr3)i_}PaejN6>IdnBF8-z8>M zR!8UW0+_MLo4Zn&7i!WaT+J+|NV!BYG#*dV`!cZDloZY^WNIb9HT4d&W7(Fvpad5c zIYXPL^b^-`=k7To`nybwkq3LIstVN^o#*<@P;f`nxuvkL#_(p`D^=w?9FyHTimIx? z(W5X0M>hG}*ph-lcZ_ReB3U(Z8(ql)YPhyqolMA!lx+~}uH&jIUcS0u?h7ye=9_hV zvf~)6{rSrMzHoBo7*9NPxvoBZL?CMaV9)dO((>vcB47!XI%swcXq|}wuac)x1BIN0 z9GWzw%05d&l%RrOkEqjGBz{S_j)Sc3^02f?d7okXFtR9P<3L2onNhFEHLD3y40A&> zk=g82+02FiqSBp;YPS$?ZFo=5q;$ogygsUutl@+|OWsR9q#}y;VPofJ&gjCdo69wB z9n&f9Q&Ab56P_MlS}H6fs>*|hlnpDX%w%#%2ZC&VvPZW1nF)o5bD~*u%9$Um2_OQ& zsTmjSzGin>SJsNk;FX7t>(M8!;_Vyv@XvnwOL*n$Z}ZBP%XR&+OBhRVp072nG-$v? zB5SJIV~lViv1}lA@VEAKa8W@)PgE?Q;|lk#V^o#6?ercEz_KLDc0Lm|77*)&0+?-Z zYA_?+5=pcwu4ZAT+B0?mJaYX6?|SkQu3Xw7XW^x9-pAK&oZ`dpe`Nmgzx;7N;1W+< zIzr_%ggTtl%9dMkrq#b2furRTZlAqYFWz_s0}KDvAN-H-FMRY*)QfL?ny*iWQBvHiAaZ5s@V=zO6-jr88U#PqPq z;o~qsDRTUR2okf%Ax=;lBC1D#hvd|hvzNWfDqC~3J}497h{PmD9xh-oDg(?Z?th`H z$DM5SEU6Le63Y-vKe#&{xt`t68#iCuWzO~4cRv58E}vZbo0a(hR%cWzdXWGZ#D(=| zlFg9ERbIeAKmEv^;S(6c2Sm}PdccUl?r6cx`3GKm_2vHV)`J_D?moDA_UIE2e-z{w)^!zlR>jAynNU=rK=KNLHjbNFuxY3X zj43URmCuzjld?zyu;IH=f&Y`pX<`Ir#kH`Y@@f$ZuzVey<}V4DjA)!Q{l9K?v2KFkP*O(}nU7E=;syxElAf%g!b}{BK@x`o~_cmiEJ=`f3GgNKcggYXml> zYx%mJf$FR%V^gp8CY7Q6)#Hq`sfaeu>5l{49W-kzf+oE0!X!ffT#9&N5=+Ysw2yp4 z&W+W#po6omj2K$HwyDPSzh|l6G9wJUbzYgZ*8}%KtmkJt1q0aP^cmCOEe>D`S_t}w zI)Fi)n+CdiKsv$GPgaq>;c@$oZQldEq(&6hFf|pS2?h)=QXfVeP;HRs^Pk>4n*yK? ze+OXGMv_3y?(u|6wrDfO(NWU(UQX(|wh89}2n*&Mq%D2g)C($LB)X3$mFS4s6*y~N z$B$~F=1`6eD=GT@q!YAkAdT&r)5&QU>8ms@4gCsRpE7D&Por@+c!3m>q-T^4ay>I* zMq(5(oj^zD`tFxEyRlWY=pPa>70~=l9S>+sSXvv3Gk%x;op8$pn*D}*YaE<2w?oQR zpTdGJ@A&&mQl9XhS7tc9{Rf6WzY}z-a=7_jC}DM$VqBnVY*zTF75=V?aHW-S591<_cxx$*KXe6ho5;rcFfx6 z9=fJLa@CyUp@GOsZ9uB2&`i5d){0`S3@$o8<^)a_VmFANc=46`pS=HF{40O{JL(sH z<{NnIj`yp^8MZGSYnxPO@lD z#icP)Bbx$+oGgPd_Sqp%8v-r)2HbrS6)aXtlTk|)Q>WpwBj3KY;^9Y+^5i7qwHt|2 zQ#B$&vnssN)p8(R9z{~QD>30#)^G^XM#~_WF+k1jZi@(qI-OrMH#C@$yz}-cmfgU+ z-url+93A1UH}CPG>zA<`%zaLs&k|D#;ukAjdH40{yo+J;Src)M)g1n-%M50db&g8= zl!Y*)B{|ss8KsGOu-~6UMFoeXz0TFtb6zqUQ-sptS&?>fUKLSObg;(0IBHQR-o1M( z9=?8zXWn^@|9`IjH0ahey9xu>+RuB2Z@yFByg5~7R;o%hmj-Cmlz@@|Biq~K z4tL{dV~D1^jl&QffdjjPa5U{U4GqQ?m`NBASf+&rBSEP&7L=+=NTrfWs;r#Hd-KlU zc+PpBz5B=7`#o2kDoI)Qe&0Fo@I3q3Ywfkye&f~SI69hhxtzsp0d6*9y!P_FSnka5 z*!3MAUE9U-z_2+Vaj|kzn>b)$w5GA&^!z@FBqCTX11uEjFwI^zzP+AZV?XbZ=svnF=88jxOz3odXfglN?vGv`0h87%_=`vl-DG zXU8j?9dCGaWf$N1?|mHayn5Te{Bv*F+ppcl&i)ctuI*qhCs=IO=^$o73`V+#kFaQj zWi`txraBEItOm+|o2@tdCi6#s+Y_5HCN27c+qe}Z*iW0+rxfd&s(SY)2YR%rqAai&Da z)C$K;gHsz-xosdwmJJ%Sc&IvU>gG~qliw6oU=kMvFe>-*7PK4_$HIk1heSd)C6^?P zIp8;vj)%-}!YMVP@ME$sscu6`?9&`iFfNG?r9LIHgZBud2U;0$t5}dmX@E1>21DZv zo6RPc%iSPow{E@MF2)PH@z_)UuWMH?|EB@NdR$3f?G9Q)Z~)lV!C2MF$}~WB~pgu(LS4pt2PD29_KF0)ugY zt$XjQ(=#0HUq9d9**SBT$lA=U0}ZWpxM%(S5v*5vxK?L_B}N=l zF@-FoR(>O>XsEL`;A!1sHJ-D;!6O(a@j+FN9TKtCh;oDytCSK(Cpbt&kYNWX&y7U< zfpTBUKS+W@9sp8Rmx9GCNfT3HGZ_HIeJj#n``p8jZDZ1@Aw|i|3JsC%8PKy)dRaG* zA_9Pdr6?wa*jB?5Y(=>jdPx^Cu_dA0Y+@(UZ)S2;M;JQmg?1DfUD3hwck=LtQhlZT zdNwW$mA(8-q=9r_bQFuC-;HFBN{tx#f*pX&^#%J`KyjKN>?zr{WQ{o8yPJ&a;GXjO z1m~#U-po8I;|@ZR7$Qz0Q092dzgGa%IGjslQsFiiT7u33jWSA5Wv}JN zkvUwlkyC6+Q$N#ynOu8X7ty94)R{2dl@7mT%~T83qR-jVs>C+Hy@NTjP|iq5wWHo? zH=y;b^=s&}7OOLvJfGfOzC}#7SOIXFgPLcS2h=aPB`gxb2&|HD$Obq9zm7Noa$}{L zM*e3ad+I~RB$f<@?sZJ*nR~sr+EZO)CYXeSE42rye!ndMW8Rmk*GO0k1XY)DY>@SV z>M8Z^iUr=XkX!vkDmp38RaSvIH!}`Sz5iPHW@$o$h~n5Isig3%CK-AH4A?a<+^A?0YW7-HSCpdE;X= zTm-zsVwUXZWd8z&THkA5HD@x-Q71ujgcwz|oMLW)W-b2t%QyMS$1d@Q{_3~m#V_6C z(PD_3caGyTU$~Ety?+lLjUHq2t}S|u%(|)oeQ2*E05&hSS%IzU<1L#yd~aeY!M_H$sP!%W4YpW(7RqfyE__BPtNhd=Wdwyz&Lv56k-VT zEoUiJ|B<{;Tg}o{A#xWbvG!PHSP#Qlds!?{AYpadCcz_{r=zJC^%uPtLf z7|)JZ*x8+%#Sp;=n5z|*2^1$I7gY)g0>aoV+|Q;%z}i%mM=@@CwE9setWo{SEO{b4 zoW_>b<;WMq=z0pyBrNmT(O#p4W_DYopcg5xWLDX;(=xY}X|!v=-4HRx>DdOCFYn{{ z!G z1-b`%cMwhO#d+@5gdP&9kLQ;4oHndz=U$=5AVC2l=wZU;$#K>}Hyv&&$~);mw*cAz zs#z$rF}4VgbR`n8Lu&xtqqrzk# zHIs(sDg-^O*S*)?cCZT`M6;xxd%#UNJ_5M^-YL#chq&?VRX+W}$MMeBZ{sUpd>wDU zexLKj5C=!Qn9a$w%n8flou(d2^QCQDu{lIk!-(kKj2y*&RZ%EfieQp6$_zoqvLcKo zo=mPe%;HYwE2RU`67cL#38ytvdk@m2%U}($-x_-}cXvBoIll4PBm4PZeTYB&cmB0M zzq89_Yq9DZQ-0@iuqsk4lfGseethrV8+_&TKA+y*i$DFp`yqVn>2LF2e)WsIb?@bv z54)V(ZW6j9K!itA#uN!IGneT~Z9-9A=ffn?({fU4G6w`aTKcu1OuNEsgOISuTE6Z* zkFZcG=MsL3XX%JZc9<=@?olN`PQC3is7;bSL`iu1@+Gb}WzJI%n|?S%sxcb|w?=O| zuCv0-pfy26%bYZ=SwiE4haejbK6;F?1_{4+|E+d>`p_S{e*G`L@c0Y=Y40O8aRDC( z6E9m^In?HrMLJe8Kb0#Y^RYy_)mmzn*9uIwh7Q=Ema5ru4g;Qh_3JPF+{xMMu_qsU zYO}n4d5zT?t+REEjh5XvL27td*V;F>fyUF*Q!W?t@v(!e?dBV|zx34mAA6x$pv+07 zHVKq4J2T#l4v7epiz~r@fzV7fEBiv3JZO(NoJl75RtJmhWwS1R-Zc6R9?ZgRlgc0< zg+vs zpB0pWl22zzfnzTnD-!HMlMgJ#TS?-i4l)9hduIW0Om9)2<4HV3k_EXNP;)$cA(cP^ z?ed`!(2M9WZ`(CU!n*=FJT$)*d9uML@Ly{z^?6WjI}hqiQZ1&|#47V4@;M{P5*cq{ zOSpwijHOld%|eY-CPa6)?x9JTs2R|{<;sQZlnm9Wj9?Tyorg@Ry-cIknt7K}q3+ED z`_~?kNwsQ>v%G13ti>4TGx>z0Hlh`8JRT&lm z5nUOi0&O^YI#5@qR#UH{fP6+uua%r#40<+d%~HZ95++kTt1@bYv5Qs_jq4FhgPpOj zf#3My-^H5`-;6);SD)k6!T2pteH6}3cvs_7&e$ckF*LD(ZZ4(WYTNbFB-9a1VjO@5 z%q;L@ue`%gJaz?N`uo2lUi`{?yuJ2#>al%(@Atoi>qiD#0DZH9(=ae%R0jc&9&}nQ z7LjbLM>Tc?#g-?z*0%-J)M0AhdG5d@Pm1J{R|vvYfn_?-GJ?$z>g|3+nwE#qlYhK$?JtT3A)qI0zxx!9dym|8;AJ&JtBgj2KZg&tG{%9@_W>S!<0f!-6Ow~W^6 zyQtQ4sl5Wx8PS^RFTpfoO;UFVlz(ceZ&TAP;c(4YPpll#gK~{?*K9sO(g#LUqG;Ks z=9v$~C>qh&*vG(X-ErfIL%e)?3uhM_E*3NN%_yc~2J9>b`l{phJ14mP-U*KOclp@U zdptN;VrQS&Tx_sfZFFi3hMCRLqGMc*@G&q(2ReMDTeF7h#rI5Sb&RE~5^~rR@#~-) zMt0DH&IV20a++H<6!-1#hpFAg!9lA-6)SE5dcfGx(ee;7m&jb#q}Za~j7%O0bkEV3 z=7`>P?(_uUgHBBADe)cvciML3W|5aVmxw0!2>GR16Jfw@+K_6+7z5TUFha&InIuNQ zjH#evTKqfcf!-4AdC<(Dft-%e`4ACuyA3TAgd?r>HbF>6Na&0ObD~I-kvSu=RffTZoYMlU-;ZB{MBE06NAlh>DmGZ zhdXSI2)FWtfNW}^Ml1lXzIBZzjAo&2qIX8h-_U`ygv@hD1tc>=cKD|f4D=qb5@ZNE zgWY6DX#sRbYr*i8hqR2JG_}4&$Jj@Zu(OTh>FaZR!;`!I+yBhZ+Lbog`>!8hT#uQ1 z5sYR5rnO=a-eNHuFvg5ueeV~64e%fS_Wv56{>1-@H}AfU&wcf?jPV@v*?|f?k|24I zCfppAxMkig2C%6@#E{J)rt?VrwDN1pDsf2ggNZ0X`gsYZ(bOiU`WGkABXgl3yZR$! zAtP-7%qhZ?KnzZFBb`Maq>et@(OoL$oNtaI7lIt&Dn7G|)K>ILCv+qfS8O)OASoit zFeWxBLXYWODH(jV28ggs*twlYp1WJm z)kB&SPViJX2~prSrSdW5TUw_c5c}xLvEVr|1bNOU?zNQ5Ba*{3fTKu&DQBKgKFWp) zvBf+S?ou8LuPS{;4$<7D6!xvxT#`@$kuY%NUP(-q;ubckFN%UJx2B2QE?rp^HNGXE z$ow3J{I0FWQwI23zxC6Z^cJ`UVv=p;!$(YKqh40t@sL0kHQpjwivSQQC$B1ey;x#; zAVB_If)XjrvH+Mtv4U>_qeoefn*E4JiG*4kte*`$@_MlC7BW?WL*x3k+`2B%z4}2@ zreZ|%ZK|qMxk>gzOk_qWCs^P)!E~`PQ7;QRFFcTa8p)XDbqo0bseuG01%gVy%wQQF zFqvd;!By@QiBpqYhy?X4;m)>o!%_yrN3Wlh;)CK@y9HW#4$vmad#M!c5ibHHi9iF` z1IdU?iUUqyNP#ixS{<6F?g(_WC^Ra%us4*7${|CP(t}U1N(2WoAyq{`42ZAtWKUyf*hg|WIge6Q|wbIToW&^&C zF2KWj47fBPe&qFAxUswBTR;47_FwtM+xEuI9v}PAKK|kNy<#uDb~~PX?vli-XTos+u9q9I4(jp) zph92Cs5JK)l^UxF#j2e_Nr@aPHRqRxsMX0Go`#q9K)IzT=|7v56 zX2D4QqPCAHDiNXLEiKo^Bh{B8vdtAQ(S$dm!oCuP;D|=U=(*mgdJ346bJ-?hrZT*A zUoJnTI&lznEO%mOe}>IP$15-0h7H8da-nBnl3QCUdB;tU)kR>l9=W$Si^rcj#I>i6 z*cz}pU*r6|M+7-vG&D+rIc_?7@8~PXX52)$vyX<^d=pq)2WT8o&^j7@6>^&F@ZIeLk%bZka7I1+;_1Ot)Y zlu&buoXT~UyVku{3@t~6>Ra`5DDAojph0#B1_uPB>hyG^%6TeeYS3yFF`CI})gypz zZ8pP|$M$h>;|Rxhj^o#U;Z45y<(oJ?-C#L4%$6;N`JgbSBO`14NKT@1)jWq$UA#sM zS2{&GHps*_Kg^hu=Z>YS<+$>3P0O7&ee84ywzIO9h;-gP}3;)>f{t53f z^5vU9$FqxfF>kv7XAmh9Y%XqI`_`d(;{aQNVkW?zs`<AQ$YbT)b3-I zpXQMR7cZL&KM&#L9*pc91DgnIj6Sp>hS@C6&W~re-g}3$Ax@ur;T!+t?%vMd?C#jC zH(P18v;-*oL|C9E443+yjc6QDJ@!_WohV?Md1qoPsnsHEm|;1a;q?6EKYH`cH~uoB z&97g1Dt31cd&H=Yx$d>j4UHH=9pcsUr>-NCt+Hi(uZLl=nEAS&4fh}3KYjd(t52V= z&hIw{#)?wmQfI@r=ZuZ=$IHSFb1xx!;=v?56XQr(p&tMG%<#y;ErFQ2Pg3oT{2rD9 zq3~EwjFsvvsPLx+>6qF#Vgv#78qFOl!sa}NiHPusZIKy~#nX(TUA_WY;7DDZpmJ8} z^m)w@8&ugo$<53;LNr*_(n|%A--axT>Ax|u9XCozR_eVvo%9TPVMyhorCw@z{ehSi zB)xKl{F(49ZRK-S0aTa70Qg&f!>3U*%DsS~%rg=lWT4Iuguq@(4hhD!5j? za?^v^0!B=I9&)3}imXZY5hx--heuElC1+vh;Zbgm0?x@<=&(W3X}Lg9X(VEcJ1a~w zp+lKnCBU525{U$+XHW`+94g{z1f_Q+Fw4Ls_lpeZlSGZBfk;s3N;x&R7f5FW&~mIs zqH*w*?EPcL0z}%(f&2({!%ZVutd%ZNk#jLyKOEgs8q3bu3QQLjH!HpUxeD zI15>FONLT0pk81e5|vNr!2vZA zmjI%v7r1u=(Xg|#z~{gCqc?x^H~*gN7weO;1A|Q;JVl!z2WBEEKU_A;J(;UX)nBvP zyoSdCN6hoCEO8LgoLcSZr}Ytm1p7#cK7|&vH<< zR3=P-xut?KDNnzjElizqX~yJY9pPwdpNdKSnd@GUE3FDO1TB+APqxi#^@PErx~GF~ zFze&WcNd5J*z>=WKl;V*iNE%l|CrAl?Bcm=-+mu$(UB;cn^KA#4U!B$O1( zE?)7)LBVb$Tt^G~-W>KXUb&0Io!z+k&wod}@yqw{rLSJ_Q{T47pZw|f{73)A8}|JB z_A$(c@liD%CfqOEE>3aa3-LM(s>~xHA9r%NP;Dh$|J15~;^-RJ^0j2{`OE^L$y3+G0 z`my9dHaP*XD;aZt6qJ|qxtwv8H1=RLj^;&)#l%QMeWa1~xx|JtPzaLQ*A~@nnwQH8 zy2_q|UH?a7l}6;W4E>0N$SQ7-w37V{Yvd-I5~r(#4#k6}PF^D&&OlacGy z7_(X9)$0d%{Cx*FINZUwah#v5u<3zeXt2S;vj4(6F>W?kUG!L;ZP3?ibOaakh2nQ3 zhDK*IlWNT;@x&uqA{2*$Y!_%r*j6GWgVs!HwN5%CS`a12Rx($>GQn2$O1BCq%7MWq zvY^h6npTR!S%L5tY|$X}8HBY2#ZaxBseD$#k$z8_3=t`?nIvL48K<-XIvX>^Oxh#Q z_NYoE*~86jdJZ}#9dblOL`0Lo$1IXmCq%mhsIi}URtVWkQsN9mWM`S?$a_lVf!y!W zWSlPAhG13s!N7P>-`^yTz{~_=63_=W!}r#iXmqV<}k`H8E$NfV4Z*`x!^W@#-8O zd1?=z{D!OecmB(N?f>evoBY%>S4oj`p~1HbB$jTU0mjgl7C4PB-+YHY$QHs(9vM%4w79^Q2|VEDAgrFReF0Q$#1y~ zWv+vq-Vy-l7G~QvE>PkJX`xWnU&}l%qo}eB9i&W$8>V0-M}flJ^Auu_I}Uw^tV zq-|*Ox+VYD<}xa z2agUfec^n)!swf+pP#A(WG0wen$__3^WjtgqoGXU6}y- zly{He$?ISOz8zjJ69wYZW6t3TT&q=9v|nC_emP4M3Fd)qWxof?4@~WD%D)t-mHi=W z0=l0L$25au0_3`1SAL-gt!qU=>GjNMbQnCWz&j*3)iOym&KZ61WsD2^j0SN&p^}ZI>r=-PU zl?Um8d(qrk4eyzF)?TiMWZIM-F4KZ6EjAKkbhNOkA1K#kseLhxr<$Z3)4gv2F#wvz zRsd;0mcPiPtfTyfMQ8w$q-1Y}Ll#n`$nJ_#(WZBl1O)&e61rt2EAdBu!$gbZbyp)nOEpMfng`mEazK$GrAl(@6Ig&f@nTi{ zYCBHv#1%}64U`O5(#Phi0#%>qhIq9524k25q0u#n62?2CS;W{(64zv4lU=4osO4BIi5cbt2(Xzm(1h19lkQ~L*i$|W`4qTi3e6VkKn<(D{A!;Af+BLlVPP(|FU(4B;oKTn5`YFF z0x3_6$=L${S@QrRA-N$eazwpxuuZ>*OFPfmhoAqH|NS5TWB5m3`s?`c^((lx^Ayfk zt7rqM77wxFX{iUAh4*y7hf#*@9j-booL$g1Y%;}1kc*uGpZWSNEZPij{gdB@TQ45t zb6+{;w|(M(ufBYSANtI@eBa~ym@k_9sQGn4)j$P;l;wxXl&*BEYLr$?NVplW<}t&7v(<)AUf%-- z#CqeLS;ioEcu(Ll=yj>vzfC|}(IBfH(SqrgSw>y8f1(0=sTamTBg-;Nw#k8wzfqH7Kqt-Ay6Y-)BKzo~(v4(y0%5M*5seK!P_}W1tVq;jt|}9m=ta*LYGy9wthiV;)9Mp<^KsOIIf8VH~9rM<={F!qt-2yJKU(Ffx)`cJ^=;ul(XWc>9(2c(EGUY{1dM9OvhV)ylOTgV}B5q#4;Q!kpXcTL)T` z%s&ZhHWi5EqEXF%QwEpmh!tE=erF<8V@q<^^3Yt1dZz)jfw;IZoc9&J?Sq%%`sF?R z)<5&J@zwWE`0dYJXCvdHl5|K(K?ftMBy;08ODuVo1v%M>MrDh>VP>8~c%BUcj6l zIV&mUJwS)mdPso*gWcg^=U|Q6X>NU(HEuR*U(5!-xHw(D^V+Yq#lil+{)P{I%HO69mv;9rZ7yFrS{vGMadB?4?rf6Tq&KMc(R9Aql%2-ZOokM$L?8yR zbptFq;O@3FKf>zb?(b{O{z!EAfE;Q^f{|x?62#R&2~449D`(HRfy5vIPb|sI)3iVm zgQC2{(%m6*a7-Yslsc28ND;I}0%ZOhs(k7n-vVeN^s|=g?QAv9Y6b{<1n<_~hKg@d zpd1Kv8IYrhg(Qmu5PLON2A6_XWVJqR%%gh$ZOZSZh@~)}SK+ks02Nhp_*LcrwrzWFP$xpr++@?VEp)Jr5v8BnGP z(}D6RI57cQtqrBS0z#M^&j$oX_lzEDIi@66h%gBedM!GAUZQf{@*|6{NQKsjdXL#J zI4#1+pv!#uVI|wB=&k2qJ%DBsvxYXTC)_L{W`W*XF3gIOszTe!wF4-F9&McuDbM$DLHtV^u4noXw0qc1OQ`;F;UC+&&L_t;W z>!h1JG#&-*PH-~HNM?(5loF#k*xR&;b<)i;b5(*KnZTE1USkZkz{6ErwpwRB$6{$0 zW+D|}ANAj>sy^-G3Q&R0o}@??@Q4DwTCe#WD;Uw3>LrmP8CoV%O!XSwv(r;mU{jwe z>wQl>%Zj9(%xY^nOGNhl0M$teuTHDL;r@sjbjJHI$E&&$&wV4K**3W0E}(5N;*UK4E}1%Q&m2R4Z!7B zmZQB~a6@TKF!cuJOgz7f>BI5tlON?@{Jww7KKJ^M#Ye9`6IXUG^L%px8wQDyLQW<6 zBA~{9HG>kghzM6^Z7qfDVM4PO=sm(3d9-8r?2B(3Xz}Vl{B68{|BOHR#fSXx`*&bw z{&)Y$o4mR|mex6G@grClXEp+Ww9lHznH0J({&ftX3V@uI{0)0Pleq40P`y(&3ptJH0(p}p4yfnHd<{~wYwFOBS4O!^%*UXm$5Q%y?yOJqEy9X!g@=h0fO01 zRyu3R6tYzkB*MWN)&R`N7)#CBm{lcdS8Y`C;z=p%4)e1W^ac*5n2^LYG;173UL0GAH;9E6QyV6*C2ovpE1 z2b$4_#SB-jF9CD(4y-RmY|clVJvhbcw9`3aKm%qCvzbNE^`_V;pzM)vjnN}EMp#*| zi;>#)MrLb9g*B&MBvW=Ap+0RAgj>4nb)-Tglg2zxLO7~iNg2#)PtI-bF1Tg2M1kg* z;#a8?fb`W2BFwbEvMQ}pzDT4>f-@%}CyrQ3D3J)NDrvQQnBrdK5W`ejiWQJKq%N>L zKV{OMMjxIOQ@VQtWX^_hppV4by2a>f-Z64EYnbiKuy?qF-NQx92aPyAdvM`5U%g{D z-+sVb@0{Z9oikpXt-P7p?%@KnS&PMT#>K)g9}FF!cb_Og^a7=-I5TCP)tOMe<&%mz zOdS&9TOm_BeU$p4^W8ZrRv;kU7#-+%qX1bk)2%1`9oxp@TnvM(<)bUps-k} z<*Ka^rFc*L6O&nAB)+U-Ykt3t0^-p0p;{lLu=arTZpgedVp~wI7Wk?ZPwE9r)p8HL z_o)pWsNyWsp9b>y05L+6+uXW|XNJf`Uh;LH0KGPl=Nw2SELDvtRoetmz$qrw1FUfx z*`tT*D!EPGxD9Ew;?xB}s0gtBpu#}9n?M4g+@T6*%RRlU{`q1-Hx3!zQnO3X~D9%>=vch|O({H!)l{`cyI z&T3~F4hkEly&Sb!EK?le-MlI$m@OSZ+8rfJo8O-c!V0`r2=ys0Bl9Df0%_O9BcK{n zm98qOS_sL9t2CcrzA9Tvj%$M_2|?5RZI$j&#(8K&^j=Q}Rs9m!-%cE;f!Y@7^qreb z?=2bs$rL0Wxn7Z6X>&XQ7I9bly-cJ~&UrqViZdqZj*i_yiG3XkQF}KbQ#1vow*B5b zrgbpZ=4=&k>wwKrulsKGV6%~pj&2pYS~S#rITaXW_+;*gJa;5f6r>SM)U*JW93??@ zgCRn$0M$)ivD;Et&1|lV;6%ve@QnQ5K+n0@xlYA_O(p#5!%nPiwI9h^XPIo}SttAk z28@hRu04->mjY7Isg6^t5aBgV}so`3u|$JL|9 z_-Ft2C-LH)m+|rIFJLj7+xhxbFV(jdfm|&PgoX$*WpcGOJhlz&kw--%dPi7u9xNJu z^5xq&L&vRu{BLtU1U~!O6I{PB_>CL8_V@nbYj$z6##7JiVeD>9NwP>9W@bR9nOFp! zEfn0U%3Jsq06jVzRV9PzT52zgv_mMdkhVgK3S_)xwq6?>2yYQBf@V=vvw){th+YZU zqf)ZSWzhk5#+9oJUaZF=cR(`vZ@s2E44Gz=)@0!<*w+S(=_H^2Zr7>(-FRiMxLQbCXss`{Y%JF+(&fX=Lentxmy&+^=Y1-iOz zls+nsw{ii`Pi_kBifbi#mb@g>W0%Xa$Y~i0=Q*ac$R@JVqKDT=yxKboyP6T4j0cn9 zOTr@9@-EXI@9`a=O9(-YlzBojofet)*kVkL)DaERK$LIJ)EF zbj{aazKb_sx`&;Exm|ty09S4t_|cVJ9xit{v>DDXRyaQ!aelFmCIwCGEC()k=Qy~& z2aFx7(>2b{R@j`auvu>yxq8e9nzPZgH6}F*a&Vm?WJ7@>HxxL6nu^oHgk3?V#fz&i zK0?v~Dd$z28Zx1r0#8U<&?8Ve8eR3um{N}n4B2uL5bvXHX4`k((hVghZ4UsWClE@H z4ISqQcY}#d$_1Pa9tHps9-t__>i5=dk6EF&E_NnCnxK8aQm2F`|?|O?WNne`OX8J9iLlx!)!KS_i%xO!x^qU zcG-rZ2wI~@NAC`Af^`hCi!D}|VtLG|uBH>&VU5#ud&s#Vt!NWMYDmNu(I49&8GK8}qpF%2_=eHW=Ux$uMG?FNqvJ znQODd!Xm=ZF-qNPVp#D=FML28c>~zfqX`y_J}BQT7QutX`fgp@1LRa5B3lcOU~3Cl zct5*%$OjMKv)OzYZ@llupSpbT*k6psI6po`9~0CL$Ydhr)7BxjxM%tyVxR!A@}dDu z+u2eI*MToZYQ=wGvAcxP+Jk#{|C4uKfAc>lZ2$7*XZvC~U&9GTx6NkLW~|&gIA!V&2jH6g)Aq&r8S{Ivk4vRSLgV*sKfS_5Omghvad1`%i}`}D+(!W?CE zKDAoZoK_}UD12uCdIPbUB79+_JfRVCx1UIdoD_;2Ig-8up^APcl#FdrTq~zfdApvQ z0C*YdZ%JNG%HtjtbJQ|WPhPQqOXiB?FET@9hzdxL+zf;W0@;2-%kzNn)+U2>&+82Y zhJ4S6ym(H*k*46iOb}9!Gd5}*i5D6Y6 zeDeC)ta6qb0#Uqz5LrCZXC8zmDkPZJwDJiTrQ$%>$WmsMfle*5yr*d)lQ{x8lLuC> zxrhtPccH#MP8%CARi%+mm|_o9$(?M}0$>?RNhp?#h~_mTFm(V-*-s@dK(NE2IPw}5 zsdQC9I$$|mUc`e474+=Wb>{D8!cwxE+TflHT8;n%tLK?0RfW!nn8+9v5c@Qjlm$m~ z10y#vNsNe`ui4T#%eI@30e4mWP-n2IBABLM2ueC8MDz%t22idL1^h+sh}5cpkeDMB z#Yv~LIoyU&kG~u?&A8cz@#lH@0`l->ZL;NjS|_35OfSp)hS>saAqPya5Qjc53if9)xpjmPnc zCq5j8ZmV&FhGF8X_1d%v967T?$Wg=!CC_vdmz|zmY>3!Ipb^WZQ$KYRU;fpH@quS|*ksb6;qwMq>EW`{1-G!F+bHpa%&C%r z0HD$}naK?vk(3GCnnS`vvVx!ztnOJV%#37E{3O--p-QQCY5I_4Sk_z0qrOC40b2Av zvNiC^(K0U1$L!5Y(rjXEHROg!F@QkG#PS3igL;{U95b88<5YS9YW!@vK6vpIs1=_? zpq8rVA15f@*4J$UdV(5F);Y?zf-y3iNCMFaO&=hJ$mxzDyJ-vg^+zRdgj*V@bwv4N zy7F4oh}6gm53NikrP9zPYa9bSV8U<3d6q<;Li7>Afrw4i`=$3}j@6J{d1RzisBuK^ zB26s<=MPt0?hbhV8?NGw*Y4xN@i~t6cMR5|_0AwA2g#=-gUsLxj)9E5qb2tceKSVe zoUifv*Kgy^ufJzQ8*u6R9&WsFgvTDggvXvb0G+%zU*r6Aw9Tf&HyeO~VVL>yXxDZx z?`UGdy2tryjaZFv?=h}{v3K*aW9*S;hy=UkQP-i}hcc#8ua=AA3=EpZsWLH~BN8Tw z?8tU2dgR(_dG={eOSq;uRP#&bSj0%gOiPV@@-$QBIuoWMM*Gy5SxOU6C2R+SGB2s! zZ5kr0_RpXhox#>=8loN<=*#(~Rglc~9u5RMTTGuCZjB5PRh!YHVzH}mb7 zRxA-MseO^}zyx-5=>g59AlM=u8BN-=Db@-65tw+Ksz2suWb0_Q8RyF!Pfl*nPS5UO zm@V!+|H23UgT2F@AKHu^=NA{s(`}|IY)Fb#O@w9O7v=)9mU#%PMYT>;ZK0Ng>(s^- zp@x{DVY#;ez~!C0xBkMdw{QN@ot=Z-OZ!*G#bR$Y_HKO>jqbrl(jt2AIrK#!ENdv) zX`@&=$OhSx%AOc3AP^p5E#%IsunXu-cJDFJf!Ta99$!58-ybga{=WASTMbMWdX*lf zf+34GLm=I8CrLSw9&&m!fC0@7ZaIUl2$3)}y09Y%cuzyoQnKm0%izyZxioYn)j#Qv zQ`Z;)z~x`mlW2&<5r`1LXPZ8^+5(Yzc!WmU(eqxD)Cl$jl$-$nq((;+Pol^?6^n@! zjU_%vQ0IwDlAfMgtrMpT59Ob=z%(v1_iY_Z6|8k$2$Odt27Uxfs5>R9hN&qr22_z#ISOn4&D!*R|eipxS!`DmlH27mdF{sZU$@aKF zyb7Jk5l6@%sAVZqYbqTw1-#jncw6~&KuoYR7gb8=DY{i)0f82|9P)a?IXNaPosLRx z<#iDGyqzhd&$B)ggQye)X{>^J-PxXz2daX6R#GWs)L58?hzQWGcucx|l&k zb5qTMCJE*S?w~uHMdj&%@O(YObNK}2&OfmkPG`$&%ls7)WYdvfS-QxQi7ZyLbY4!X zbh6DyzLvUHH##$j(U8cr5uWl(L-z>UWYnk_5;?kvun4tnWk%Bh8w@d0F^!tqo!%G8 zsKH=N@1u~4B_hb7c0045UhjR&G^$0fxd0w$(m_P>Y2>D#qglhZKL79duf6#%@#p`^ zzhOrl@Zrb5iQdn+!3M^m;!YmP$o(J;)ob%5qr>i$*;3K;S{og}%!AA20{`-*TR8WP zz4bSKmmgl*!;k&Mn;7QCXKu{>_1Dkw&wuh=`@k~`E*AqvuQ;4lW~0Jf2%JRXxW@Eb z_ECxsGs{`(=z(Y%KrG6x$MW9;DzI0LxyGEOWuCdFM23MaV4d)kEhiThMzWH$487># zG$+uqBd!J47ahylfSsLz$0sY4tU)odi2Po1eoF;Vpu8n=9FR32LPgF-!aeC)i|DS6 zYE#Sa6d+}HQtl6-yCsF8M~4rT^s)u+xo#)4n#XjQM4_xLuuUOo?b;^UIB^R?9#u!DvOzuX>iVGc;+aMt{f5!tX5;JFGj2{ zHuUq2^caI-7zXYx7vSz9x~ozjn+Qi|grkpv^~svv18eW$U711;CcR3He;O*Dn!+ST+dHnDK>-7kL*gcqYcW(zr zm-phva=^~ff-MYPZqVpHqOTg@k_4L!YN-I@>^aBd`=AVf(({4+AV%88O7Rwz(VE^qmU;m*yx8D72JBxihe*Nhc zY-c^N4Vz6@qmqHq^w`6LYy*HX6H4{nn30~s4=AI_Bde!e%`3&pJS`gysR1?(0Kam;zzuv5o@Bth_mkgX&7jk#^Ha zx8AnLmk zLznwdJ*UFuNW6!-LK5c0phYBhv%qhpaX`<}0`)(PHW>o~E*V19N(@wQqHav>14A-M z-4nF6?aS*yG%=df!$vm$b|JW|T4Jhlr->7)kqgCg5D9F(j=v-fVq&RS*-S1peMN`N zNfgU0OMzlb#8!DvqJfsq_6o(5L^)pygD54IMRtXCb%^N*$^v6){V0GO(&|C!pJeQe ztR`#|uvAi{EF`AcCVBp6U@AxcD3ii`PAVZy3)twX2H^C$j3glB1gvyF37nK0$xkld z@#Ou$YE5WagWU2^+WN(QF-^JjyU_wM78ne;S@qGPDymbGZn*|*>%^cSm5ggLV@8Pq zG@&6;S)!uOhF0q*gB+2cEt)sk%S;}kp{M1tJkoS$%oXG`c{L;%HhDsX_D7x=6APgC zti>!7TD=#^(2+s((I8T-D?r4EG1g6D?;(1OdV_9GbE`Gt)$YKcYr!o6y@FXT2wGhp1VK7ln9N=sMK-b1AusY;L>VX|m5T_~EwcRUb>d(aEr2mrv6wCR z;pe{#zwqJ@@W1+jKY;fg?BMB3&!hV>V2A;x&>nzX9rTV}Sn(r z8R2HBlJwj|DUD22z(}7>BP=?4P!%*Vq!%68kh%xfuieLHsYy9QU zgS&fkTzmR5o_hAGA6=QZD_3>^603E``eYTG)rJupb*#-HD+ZSH84iw?Jlx;G)u*oFV1K}TF-I${_)GueYxv7Q{Tf>I_{d{ddHrw}s|&|wbViP= zuVMzR1$rl(yt+J$ch6qqSMS}#cYo9W0l)XR{dfHsqf;H(jjoMM#?DIBs z-)#Kk$cfFy~ApA)nIe{+ zp;193`i5vyw^ZH4Q;ntbTW&sjO{iBOP-%pB1X|DRnPQE?K{KPdZTi}m%Qp1zYpc!b zSp;4~7I{!PdI3u>&$T|?qg1?xh;gcT7l=ow?XkqU9_mD}>YWcyBi%?TuJX$&)*@x| z%HUMUm?ZQQs!%@K!%^jQZs8z0)3YI419Q?RCKMiz6b{7#C~r@|?U}bHkD<@^QVach zuN4$xN#a7!CiDATSAi;8ZWSY2;-St*PDGPOz(%o;k@&N41$|UIvnOGYQtv3h5sqMu zFwNW^a&%!nf^LB!xdik~Or{y^S*@?!Sqw+YtHCW8K#>^<%T#oLd(Jh_UTH(&tLiHe z-RCB-TN7~|Wdf1G1f}T8YH3z|byya#O?Jp0VQHo(S}Y#aUXVABtgN6E2c?9_d(oV1 z^?p=ua${s4dqC!jzptBQ@3-y6F1oHURmst#pw7<71NwP|AV0$@txwc_+Hk|fC< zUlg|4I%P5$paho5_$cXDHBk5{TRf(sTx4LwvEDv8a1fUF?shzOzkFA z=3G`<4#1)lm<3XGrY)^d5tbv`J$u+K`3x2EO3$(3IxhTR=L!4Bv)_py`B(o1{>D%K zIez5IRa`r~9;?1mP0pppM(eCm&j5E zq2kSCbf6W{qA|KhN-m<4sPX6Kvc3v*f;7s^322DHOz!xS@{svrvW-Q;ovxOAB*E7# z+l~;Z=2KHn&xjGQAy%8fQ-@1Bb#}CFXzYq-TKV}(iJ+^f<&0=#^d3MXTJ_kfEhOdh z0vxQkpBi8Xs>|LotXu7_38u3qT1u-Ms#tWN9FVlzBSK0&p=kHC=rd>Uj5aWuH}e+o zat}&}cR-#AQ7)nxeVBQSDhPGn1Q0FD&8i6I*&G$pkVLW6P8L!XSjaufnhb;L7)lXP zNIB=-(`c#U4mg>cZjV!)KT#1zkAQ^rP2l9=1s;Fm$mg@cUVr62Vzb74e~^M$aaJ@5 z0jp(eS$QxcHP;E7?y%HTESAH#yEku_xsT1J$Hm14@4RvkZ@&0$0}T6@cJbs>m;KTc z`*!8>KK8HfU}(nCJbZNsF2!*)@#ISI)I-8d7+G9UKOOH^yQ)aA$wv`$r2~ z9xh|CGb3jWvDxtK!3B;Vp2jbI{w==o+HJh?%5A*;_5<9$ckVvAwYgz;xrp7v1r87A zyz%5y+})dFFoJ{S^jX0a?_EU#>u8Q(YZ!)s=?`Y^1JXn$!XsL1*4^P^1e%nJvS1M9 zn8a1>0(D^+%E&d0soD&S)C?tZQ_B*k76Jf>obVQAKzNvua{yt1&Bn1_J7|t)9$Vt$ zFC2JxxBut&zKs9&U%ZSJ7$3d7&nGS~xb7Zj7aMt^C~Zx9Y3*TpXR`sb`5wOV&X;g! z-SHp&j{i@5`v?CRe&N^tzqYnhEZSixA1gNqDy~(w8-Oz?@1#IeU~eJM!=MJ<7=ooV ziymaQ3&Vz6t%rmRY|spm^8d%A_C^t zK=qSbgn9F>#cgaMS4lM5RK)X!G0+-&us4n4H6>z=BfA+e%&l)W9q-+Fv)y@cYj*wd z8$a{OkNw8qznm@KefaPgo8t@RTZtK3HuuO#R+(9i%1-H;GB*!^11uE+HJ({`;1QlJ zfZiQ#XjtwZf-xSv^XAQea{KPv-+B4c6MW$VAL=bER(;dPaV4FaWCemf-oa>kw+s;? z=_SQTZkLt)7Gn#`ihD4qKGh*J`tI_GaD=)=!a+A0!$ZGJrP&y4*6BE@b_YZj{wqa! zB$p4D7eZiwv1dPds&#`X^E?KnaH6Bk1la@L3bPPqo_RV^DN)Gq0J&Qe;}p>LJQRgH zn#I)SD-WbzwaLUIx}L1IV7j?Sa73lP%s+Q-)i0Km>Z2HHt6;NC957)_%Oh3zG4mg< z^-;cUYE5S7;n(WZfAqP8ey_0#@0k&>u3br;jN+3p2qpx|*5nYGXST&Go(c~AM|gdx z-n1;k3J|2*5HoTylWvRr?^y<%Ky&uyl;oAMl`|BXPyim1rX}D3ki(#Wt$YKOWGFw@ zLoFE&bWsx-5`a(eERlev(5oJLZR|XB_2SdSgUofI>W~#E1aqMZj3HlEB}96edZ3cq zC!l_06RD#5sPj&;P4%--?2S;mEf!6=ZqFVUL~gz!G_busOBM`89HdmD#vf{H3XB_M zUSVAz7gT>+cP+Sj?x=8>??@SPZQ}a=To{jUWs$ zr}l4Nb!eey#=Whequy<GB5P2e*RQgS%!(@fy6k_Un%%=AdM7HQ54~g7%6qY1GkLVa^ zQp0!4o(I**T0}LA_o@Q$CJ-eR?6c&SAcc}!2G;_fQlrF@g$G&;Upeja5KczgA8UJY(&@=iZANfJTI?ls4>Fn~+^Rhl$kKWek!Wf;VRn9nX@9Hz zC6ZthSdzGawA{QAYT!qm_NFP&>{09qj4fy`3XA~Uz!Cwe;G>6}HhNR&9Hzp8r~g9~SCzVp8WTi7 z{$3%uB4yj*J$QV4W|uE7v0PmBH(z_e^9Lh$4+pRrd}P2nMTpW3ZtjjSO=k<3h9McE zW#M}a44bvX(9vcs4)zwdd$0qHJyzq$)%l2D`^r1q{QL-#v46Pa<;Qk#^~uX|{l-2o zUE0HJ(O~liV~cS;a=mt5oNr=vvf;^k(>57>W|?@jM%rMQ%?BLpHS$m;RBJ;7oZ;Zu zH}FkI?`ka=JHU?c=&+% zoIWAJ1l_x-FbZc=^Z@x z2;NvE~OZAK+0D|c^USqG_7R8w6{d|R6>aj|-cFTedO z*xA|TpZkM9gX??G@rz&ozL2`?fZm<3=rn6eRcC-S4&m&y7^aG-rWMgF5C)Uqs_r?@ ze=R;Wk(jd7oL@)6JgCmo7G@6bO#M#=m|~ao8|FH@w*W>Fhua*~yb5Eg`s<5HHqyf3 z-KrvJ=5(3pQU|%J!y;h<(crCH^9E&t45_}XVQ8&eV_*42yM6b);lasm+_>??PyMey z^67tjU^_W^aEvovpcz4vww_Q6t%`Lz$E9padIc)9O@KFpu+xB4d@4jJ^gEDl#O8a4 zI|v`Izy12lKXB{z&2QV;-N$p!eAMUj-B_)wsAL18t-Bu{1H$bhS)DsWClOvG=TILAx$%dj_!;MD^!O*3cl&sl@Lv zM034|4n#B9sPnPZ*jv+?%=ZlXoEk}~%oC!pm754>MW()5+>C5bP+kMI6d|pm6X;Ha zd+w~L959n@q;fAYh4R4K6N&kNX}xO?3AhA{g0TFo%saE9U4`p{G*j*qM2;keXR4ms z@>E1-+7doFSV^A9s#X@jNsyW7uvPUkU!T~Ar~ws+2`46}|DNB=3P8H}NkU)XkXx8um3_Tv6KJ7O zSZ+f^x}xWWp_~W=>=MY;hDoNUlpA12r<|U%t|L~us>tc`i(Eif;508)q^8;}x=Fq? zEs}ic2*~U%pL0;=I#vLhMcUN0CI~jk#N8&YSHDBkW~4T1hsotQ-*aY#(Z7NIFRl+i6tkU{QN+#6DI6`U_P5~^WtWtSiJ09hl8l;{}<{d?BcuI51SS65>qezSvYE3D#96&w* zk&8%cl?kpr+^m^PXk|tZ&1Po3Be5n;BhwD=>R0~pzl*DNBX_2FQ&%$F(%B+l-D ziu+7tBJ`U)PPHy{!cB9n0HRB*+!D}aOo<-q9}r2Fo@<^4GX~E&J~@p^FHym~lL@E= zsSu>xMzZ>w>|M$hv=&5{OW8s50i&%V+B%-S{Apafe7*hI|KSDy>YZ2ZH$D3iv^A<)OLuS{YrOtfz8%Z`IY0A- zdu@L%DeB|b7xDQozKwtRbMN3o-*6dFGHiy9jSWF$oJYWj7F}m`NotvqyCJe*gQ=hh z0KKbf=&gK>5#eOZ8O9Mt^}N>cnERe!dI->sXfZ;3HDnPPM3d}E&&+hJH4r0UX|$&O zYyixoclg1x91RH3?I@?Z-YiS$r{rUAB2?G~Lf10rI`1jb#&ns%TlvIxf*LfG<1 zP+XawtbS*p!6(@`rn3}aik2v`SQz9a8&$Ytsq|z}7Ijucm~rS1N6P^jLiwJm+X&VE zjZz5NluW@HZs93IZB`{e=5_^|Ppu+wAV$W?@rt|qbNj$IUcsxc-p7O67uY{sgw2Q= zo$G)#_1cvjRN|Zfh{hfcqdDQGPU3;%sF@A4j$q@g4Y4?EymYu=cVOIfJ3m|Dt=I12 zm9O8n@nRi|MdN&D$%CUME?wE@wWki^`i)Dx{`e7=-*|vN^T5huvvRvQUt@i?#`=83 zYPG>QI!=!_-p3U^1PmL6rmPv-5L(1D;{XO{MskMO0b&fEU#tQ>N#-!h@|0k7csQ*Q z-Mcf?csH(9R`8KIoE5N%>%O_ z&PR;r>o~o8VRzqph?_Sb;^tfT@zz@p;@!6&@Vz@HczFN9Mh^^wV?JA8u|LP9%lo+c z_?)|YbIj)eZOBR==%I0ph!N{FBdMNVGSD#7C|U=i%MoK0JrJYV0A7?bCk8UeX0xHi zY-Su5hJ_87&60;Z_S9ob%p>@7zx)t?=I^|SANi#_0f6s6*yH=I9Rb~O?i)uR z49(2KWk3aHT|4zJn=N10aYd`EW8i}QbW3?~@ zti=D8E_+~r-jsxO#Zu|E0wL5j!ELypO{fyAJg|E0vZ5kIM8Z5312zB3JAy;yS}D{| zmQg=X9ejak$$=sbjf~?9tx7Fr+ES|Y< z=5uM+qTJFTrh5Xvh=ZfOK*JMX`_-@h@SR(?zxmqb>v;e3-`wZ(xouXRr)LiVk_|%+ zL+TEMxihS}iVE^*G-*{_Squ;;hh#*k;BJjnL!9MxdK$}Q$tihYQ(6%4Xv(j`P2V3W z%tm7n)&_gWthu=>Z%*$K&=4GjTbh8r%d{SJ}pcDC71F~seGB_{>1V-iPnZz&|uIzGwqbGS$09gu|Vi1JeD;J!Wu#VTnrB$?@ zu!Q7-qGafq0%7tSQ);Ke9(77*?xXeJk|0+ZLrQ|cR{32CwC0$hYSf5qH;|FK_Lwq- z+E4n26fSNN<-eF%B9IPWK;rvq(YGWAhPvz8r0U^ml55!ZVu0#%%*Wyq@ROR68LVQm z5(KNLls)#DSs6<3t*}*+6}Lf!O&1GjNgm{Sm;j|g5mE*C5|9@5)pN;kmhWwuy*|{5yb{yr{FNDfZTLTACQB#*S$OV3HDPlwT_bkIMLUuxrCjEE)`e zt^;r|LqGFXq+J|2~@^jde?NMq&KZ34E71`wzdOA7kk1|i6ftVUIG4QN@8%cjA#t{KQLlc@`W0wg300?BM!teD?Eqa5QUlxZikUX>Yu7-hSYpzwXaJdjt-K zvAY@NWih}xGF|&agS$9@(Q6%!weO?5IDbGSsPrEmO-LN!C1Ep$NzyE_Rrak*s|ZI2 zZ9t3asS6BF&5IW?NHRpKjRc!XfZ-!lXxCbn{6ODr40ks((Drv5)*G3pd9v+FX+XHC z7~nDu@pSUb`A(J&e%YPM~=LMh+I_f9H)9uz(bQYVCe2_(J(f>@6y*JB8*1# z2s6)Kdbv;1BYXhm>ku*q9bBfi$($>UMgAL@DmkfYbB?S}u|AWWK*~J>qV7h(_ebWuqzFfi+tu^!)b(2Nw+--+;5v+zb1-?pBr3J2sx1j- zs%X)0c6x!ue2(|Oa1HP5+{3$Xop5Joj>S$x?~2D^P5l+&5prXzxRl9Z81z<1_pOzW zXFqBxMtjEJHg7nX@5Is7UG`3lV@F?aFnagXvz5K}#$DX}v3vB&cI^OHt{wV~ zCl1@y8%J^F`T;N9*pI7EA7Yp-VRNS~W){6`tE@(DHY3J$kMUy7apUakh=?)99zd6o z9V3D>@@TQcBrF|45hq5cH8ut}V~1I@*&LV+jTi@MTdmA!cSeV1%8pQFu;m0R`*_$n}t8*D};S(-x#L`wrHNR~1*W)_fZFwMwB z^rg0hWI&ph2BSL|j94ScSmSIh+g=i3Xq?Rr2Rk#i`G7@hF&_vYnH_%WmX1+fwi`=w zvu9z2J)9)Rz(_>Un3h(E9soM%BgRxjF6RRdcP*a3y2~dYU&g`CjQ5W>_Tr0o@Ylcp z4g8}ozK2^E=dr|$-*Wwc*N$cgcbu&|q?o7m1~l~W5wIq(U~WDbG0b;ya&pIBzWo*! z!z})5ozBkf^rFgC>M{60>OZ@2Zh!BbB@`(Iza zdiB5Xp`ARqa||<$5o=OKhzRSIogvk{8DF(Bk&IU56Dmw4nCleNQmAh<<(j0U*&K(L zmxo{d<*$C`-CH+*!xPt^!pA@In;l?ZZ&vN>Pg_PKC~Tko?|he|KQkH+V)iY zhRRAxm#ylL)#jlpkBWog=ykR-izL$*RxsGACW>Z3w;EZkTZ5;QJ>bpJ+|fe?RibkA zJl|90q&DkFW9mGoJ;fz?QY9z@(NVIw$d<$+djMo_zuvnifJau|EivuX{vBD}55NXu z9Jkpkl2EV$hR7ol^3D*Kkp}yr0w+3@vjnF?VYN1wa+|Ix22+Q38q${sp9R#u>{*bW z3b8sXQ*N&Uf87rW{+4g~^i+ea%228NFrD{Ru`5WaWYXn8R5p}ie^4;B%KXKW)S}i~ zesp1G#GfjPTX{D#q0667qYXSlO4MQH2sCF(Pacu-=Rz&;sdt-zvw+$J*b~r3E^buY zGV>1|ITJt?f3yTV$?hN*BE?1NciMD~r9$@nE=w;{M)vYrNXB-+W7ewcg28+~X|RZVZV7VXhKMeiXKcs3 z==+AO6pzUM0|gADsdZa-LH%kq@I225N1E^M${C5X9A!sI9 zp;#|g-e54y+8kf_@{isA_D}x4tEa1zP0)?j8hi}vWHhx_NH|i;`xIT3DhL(Gw+FOH zMpD0{Rh{$3veifW1)0w1XL8OiwY>tC#*>z+3dh#`R3!<^QDmSBY*sL3p$lSg$X**ty~#dG0&?OTYGm?Z5e_ zzn@oj7Wm+k9|zWFxQLB<5d>&h26;tDLNp~UylNnDhdUmZt}M9P5EimxkfVd1z2bP@ zc&>TE!F(sogFKS@S6`~0WJcm3CFQncv6rzxzR6oNsV)y2da|FyR&dlZ>k3x1cZ6w-~P9j6QGU_zUt1@DKuq;t>Y710NgC4!=XUZx?4CnVnL#=$)J!^v@ ziPR25HbJMqe9vP4q}|jB1FBpjIwy46R?JWDl^~5|NAE-%h@-=K+_`hkH(t4q=7`-( z3q}B;z64CCcGNj)h!_KGoB|kgS7|}ik(3;S{H|PVM}+7Fw8m6#^%&S-gYn+|)3|&4 z6enlv7*E&StTs4XcdXAg=v`)VHUNux!~V`3M^_JV?Z!S1ukPW>V+S}q+K;`X1rILo zV6ij9FdxunO(lGSW)^G%K@$r#dfuYbnxPYpO_w*DHH@1MKi^=py1@D(FkWnMdb+~H zhi5oH-C%XLij#*c-n(-erw>>(Ip!3DUpI%CE+@wV=@*_f?J0k(wG3rbfJvN z>MT<(Q18gHkO6(JF|2i<6rsc&p?@IiXd|*Mc$kSbReTX0$*mE8MIwh5`G!-=i+;!|Y zNFzGi5N$ACoIkYt_il2%+4LJvJ^SB3^~95ZGSK_k$r>U&o94fC08Iv(-3;iMdj6${ zCvt}Hlym7GQ#xLSn^68gFwmpHH0BCDfSvgQ-o|hG;+H=6h08}z4bMFF{);xYw%%OK zKo4te5_i^gqDmD7e=_YcBEr!m(nysRB0^|idX8#+jZ7isDeOriBXSh__8tS1&d)%K zLNHcvb^sQ;JD2VF;%)5h&z@P0{mn@2x$HrKG30qs7$UqQ=!uC$6uFF}6Cc*(wLXE& zKJ9x~C4GwTI-d#z2AHm#uDv`tI7jB`kuMfgrM$iS>XA=XWMP}Rl?Y$GKK)jaij;7aHwwh58!5FmF?WOke- zZ&d9^??O@|QSO?;V^tN(^CmyHG0j{A~so+sbd9h>lKgbH9QR}e? zu8!;=eI(I#5H)Eb-)?9krBLw7L-VlENO|3hoYjh} z;dH(5M~D>Kv{GW?c2c#G&PaOKRzxL&R$TyuM^0PRJMQ`ZBn39HW?2cdRR2+p#e&rf{8lNLhJw$;>kHVljzV=|9@8&U>VO@p#>sKqj0hX zmC)%h@>*!2&(1w0KA;P`q6;P|*Qf&{it_4F>`wsN_v}1ho#6VFkH!m6{|5WVKmMof zAN<^3zy~iK;l`D3#CkmTjgLlS#*F%O$ju8`f+^WZK3^-xP)J%0Mj9eY@iR2wXx`#8 zFW-)n*zo4x`kl1h8Gh=Ecd)n9e6!iu;o%%t_hqSs8 z9*JR@sE4MBK=m~JCnCt6NsQ6SXd<7Ba4e#i@Q9GnQe}4-q_LnQt?L=hQpb%g_%}Uj zsZ4!qV#jg;>EJ*_I91~!#*M?Qaj_(hkJVydeehANhGUY7=s~!05Rr&-QVOG)m%4~4 zhlKZ}Q;T)#V7H3hMar@o_eJ$)a@cf`yYexzLTfhbuOrZ8rW(d(4mVuK5TgbE52@^t!@G??lSdg6(xR7cSCgj#m2F!tx zBh3w0F70rz0effXJiX{J5W~!5j@ZM;F|2Pk*o=YAy2t6+Dc}3W2RL7i=$keA==2PK z>x;FV#BOs{hW_8foRyGU;lRe`<s|p z!)!Tlv23_<{Sa51vCR#$VFnJGZ6C>9N0Qw=)S@xEvv-Fhxb9VX6o4ahTuJ6&!Qfz; z%-~(y&e3Hplhawk9V3xRG7)P4gPu3K+JM5N)NF&O2N&VWS=8_0pcFj&mz#+}(Lo_uV{qul`q%ZAxNY`TSy zj*E@QyZ1)?$mj0x7hXNamtH!?*X~^43?pV}ygVE5{N;nVdTGYR0E}T+uSXo8j{sU; zqs!zk1ScF|>!8hM*c}#JZz5j1{UYBzKgIhmJ;VRYZ~sSl`r1eF@*6)J_s-to?ra%o z1HJ4ttBe_-Gc0$lO8jy{EFdkE43IE&SIud8mMXneX=S{HCTqp zvJQ<*Bg|D8bm}@wK$SU~XO@Rao`gWbQz6>|r8%(@4Hm!aqchr4%PYk{_Y`swHX{Po8k|1TYm^Yat9n?^`mi*Scwfacl|J%-6wN{mL1`ZLM8 zC(o@^u?qNS(JT)d!L({eY_RCqUF@Kbr+?%NU-(zw`|*!{YMi&7@%;Q^SYtg5Mi5Ol zv75+&kE+NZD{}hO@`E2?X6UZ9Uj@HN`##hmAQe9|uhkVZsUc85iYUWa(MCX$a-xT_ z4ix}7YxCYP+In^V`eJ{0vodUKY4thofFr6SXaj)L*EDq+`KEVJ&1Gg-lj3cXVrGP2<2?LWmy}-Aq z=<3;3uIza*6L87an@eUQfUl=#b^od~0d=1RWDDFD#MeCNBvmqpVo5qsi1z>QnK{bg z83ZgkoP!o}(5z|coaCv_u_igEt5%Y6`eHMHHQp52(2@0 z&G&MB4*`s-#)}fn9a6zF>Kc$IRf54}>zPt7EyE@ch77r-kcb4o*Vw$9+Sx7HL}ii<3Th>NQuLK%ItujFL!$NzN5WHP&itLI z(TW+pv#gBN{-D}B1q@;$3RGy58_>2GG z-{8-_`p@HA9(w|NyHD_9{XiL=A>htRrU4ybV9x9209dxy1&Ef4pwK>Igp4kG-mC%3 z*&=@I#W&~x@BZ!Y!u)XIKk>P@ZD(lk=-62f@$`)y{NVrcCSQB~0^jrvdw$;6Y=J=Q zdd{A}2vHR{y{_I7W+Vizv*P2l&Y)(mrg(I6F%~BzyV~@4%c8KTsBT#uVMl8NdW-^E z#z|)90Bj6rqk0?HEPhTDdIz8P& zllc%TYR6bB4B}_V=zz5ra7W9UFtd`a8N_|tHU&${B>8{{Kn0u_gE}qiOsL>OrMjv| z6XiA>ouLJtouWi=lhRVXn+!~821JYKj@HQNb(&J`^jdD-Vb;)d-UIacFf^&==`O9> zwAFL{DRkw)zB;tL%pj3*zUHNrZcctY$SxnCml!!RX{gky=7-DWGPO{V{$I*ZGR}t5 zu#<DL7gF?BtFSDqDX%aXvn#u zP8L|w6t?S(u_jVxlpJ|6Yl!(Q&d%06Uv;#VfdkRfUZ%B%`2w-KH|NpSy{J<%d{pbl z#$)V3gz^{fjtE4jbI4W=5h8{$WM#tat%v|Hl4w*VMzEK4MyVC8!J0)hp|G}|9z=NOAn=FusmStA=3yZX2`$1ph}ME|?hHgj3v1wD1~lVx1`M-? zVK6La<~#F-c>|Yoi#C8`7}u+a2d5kS>#rW;?fYxL`(S17+&%ZVA9lNa+Ht)0IPp$H z$AB5G>^6M#@dF&~7?z8M7)5BWd<#Kp)WtVkn7dWN1%xO*AgdqAdpcdL3VX_g8rH zbQO>99pFFy^k3y~{Ky}Q_in!yU;g^{o7up<<-Ye0)0v`IMFiCmm^n&GEyE$Rh(uT0 zF8^(XE9zPRbSIn1(+3XoCJq<1K|Ei46@oy~dTJ0W^R!4xL0n|ovqjYql-y1cqmr9E zco>m&;ixQ3SXH^0nZQe!dexOhb<%8x*1*{GxOx1B-MfF2!?1h)(GUNoKi-zZU+?1z z7n^ga#nCWpgcR6RfpRj`3$#?sCQSSFd96;A?1c{XYFF+F=u9voMiC#42KK;gm|?Ry z`~Cmw^Pm6T_dWZe4Lj_7yl6wih-jlDhCooJOzeQh;I?WfC(=L((JTVqQh=bEa>@gk zN2oK~#-cfMQYa=yI8v;xVrIF`#YEHz1v^!i^JVNq=nQIxO)Oh zsoJWt-!e~)sko>Ds9EyuxeUt`Ug?_w(czJpfu3`q0d!|Is?kfmGiG@r(*OYg07*naRC(Q%tUx7y=%cdV zrsrDnQmt(Z&wA>a5s~mnA1tIlm}N^vj-`TSVI(yYW)nXI&?d}E^&ngwkRzzeTAFmz8g12Q+uXNS7I?8#DZK`7;su~i<#k-#Mhg2?A!2#joU zrb=j0b04STAA!g|>?u@qfCEyG)+$pG%bVs9FqIo65~}5dG_O#mMvq4f3H4@4%Zi@o z_jxKC^UING5Re<9CFhZ0p&EWohoqM@DpNFBb*Q>qCKQ-92J)W_rO7e{B0<V zJvVtm$C5g?u&I(?J*J8FP!l9{mF0Di%y(`^vbHEw`K@bGE1AP6vk{uco9l>!L~%@2 zNGD(>)k1ERI_4@+F(g>BV%Nk~Z$Bea=2J{gT7{MLzt(e9l4(t5+YR-J`N%BEvZ&Xe zYet*XG8pLi=hhe`%7_i{K4LaoV$&~XAcAcG;MTjlg(I58X*C5w<}S){C`)XKdqps64+f)) z7ZRnQ04BF1RX>Y5Ma3xC6eth~Q=b^Q(HaSbPOWFz2z3t*R2yBm!#(iAlmDjgwvGMC zzw!)j-@Aive&&O07;v#UuD75GLCw4cO)mC2Owy+;leNt0do@sMVJQ|zhJb-VVh@hb zyz&-zxWr4}`)Obp@l&6_i)AA?b3~Xu_v9|V@TEKPFMsJ4KKR@r&SGP(Q3x~_kws=z zRx;eAdV^{DmsnMMDi~%M5Z-Bxh%r(Xr9>h}6v92&s4U8Z6TC%QD2N2?X&f=-n@Ls? zVYFZZM#X6DILp#*1d#?}TKxvYh~S_I+h`mMwjd%V7brv7bS4aPruRhd*U2R5k?Kk| z|7*Yuq29q@n(qLJt~x{J2OW+!ad;tHyoj84??Cb@Isjd<+5^>sVO!UuiG5E1PuVtY z&$sXh6|)=EV39sQp7vXzzKV#di<>oeSSR~HSWm(dfVa|UJH?4@_ZS-3J*t0Egit!9 z>lp!7twKnMnPpG|V9l`DS@N1$yz|~EkB`^bSq#y3hia{iwR>zfAY-isKmudrwzrUyrK5oy zX3SmV>Afc2Hy9kMwIL!+yMs1U#ix%WK%?cT+i=9_AX;G49evYNT4iAG9LbO#&1I*7 zlCh$g&Llls)I7?biv^ZeOF$Z3xf@}OaXR<901OnE?;PQ_q+vcZyR?|c!S0+-Trryu4T}My z%2drq2XJyBsi;I8Z7Q(z4j3BTRPVQ#!P-b`1BTee`R16f-1;gWjL!ERUWq^PslSBZ z@R2`)ho^V&<(EFo)#f3GHU~J@X!&5IVR9u&BC0bVQB)%!1I|cJKpBY&K>4n7$?AAg zC6yU?vwUw^T=G<@v3GPfl<1Htq6;8UdQD9w!kC zoq^`zE_r(&*c3w1gBuHwGvi{uGsgOSymja1?t|m|+}YiG=ffZR#CPxR?0sRg8L_!o zCzcq}mo;-8D!@^>yX<#}$>2B;ITb5obh0OD1gv&V21d`^7{VzY0kZ*U9gRefh92w3 ze(@_`{)Zp^;J5nz?jifQu|@z9LqJnVRLD&|M93bGu&I!y(hK13q_wmM5={-a2=B&L z#SCf8Wa&!svJ^im_o=y=rkiou(M%CcsIa&XR$XPpT4sghw<<4&UQZ6 zy8yZuD2@cgS*R+kjmm4YrDxO^R9dSuH!+0Nt6Mof5ZNAEm@IKy;iN437Jp%BDTB<# zqe!vDNLf-x6||7ot;5P8xqg`mW--kauYtm6)hbZa)R3&L$mY1+U`6PFUT4;&f zRIX3ZE+))f0I9VUR^1S=aIV;M-6I5CP@-levEi;vzS%R_c#x%u81e%^x zt+Pz-6?N2qX`!lwCiZyt>MO`p2U=<{1ZEqvaZ1-#TSv>Kr~5s!T{T-2a)T96Cmlr7 zV#v`E5VMEIsa<;7q$<42fTASSsz*eIL#C#4x=|f?Qy{Dbh3PsvLTZ7;uC<*pRrgs$ zRDEMIZMkve>omRpta@$j4e~iKmq-2oO8S#+*3%#3uAz}_HO)*|JH*~QkOmT`;p4#>A7 zJ)th9M+|LF^?d?lIGKicp#kVkMG?Xf>A5gjUt4y^T9#O<{cI|=q~we;l4LOvk^8tc z!bb;N4s#DcgAH6+aSyge1}zH)if+}Jo52ZbwM`!Z4i#S!J(ON+Um3c&CMp>a)380t zX)>R=cBc}^DE(|u!c5a^l%a-CGCxv@izcTqT3(-9cmvFPpCU;nx#9yENd1h|NkJgG z6h=Mw1ei?~kueg}`er1J5k2(~%mt91ebG4eiU)Xcwu$wo^Xm0|931WP?N{%{y}M`F zUCub)oyqHuq0Lq#4iOl;tST^~q0!mZdaV0q^9T+^@4+@$ zT)DJ^$FA7>Z(Jd@S>!F@~8CHq2X69@|3pO?kGr%}t);eah zhNTfR8Gc|BQXFl9xL7;a8z7?Lz4P#k2VobR9vABk&(?S#~6%oXYYZHgVC!r zRp)ue6WSy_ww!P~VvL3vXKfK}05@aD!_)Wh=7U?&0e;}xhwXg6m zt3Sf^_%P;e2aEYWCeK|_Z%mpo>`_3{3_;NPGWYSGA5U56{T~|6dsf;WLXD88u9^vZbr%5Wb ziJ(Pev~X}VMgt^_36jR(se+406ZFCW2JGK(xpNm$K6HYhCDyhzyQCl@7=dAjaeW>X1B=vx7cRomtN^}=FXA% zOsWrntk2)>|0<~FMdWiz4QLvnC-5$+g9TBy zt<_;*qtBQZuG>v&s?J20|6Iz;ZumDM;wP-qtjMB+yn07pY$SQOZDcW%Pu&k=z zkpQvek`0(U9){IDv1SRjk|866Un(Ada}99N8b$pUCskvh>8P zkS2^ZB?Y~nahMFkO3p?lv{P$C5>gWzqpFjLi!hYY1zGJV3IZwVO~6l3gTG<*enZlH zs9K$>h&D06%!+#Cfh}YyL%|Z2N!eAJ1#Nt*HRz^&kbQV{T@#no9AHCD-q3s&RbNt2 zlet2flr&GK%yT)HS&ydPMP!96>X3`7T^mKwLEwyDexwl`jVyAEZkpE?oyL|nw2++4 zoS7dMyu-xGvbdhKYtM*nsXSFp5N%TWC}>5_u9Z;4BakU6#Y%vtdSrsg5HZ(lN#)+o z8gMc=L9-!3qhF)hYW)C5d(Yux@BcJ@4vm73h=GXFf|BFsH$$V~f}Ekn7#-adA)3vine!)KyMy;#z8qisyWhrlZ=J+1 z{OXzQ?2y(y9KoxX7dT&y`2HV$6<4n;cyQRlH$m8-#HrLd)nY>jOwK0=nV*FjnQnLh zV9fzH;gAq*XKZ!Cs~ab)zfcFgirXTGScIChseqIY_yN}$)2yO|!a?rIh@nTg(YmQL zB^JGhi%ie5l_mY8b|!MUW9XpTiG}D9)pKUB9`sTIsIZ!%?@C&>Jy$AjGtg)P%$^HH zB!bd4c93E#(nJF4kKiMHf0>?{DsSS_nSyxf1Wbno97(=9U_)dDAz*_sdcZx;Pvj&{ z^;1U5{gl@>K%+i{=qYVGk#cg1ozLtb(DV|~BtuCyB`eI)(Jl9in~ll+IVa6iF*8)1 zMp;zCN2N5bO1Tt}A@9zfy_>lnQv%6JX30ixT^oX|JQkfE14uW9(YT4g{d;FvECxLH zp=&&P@5JAFcIWsUaT-|88T0v!Y-%@eIZrw~%wX73V*BCBNL zDC#@&A9cD1fQ}yE470XJLyzN&JACi{EkE^@Ez$68&;M?I>LdS|KY8twxO@6GUwr3h zx#@St)((f|{4zT|)F%zMXld(%8v0EIe~b}Q?-E$e`tTs5Qw4SVsmySHvI!n zKKD%b(Pv{8_y`S8RdSCGs(znPj2cDPJeryw{ z-6I+sK)sG2t%06aSPAmFg&C}mj@IZqdk5_OqP=2|IK4pul%X=vvpSBRY0^z z;u*-#&{5OPbHsN6;e;U+h7cx5rFbxvq$(uBApImNCOGvVhJ!> zKCE(%ca*Hea|0tMEgKtpUSB3qk^(ioG@XSqtwR9Om?H0VV>9^&Fz+i0py!;6l3>eB zB=^Cz1c-FI$i`}JgA8ICnJA-5pPnf)c~4?V*BQ{rLQi%mK^{=G{e01_Od#~Yo1v0q+U-gZ0-I)cHJR3_mM0xbcprYwbPW_4-~ zGu2epc`EQhX0#4)aEG&r04E3|t!u7wGBEm`VrG)xF2^-hmIf5))uF7KW7OSc@3azq zy^s`4r@3Y`xJQ`LV`CQ=ck%RNztP|KSm8wg_{^NaHLI$1(3e zH?Jt`d~G__6x-NB6T3)Mi&YC?`rVYcxl_7pS4F=UL20gTv_K>o$x1@U2w09tHYYo? zo(^K9qou4#=Ytw|3?{$67S!gb%0i@+tm0lJCV?sQNqiZQawMupss-Qzjw*%J117RL zizxF*Fm%a*aCHm7$zYxOK*CaZ4A3n(RV3}S5sWqf$rftaf@5ac0Zy%G>d0VK4$Y zB2&oO(8KxQq;odV_jl%Y>4l|l)^6`TJm<-Sb38n~2=9P3{{P7O)1Y0m>?#agYoF)+ zhC5~6oX4ulnkT8GDhUJ#gBfg~p#cYN0^1#)I_xmGjosMph{lA2cAK#2w%r&vVQ9B8 z1eQS!HZ}qT5;B2=kdUf0ma3#ujhR_F-g)?j_c?p_kG1ymT}3BDR^I!4@B0kr?6cR} zYp>0Nv0xkwj0LD!7z-<>a=mh@sfv~^v(m{{AD1D1N|!UW7G_nBU{$&6uCUA~jq56{ zS-YZ6khWAW6pB^SWKCf2O+7%YDv)qJcM_O=+NgGPK!Bq#P&EQv25O8Q7kvZv893c1 z-aSQ~?2{+wv(9JW!Rd_CbKt>w;&hVt_r!jmICla~AVwqx2X;$fhXu>1IywsO7{!Gg z6=d^aBZ_I~d*?}1UZ7SpyYq^KRLsjY)Lx^sBq|jH zijdq#JT)t;Vz_v;rH0)Hq*|V<^P%q(vy;v$3TT{=pxB90Vv&f5%4AeGSV50+)F_t% z=RqEJSmWo+9Ygq=?98ncwSqHGJ$Cs9gnDO?zuP*0#_SO^6KJnWQ4|hL(em()J z6|Su%ia>Bm5uUEZ>sp=r8oP~hmK}XQsxVe1HDu@Lt~dwb_H2;^)8I(&(V)m_H~qiq zqD2!uTW;O^wcQOM>=_M++ic1hN5=K3Gd(ZBcEyl)8C9ffRh=t~QcRgsOo0X!7~`Tq zprSUdxCjN7Y``wHAD9=$eq}sOUmx`|^067)`Xjw=YAe}p0v$kguBf5bMz+BrfTV~T zhDeRh5&HVKdxUFAVU_g8Re6sP>fIjAs%S$@A8{&B)J7R zblSrUMUZqwFeNA0JYh-_DMGbgaOHPna+%6gX2=#vN&MQqEr!HSLP7>{lDZgB$!=kw~ecn<(>^$tLlN`ipL=wR@6hPwg;%*hJu3_%1|$n= zhIy*aGM$2INKry%UYjOTW|=+|G#c9n*93U98(7cx_@Ou6!teUX`|5xBuRc>>_`*$m z_3Nj+a(MwVxd(9dh5FHG5`*}T>$AaXUOMi+^oM4(W`C^_Xt!>b_N!u_@-b zAYWo2GZ}YJ&Jhn8hldMZxPBSqsjE2IC+^-o<=uM^aqIpW?%&xLqIft0i3vDbpRzRyK&K0@AWj)Tp@>2Q%kXo0EcFF)AQUgUMCelwc5R z&RI!bIvBC+vI^RmErH!<*3811$ zH8o)u1J{qTczJL{VDTwK!qhB~bNE_O%~4lX&Gw}!6xL(~nOs$*C{!>RNleTSE`TGwqAOC%3;pFrLYpsLiOb$(&z(9|J?%phRZis@R=_8qv0UVtwB$eN# zK~qO7C22|@yA7b6Z<4N5Yo*l;#4bP#ynFNAKl=Pr?}?l;9GO~!=0c1UriBW~RB#iE zRG{0lM6jxPX-oBNsEPn4Fo9Zpo}*;uNI_+Dy$UomT?8zFEMTczld+8C80$Q}e{gR< zm-}~a-o13?${)G@#It{z3GC0^_JbXAOklcFZQw1Y$Y~KLS`jlX>6gv9P4fWboi2JB zGZ`G}trZ%0woXA5Qf?T@NClbULZhT3Tl!ojROI;S%v^p{uqJ-Ph)R8_;{!#=}vXv2O zm4pZc#b7a}MxQiR0?=)FP!=IpcW*ilHL?R-EMZ22?8G)?9aV;KZMB(yJxy$sQ22{c2|$#_5_QavGx3T z%Vu7%RmO4v2Qs~J#|2;q`Vo$esjLmy`grg&l}m7kVI`v=tiqSyMEL!138jhI1Wwyk z0TU~thD$CR5a}O78|XEiqUS79Lav%aj9^UU8lLaEI6($7rYoj!FBC3_n8=I&2JI_h z^+Q2x)LJ@M5xlmgIPh?g&Qqnxus=iDQ=*izq5Zl2!r=u?Y%M}1f0;+#Q;2D`l^;;d zEh$YFmO#nl!QMSyAwqoF%BT zsPrfl^+AeH_4_M8Qh#IqO6Mm^->v3_)cShEu|RHfS;5`HI-j@dU|i?N-}hVbjdwpE zfAlZ>>N+39r=I&LScUWTtYSosV6B;+RiqW8GG?}gj}wf+9vi7VB)6-TC0d{mSc|wc z68F!}@uP3Pi+|RabHI2@?Ca%SM*fc4aqNBDa``Wn9Q%lGi! zmyc9d)?LeWm%trWc~ zxvFo%6;(*-hSq8oYO?JiWa_B40Ewk_k6=oBu)uJog6x)r!Un3~Qv$5*c#`K(S7aub zlzXTd5_>b~^H3OR4`!m|sR9-P=V!@vp5ge~VSdY}p24fH-HBUoJp{4Gi0!p15-W)eR#dfLGr?{XrfR8SW{OxikU8`8WY5#{J(<8b9C-ZNF`j<% z66TzEaI)5|yAOH$?rGh<{{VB6y94rI3>=IF9EpgfRGGG}PCfg2jiJ}R$}Gg|QrK%0 zOVej7iy1TJh)F_YIii3Nk){6CIRIj*MPqQ0*exRt4~ccHy7M4;`()z5S>eGR+|R;( z1^uu6b5R0ZD|WlU!6;rmM2;n5R8W(SB&v{}d!jrcc7o&#r2Lt*0us<1yW+kI zSY3yWva+}s`$c&ni?NI?$1-hREcNS)Kem5Stw1cbfL#5A=%qPGuuOEy2|;2)l*T7tq^t2L@XB-X%a9QsW>luL6q81{eLzuGxw~=)~Xn0ttQ$VOA$## zNmQaDu|GLI*c}|NHCK5hi>=}2wXd@Sfy`p4iHtxinq$l96EbboDbj{_YH*ap4KieL zg=|I$t)pbkB4u040x?)D&SeC#<2*gNA9wHFI#}n~=~FL0{qH^h^85Zw=FD7I?XLg? zl>Sw!f$XM*WMDoN`q9Q$A}|=>9K&t&Gk#~%=vhj)M`?u zGh|B$q+X=S3a@!R?>_CI)|O0=!}+KR5!ec*)wiIbUkP~Tj)Fm(o^KKwqUrT+_3QkB zfY+B#S}I415WbF*vw(O$efE}XQu>*~Vi8YmpS`u8G*jTBc&lGp3vl6MM?WjsmUbcE z4+lLVpea=0`~i%_!c-9IN^19Wl(TWe9Lz{CCBxPn!)7DOKt->!L9M`;d$irU*o+r@ zLnzfzRTwr~D2+{SfUa1lDs{PoiakBgP{vlmnG|GgdhB(HqHOwnxDN+oh!sKg;8gXap;nh{1Ee zE5B$NGA5=;-|k1(ZV^0y(s%#3f zW`^%kauhA_CL!qS>RwIybGAdrKPN7AW4XMdU9*JLY^$JJJM$}_XV+93@U-6q*zY1S z(ZxrPVh6-b80`9=S(Skp7p{vE@ouk5`cV>*HQX_vHauZ;mjV)*5ux_4!j9lxqN#+3?1l?n1&XJh^)%!GNukYH5%wW+X}gqyh$;G zfQsajOw+9t#X_)$ofetVX|6f9f?}8edT7zMd{J_l!gPM=^F~}8!&E`0xFBlgkfvgr z-vr48Ze{cIgqm5!R{rSRp{lYj$aY>$9eyN1C36fQXQjt+M)4x%vUGY&M!-yK6ItY3 z6A$m7#c}|=^wRZu^6ATb>(%?bed7en@e)^#m#W$^Ym!wZ%AD@WWS>vr+y^owqjTk~ z%E7innNfnN=2LT~i05ELgyz@d9E^DR*~japXA)V(JNM3U``!s| z-8;dZlXIZ57LvQefrtgW!NNlI0waM+X0XN#=1?%p%2L}T%0tg4ct3>fQM7c zx92WB&foZv-&dddz`xFyAO9edqu#ysOZ?)O|7V=$Lm&?7AdYyj9H6iO6--D{&FD&X zN1$R?vHG2=Py_>=>bRsM4ic28*rTS|CiFp+TSo(M({A@v+Q&bDPzBxuwo8-RC@u@= zWUo#@yhJJq`pobnHo+%|PpG?&TYGbW7ZM^xrDZv;qvfd1_vdlv=BvDQ>z(E5)oVZZ zp$~oPcOM;%ub!Ts;laJz018W}#iKoMHkIb%JG%Pn~D;uM8txe=T}`6B69k8FuHU_QVNOGAnx*xpcG2sqp9dr zxzyJ)CEg4I$kC<*N&wy1K}+9^cZT3s6_;g9QT)Zb_iv5|C-+Bi-gx1q=YQM5wWFUr zKS`|X-uD}r2(VUUds@MyDGCTF;{-$uxD}R^V^x!A6EwYDisUMX4H5It@(hYRcg}eqcdQLKxt0ow+jSr+Ak1A)* z4odwO-LGTC2f7C%D`DFT1jIZ}7Z97V(?2~zDlT4#`^Q>d!Z2rOnZFiPZBFWNBG9Ft za^H6pF>7n!G~Cgx=}-aM^*O72uM-#OT*O3JBI5KEos{%~bHWS_Rf@Z%A;wykI2-`}_?2RsT z@)6!BecW8sy!=n-Sz8UY$$3aE$d;U0IY0oL(FnyJ>gUV?1L&3%K{ze?S&85R;G+FQ z;MMoi29`7^Phfk=?5PydP{Y8^i7LZAM%X49l;81EQ~gXc(S6SvK-JK~5~y5(7+{7Q zYCC)G{+~iE1sLO854`-Vmp}Wrenzo;_(6Tm3L0@<$E{qXa0px z^WXUAU%^lO=xx0F;GBnt@~f*E00v%qX2&ml=?=f|hu^|SUb=*XWmxr7-H8gh4*(%- zs1?@W~KK6(iO7 zA3%cYj{<^SH7XQFahZkVqoexf?K6J)%?Fr>x_q$U=ulB3I9VB3r&N?vnOd!>2FGMD z$3(5pp0TZjq}>gf164B!LjHUn^F?4Z%x?06(pL~+X;PVV1Jgn_DlZe6rSw5yC&T^U zXnPnis+izRB0N45fr``+Hde|Vst_EEkxPMt5rLs=ELr&_Pfu_28@J!i8|P1XffoP9hO8SL=I`X#MIe0q^xgkS~Vcyztv^~o9A~T_QK3{a zY#Fft$MWOlUc0=P-H>oCN2GAPjUr zt7(EN%c0&|ZM5~}IbiES_-3p5;lnqE=Z&%l;xq8Hmwd=uLv1JTVCj5mFWR}H?;XPy3Hj(0lQuu+UQ|o zArurQ+8d(#cD0_gc&F42bd{~io(heKL`hXsZY7z{e=T8X*_{Xk1*TbfkSvp`j5T{e z;|xY1r+O&OkTNHV8axg43oDZokr70t)QW7yMP10rJEjCo?C7A=j8nTdS-OyBROudx zVnwELl~fwU=q7Esl%=Tj6a~;`>Kks?$00gfIPJHrEAb*A!ovYRuC~%GgWG)bj@uz#mu-s zJ8Obt$LmLl&wcX&zW(q5{_4N+9rZi^>6h>$Kl(QApUpZv)Iy5@_OtN87Z36ES0C2* z{hhD#120^uqg`NTGDcxa%bo$oboF@46%775eyTqWn$z5kk|vcOsi6?C>=sJ=kmO*Y z1}di&|DOCJfXb zh{fNT!S>PY+^o#y2SrS0n09ZqgVU309t+$u|8#OPN;JJSdax-QX11J7=ULilN5^F8 zO|5}iR<&dR)~RPjdGJ!QIY= z_~xs(@zxvnz>}=Ymv>lp!MU!|zp2>3^3#(Vcq z>*O>EoT31n=fqJ2$HBnC7>Hn@1{Y4OK6TRGGlj_>dz=-Rb7D8Zlk>vSuJHQ3hkX6U zX)Ol}uMQGXnR8n4DLLxI5OQX?P^5P~YQ^LL0&4#tP&H(6<|?VEI7#R+0*RqRU=piy zZxvq(C^xrcnm;Y88`RPy1s{qtiJ}xX{?^n~*bI$;jV$?92q-K-4$N2~oz_Ywu*43B z<1m;p_jxwvJm)^|<;ng&ADlhpd7kq;6}A=v*RMVupM1}E*ZZFP&GpjL-_574zg)Q{ zZr}d~-hT6&I9Y!db7n5fF2I2p$0~gnk-3Ki{&Lv|pcsJ-6X?(vv8ARhYH39mBJ_Zi zk+?0uiCcB8#ayT|#u!A&PN6PTOw*HD0_L!EA?f5yAcSl)*a}c846UpJlNsT%go++& zs12Yp3OSiIV~oX-Hzp9l0A^uE9IS_$U=+ofZ@&H2gS+=`$K`96e)3}<{=~m{ba?!W zH4_i+-T@#ZZyhd)5zddi>jzzE0RgKr>A#b6-i5apU0^|~$d^;OWvfjjk0wYfQ(D19 z#g{4-zhZ=oMg3>~PJ2}i4(xM}qf6JY?oVzN9$uO2_y2+8 zquuwe`xWaf%+oz86YF_1tp(W8o~c+@T0UF)j){(c#vZwmBg!`nlARG}P36~+&J)vr zqdXM>O*DZqiR&b;LW#U?)U8+1Q{(*KFp1-kTxbzZZAOiiDFgWJcZ8+5wSVZ(RDe!h zgAxa&=$&5b@Y)K_MLj2H%z*E?iY7@2a-woUA}=sm7^L*c?C+NH8Q%BPMb_C?J5oOC zD3tnL`4n0&43^ukjrEYmSSC?TCW@3%o?a7Mk=cbLAdID594`V3EXxb%Atl3!j3kH7 zcYq4{0%S}6yTU1%e^}y&C~Pwl9m%XZGBAfBRmr`Kry=)$^jV-ZTqk@aYl|h8v6E zL)r0G!Et+G>}*v)u$ej&B?uy>@;z-@DGwHJ1ebcz9q=SANrs&{y%AxJgJtx69Xbpp z-QEgR`YQ}qY_zAFs8Cyhdi$Xs*l6n%Wy7<$85p+Oq$Rp2O)Cg{Q8YW_yvB-wzDCP# z{9XiXO(L-6z^ZyVrp(DO-o{T|IH`HBi~E+Z7yI3`J6f{EiZ}X|)K7*`L8$xH5BL05u}21mP)- z0CbA^;1n!Y#gj(2Syh9|{zS`#jcXOFC#AX3Xev06OHQ1!R*y{e?@y%tTLazy*hLwF z>7dMrs7)8I=0aADZHh$ar{(q1slSc-Xh{I7kQEUuH`Gb(j23b+M6F~&emHi$tb`Q& zm0QtB8+4XNLCf=llgJ^nO;%Nf2E|IXSkQEk62>j5S%mFPa!Pou5a+d4wZ$-*esT}Y z(OX8tcVaj?w|7XlC|tyw?L7j1-EQQIGQl+gH$0Q5HH&v|onv>n;Klbnj;Ei!T(5oO z4sN{rkh>*te7w}Murk?`B2n^~5QS!oih}#93-UM-i>CHf1|mH~lZD}i4$+lNm#tNM z7w2RFQXn`hJ=E{v=^hWy_J|5B2f$$P^5tDU@z@a}u3?>p)ANakr|0?b!Fk+!a9;Nx zp7P|#_zX?+e={sJVHQpZBA0EqoMQK)+R&dPgdD-I7v9in(9+#V_* zNUgaeFc;LE$(pPY$^8Vj@sK5D0IZN;x;0L%3y(R*0^}aW^EzMe<*hqki<9+y z?B@#eSh3&$&p!4ZJoUsU>%}L&i%&iAoxFDS6K!FeUcX9v0Yx(mxKF8DZyWkkL z#8Hhnj@reizx9`67Z+_&l_r!w-XE{a;M7gEiNvMw@OjM2JEV)w<_N*9SP8xM& z1EVTZIoe1dr^8|t&a{1xcgsxF{&b%3?%j7r1Rgx~?6ZILm8U=Or_N7Ladx_Q^*51I z-?*h!E(DVDO~G2NOxr+j`xn5i#;g0)wHI~#MM`QV%x7PjoK9G$#TYs}`rLwB^i)7K zi9RjmcOy!6La!pfWD@j$yDz(t#zV^g6Wn^`m0=63WarqAD(;L^BclJ)NSYh)?;>=e z*NaGd=HfsJ_qw|6Eag4uNxE#8~p@f3GK#~ z1t|6{%8Mrf_lBWVHKz#}gqUnad=n4`&LSvPO_4LRCE#tc=MzNe+~Bbi!*~LKe7h75 zfRbcg%#oOt2)RSbsKF1X47@i`4NG7yYUjXku<>dw0Ygf6uE0Z>3E%LX9)-9-Y=qw? zAyXnG;T@d;iq+{wFF9^FQ+J;UyznZJqJV|dL+n92BFyzuS#li&0H`pV5W_^mH~I3ou# zPgfKdc0UWNG{|i#VFhQ%a+HRRRy7GB(K8W>3y9KmM2wu7V+`QvV99^*l^e0il2OvL_lk7u7b#{PW8fBVZ~*B#HQlSG z(mR2g>^5#M(jFO7IvP~U#Kt10VWcP0BD^N#gXM}cNHdC&$)f$&67(T^Nn95eVOq5BC0-@Fg zxHeEBUr>M%bXb;exDwG)l{nmNs-O<)%dsxyR>yvEq>?ugyAuq>yIGoTDBJldMRb(M z#OUak3W>^w{6`9|RXsx++lk2$VINbRozL9AyT_$VOT7PMPvfcEC-vqx?(o*lQy%UH zjxX=P5tf;#T5U(=rec*F$-_WxrYu$tl35AVsHlmQZBK@I8+-1fA}1?G5t`)Iim}b# zVZ@NMHLKP=YUPZR^XZ?NH6m*4f>$o>;`+5?saYAA`>fNmGdwum;=m=}G3< z*;zb12P#+0T$NCgI2;R(k4FK6@7z3PCUCqPnIPxBz}N?J$l9=o%q$Mz7*YEup3Yzv zGrH+oOmTXe;*c#xWfUXT;O@4#@aalh_teHER#$L*mC39{_CKXRm(;p1kt@x_@vm%Wm1nzB#l`nQ%W*76ErkRa2;7VxBwNti011ROBtyJ`JsVq3AtKtMl#aW)`d*1h< zKlMX@|8M`BAA8?-VGLAamK?`MFsdS}0;rgzY-u|0{ekS zQPaeTTYt6vtqQQRLaoySmD5#n<>v(#E}eiA>4Y7Sd;K)9_XM|5NQ*WD2GIb>z=XUu z(lY-IW4id-CN9<2$@V1Z)bgUjuDY42gldw2KEsUBlN)B*diML`%lNChh{74}%Jce=0M9^pn z?H1DgyvFAmyub^^I4>gg;!*~>Qe`w@P$x-3Y1W7UK75oaZP-%h7zEZP*?fYNSR!q_ zr3WmeXDPeM0~g>dD{si0*fPat^W0rcx_8>otMfZ!6AWBz{{BjV9s_7EG$UT!;}#HH zFT;g0f_}NW4oN7)h+%snx;7p9na>z|5G9AvT2pEDNh1wXHj%S-T1|MQj2@ z$*11ihLz#hi&TYD)#PE84+Ykv?{{1%6G9ny7R?@|P3Kzd(`1V>%WkLGe4jQWfHHX@ zV#Gs?(SW?o7!&p`C|$dNyVV6wI+5E)Vo8tmj&!R_1 z3fznWR%l#hJ7o0VwQR@Vv?)VcE+sjTmMItidG!IO5IW&Q%!ix+^;A26*LkztTKD}d zfNgen2vn!T0gDUZwwp40OqM1{Psr>~>l6v3Tc)c^q7)A-vqFe9kaD(6Ic*yAbkvuO zyVP~9N}<{%6em=Aj>%38=SC$E6!G2a3Sl6dny~Fl#YEQx#(+AHIn-~h_(M5N1MXoT zL*!KC`T8KPUinad==sm^Klz^T$gkXbBR=`uCy<$(IL8=db(;@>p0vI!KMD*m*0%XcZc|?|MJ(>@%4d!_}QB{SioIKm^25l zo+qAp>Il0i{x{$A4O}@cJoWS;=2{YuI0muWg#~r74uL|&&dIl`{w*X^dP$TT@Mfb( zm4&3zRJF}s-Iz`aBh)7!(i1Dz$XT;4V&=fC6^u|D4e8=J$hz1wI;FGw(-qX20T=Jx zxW64BExyW3Vhz?#dv**fk_nXc^$e+WCiD@QRnlFc@{x?MhI#X@n5)WN7&52fq=ZD- zK)t{S%qlL;+7FMI*Wv6mQr{LeK~y9o5vJb_V(mMwM%V}^0P-kM5?GLCz}5fE!0`8E zg-mr7UbS(M=#35JO%W(cI{ywDC)1B}RxQ4}^vg zAUSiXs*H7?wLeeJT0?uaav3}9M)2_~SMc-`#BLE=mn-UgU2(oooSp4)vR_#%aPQ&1 zUcY&pH}0NP1b7@{?h!F(GNksV;uf(IAjvgR0~tF>b?Rha(Z-27Ujnmg43ydc`Uo!o zqYzm&%MLXjf5?;bJ3KkP6SwbwHqX|Z zk=WblbHt82GIq6F9*bp(60KHMS(&+#98Se8d+XA>fRDO)JUUjkZZKV}5}GE%Doq;& zWUa-BF6jW9iD5K3ui%-*q@!)A>cXCxLuue#T5vAgBvc|uAE|)|6`Kh$*ya9xPRw%6 zz|P>ZXc0|_0Zrcv&z&HuI1)JvF$b63QIVA=XAf}i-rGlK^X&Yo>(Bk4UwP`;|D3zA zo}ZoLbbrbSAR;PD)qQOFlfhjl&7Dv7{&)T!-6+lg7ft5SF6P$$rNmqJO8{b$^$|g8 zwmp<%15o8^2_?XN<`M7#r3q&ow#uo<^V4%&z53WM9~~UM@y6RdMd!FU#;hlHiV%GkR z=U;j6@4tTau^&D=d5F`Kb9e-VNOj+U3Q^-%5HCZNrg24QwY3!@8)O>lL#Rb9R3V=l zZGjnJY66%3Y&zd|3-d6(ss-S5-=TnTxox%$ugw@@Fw{;ERJ(8K`=v;j7vK&=<3eAm zVwP6(2E`?Bh_xj@3IYu$r3%H9eDUX-i1e`@=q+CaD2 zVz*`Vf`S3N*(|d+)y2RXS~zsCNLQv)73;0;?Tyy&xr^A_vsXU35eR(-q&G)K2U-Q9 z>MDG}RelFD%X6}o16#SfOoAwpt{_Hgk&96r+MrFE_!VR{^Cfn)NAVV#Olsy??{!dS zGBY1PY5J<%(1j5DI)m1SxGV`qb@dlPdnPm=lPh2QL+KWfcyW z!OE&1eDw`};_=7vga7%jn(NH?v7fn9hfy44O!Yvq&a0leeuzt#7W{v{=a*46^Vz2_ zN3JNcwzs>Ite7tI%HhzL$M&WGX1R02aW*i}J{_b?1KYwg5bc7g$4a6wGJzpm)Yw#2 zs2+qUNMuoJX~XQFwqkhUKYR(R7**s}<%5uc;d4p5drW zNa#LYVodD88kA(&As($v(|Ev2$!m*txQr>aZBNrunT$o`5Jrmu6mp?`AYt;vm5r#t z99|_k75Ab%V;Ep3@KU~ssw2gU$-}Hz#Zg*orR3a3z9f610D+lS);kMup5`-axKkQL zc#@0W9uyQ7qSThalCqNWhDT&FWmZ$5l?$`F3ArM5+K8jm0XHi!W|>g&ElrcEq)lhi zZ(fUs6*?JW5<{t7`qf1OgT33L#cU99nYE)2@Ix)=;bjLIt9 zyMIpHD_*{GfRBCKbGUo^VZ8d~Te$V^LmXdTaCl5C8Um-(t8#P)hJrpVM8QQ)fd$gb zbk_&C`SSER-mJSk+cff!6s(E~)~-sf`~`8N;ys6i_L&4D^)6Q+5jF^l)b1iFQhv&&HQvC3Dvb!@F>>G#8Suu3=sFvZ}6M9-@s8F;5U5hzmISG@c$7z9^!m| zk|+Cj;?})y)Wf$w7lnr?0WOT#QRL4NcH^?Ldv3N-Ajox!iK{GfSwi(F!Fcq3wfL@L zAr$&hi>pS;hs1cb&d1IBTdRxo>&pL%nz95#{Bpin-gDv#DV|EoY8&c|yO0AQT(KxB5w7aU)U~I{qi6vngK=`1wI$$ZpH7^g< z13PE$(crYTY!ShSFMxtO5jAbbW(Ur*B8r(!-L~JFyr>U z8};D+-Erv{zwq44FaERFu3rB9>BBSJe{iDwddP=Hm3L)?+!9?2JG>Fge};x4eUNqu zy8xZqO(47a?A#Qclvy^VdY#n!^Kgg>M>(81T(R7A-{ z}xW^DJ-Bl4x)B?l#JJu!5$>_V>- zD{kv0`ZYGWfObA;cG4=0?&VQMP4BY%dFTz_uCSmGZ#hA>F=NxZ-O<}g{)8mU3LfF$ zpl9rYrjtTKPtQh#l>AAoXFpS7q1{ZY_Z$21sUIr}iQzTu);x+iomeDjuh^6Z2>VQA zOMP_j(mZdAdV*MH%H6y;khNOsL<4e_0`;nhh$<6G<^$!>VE%^DX1^$NGOG2!(~L)< zrV8{`xkZd1hh{&kZQfwUb|L_W#s*pa(5ORtjERhhK&FI}sfOJdQ;y?>h>Yam@HC8p z^SnJDKYmZV^!#`7PybK9uD*El7x}Sg-Vdy$;^QFpGO91sB->d~eG!SN`STKYR;5J| zG=b*n$%?&zW*;ZwV7K6GzsHZf_BQ{i555QA_y78?eEqBMjxT)W42L_gl9;9B{(O(T ze(f->URm%LzxS&+J4?Lq+;M0OrAyLB6k4(I-PgQ6P#G1giq0 zfoel@#eHCMt(akHp}Y7~$%;201~q{rqqxhiHgrX}j0I@IBS2~t6kDFag@w6+gF>M# zK$L++V2V(N>*Re-tA|x_)~tx&)j+8vTU#PpkjzI(%p~0Foz%9dE(ZD1=c1lB#nEt> zv)iHgqPAEnAUKiOHR!jpLQz%KNyxoWU*ecbJ!fu{R`i-2*a8Dt>9j!PwAP;&I#NWv z^d`l+cYn|EaK+^-2l&?ScnR;mbH85y+AZF>eNqPphde%75DN<=GgVoUjhS;R?FW*n z7D)ebBwWkA$dxtkLW-br-%^WF7}TVXm=rTb)+jajX4D{A zd^I2dI5TVL*+yE?NM{v5=saV}PF7Jv1aOMVQ>7oSZ0r2r6o#c{FeI3vUN4bPB9v0qo)KP>ibR&KT^k=ek1gj#`B#M;mxzjklN?A{$K^x z^D{j6{CofGXMg%1{%7yre04ne^m}W6dXJB^6|-{aXpb!zGEmzo$IOyPDp{`7$2h2i zqg~ZJ$DO6g5M}BLyUl@V*M0Y)qrcmq z;n#1%z`3-Cx(Y?AH6eX3!U;_ZR6`=?^)5Y@)Utr^5H=&eMi_vcvHecdML`fqsTNjW z7pm{qS+f5vEH@CGSY6=N7`{tU{@<^M-9ebgPE` z-Yj-<`>a7C)75`)dxy0}k40Fbv_?!$VQKu!=n9P&kJGo!TC9`fg1Qc?jn{xoB_pbs zHsC97s0)a>fiNXu+Vbf|I(+Qq#4p8C9*wxlFDAfySovCt5HTyMCPfUhJ z<`F2!$x&nd`q$%PWpCuPR(m#(jpk`!^22sDS?G0u2Kwyl)61L`^`z&n>smth=T(92 zwOgL5luo2V2e_W9)KP1i@xo9^rCMp=tJmjJaXPrdOo{KV6*)QBTYPT`(e>YaAPBG@gbpc=G0>Rhyj^{!LU zP2?ORp^~`iYHL zI6jE^7yiFr88>d9*L&Y{taANGl=-o;v0P8cNUHV*SMpU#PHf8~yaEiUszK=%>{P*L z)vQXVRkDLklZh25d#TdD;w?=eT=%NYxE`813DgL5p}usCXqMiGc! zXBHV4T0xGi$*8CrgVlymZ8F#6CANx2?}06lW*{pQ!!1ARnjlc45IHIV&YFlt{sz@( zv)YkHRY?I~v36}<e zf)HjCrV0Z-Ak0G2DuPNSISO%AZZq}~FtDHqtK5vrlEIkuNWWkK68#gjzfORn@uF;Gg^ppd1oJ-m_sSuc$CNaoLCS99z zuM&O1=~KS*KP2znJ;&)-@!0jtxc=lNy#2;KzVprdb^G>19vm*XbiBi|bCGtcFTS`P zZhEI)9g5zJ7@;JnPfhB~F2t^hHN+2Q_`bGO1vhWbazrmPB3H zju4Nmt#EEtBizHB7%Q3Wcuy&zIjtKHaP&XxD?zY}lWdL36TKla)OKlsVtcI~mpKA&^N`NR9lUkvno z6p18rIZg#0=j&Jb2G`M>3LZbaIY$IeX&kDeq^ z5!>eg-9tfSO|EBaedbf&{hNO7^Pm0k?|STovt>D4=J{E6CEFb`9L$=DVvvm+x|>K@ zvOK$X2fM0u*6o`&mv`>Jji(>G{<9zX_(y(w?BbiJCuf*E1v9W5A8dwmEJ@CLq^)9T zZLPYk)I}4#$`-vIU7#zfYVW+LJxWY>=)DS|H!& zWL?aNZ&=#~2Q9(T2s{D7W`9PhisG|afXZd&fk29z@w+T%E?Sgw2k_f007J$fqhJ@M z@(2$7b^7G4910L!eN6+^4G8EVZ+*+dSx+f}(OTBR2v)npqR?tyXMhRxpwk`&m_v2x z05NOA!^O6RrOX7|X4XdN*Vb6KX$S={plrL2?k;%bdWlNezg)O=5^e5>ESXuL>>IFE zkRW9Uy<>C_>}1_^b+?|Q0F4Y87vznfFH`+WO8*}TELot53)rR7H5g4PF!`a->3iz? zE+RR#S=eTj*oXriK(?|kZk`1iyJvbNO#-OlR`1pQVv+r<8q+$!`oQU=4`{}(wlzWL z*>V+9WyTl;BQ0t3e{22GJw}lK1`D&xlzP*9&a${W$OVPHzl|5cN{`M_XAU9E?)EP-S)5p!sm77MYKJ3o{vxib+YdSVgP^8`blutPi< zndL0pd(N%?v=TuAMMUjlpAT?&a0MTG?>`y;{SW+s`iHOm1AgnX@4>>O%3RfZJOd-r z3TFX?M3kmol+4QlVTpqXU@@K64grN6BO)^=1|trS2fq5&eg5+OTli1@nNP(Z`By)J zpZJNl^X6^D@lmKDeW+fRE9&v9hw<3u$p7xIeSO@$v&Z{hzKon?#ViVFs?sO zLrIR1_CRxjORCK5`O#F^v#^$vg;BBqh{}sj3-47Oj54JQs#2_8oRTb9id$`rU}jRs zI6HEOa`JL%1r554tVPxZNyJd5Q(1+jK+nm;=5#?~6wBraX%#OV%1kxt_xnU}jOgU>)Q79ikEkW!jIjeBs9DT3c6OjonRwJeI51SEHj>)QkOdGS90%{PexEQ9nn&qI~==+Vt)ShpQ+1-SI58hyMCy4 zb&Q|<;$J{4$2>ZCJXE}KZib23p<5H_^aYxAQ%1-I%?*X7;?0~H9MnEdqhYE*;*O3D z6uFTmtYVugBQOmpI>VBzzIAq6O8{gB2jO+-R>j3ulf3F;;-*$?j`5=Ape}lYFP#bqV~0qyZ7F#yASW+Aa-B-=tsZ(HyvL- z{3F~`?Y zgf8|?# z^Df5C`{xgVwGan`IIEHmK{~8XfD7!6qfKMMBBqLHU2v?S)&e7nugxO1h?}HePWc2icyuEH%us6{CCBzW2J~ztPcOHBSW;WiHF&WV zRdtVkuMRv!SL#~^*HKz5S-4H42+eRG+$I9(x*f=RwK<7Qm*AA&x_I5+?{_LGEwO>( z>i62J1s%!Qo{NJ{^}2By&qT@0UC##zO*Ql3hh9Gb?P#l=%xy0K*t`osV4d_v4H;)i zP2}2Ba$Q?0)%ZDp%BmrUuS8WuGP5LarNr4TdQmQWnlVij5GXg~inNbadV!JQzCtBA z28UJ3;*w+}!67Mu)DM#L+hqt+!bpj^fShWuM(f@|?H-hUji}aL*-kAys};M09X|Tr z-!=c2AN}{o5B$>qT%UOQeH_a%*ZuhhS^S2@B{v~#>9eXN>Lz0pau#E-YF5-@MkW(c zyP7p)7XvYf&%SmSH_!I?-aq)O@OyvP%lRWe{zg1JK^z=Iyn~2N&VAwdDEQ(t$M_%q z#@FKQcOKM7-+zsn7VCpd7M4U!%1t5q{8IS@nAvA5kwl0XD>5vjfg3`dT<+WO+XdxG+9ap-3YG4Q57uHQTnAKN^d+VfRhxNA(|sg z0^_YHwOVHV2+kkEwXW5yKpcn0V=Aiw69C-lHnJZGS zR&9c%-o^6z6fvyTnkv+SGSFADIDlxANxglQ`TF$H#{_ zI9_5RF=YItcg$kAP1^5G)@%V2%D|L{EtVqYHaP`ArUvhA=WQ|V;!%lkJsc25-cFmS zi44{b+H+MwjR+@~g?#R*tMxa2=$HB0+qdz-7oV#AJO|asQTqgKvtQLdAtJS;!a-uU zP$s&oyk;KmlgUw|s@6(7{8-@tU`u$t6g3b&d*QP5%Ok%9u

SXfO=gjxtY*LB{NQ8kjBowmb(JXl~>EkR2b%@CN- zBe?)MQ}!9i(J^_;a0n3~mg7rF`iMGI7X30wgDJ~JN{eVw9N>dj_f`^XWJN?-|IDy7 zX~4sEkdl%mfmn|ZuVuu3?&n#)b?ckQcW=MVXP$ZP?|P?FdJ7yW_;N6rOnE3EaDX`iU?9^5=i>^uft< z*RQ{TkAC>$I9jeIW-WW}4+dDvn&tB(Tzk z&1H101Y4h*%Du%TxgB-sDz@@nmOtN>Ece4~hl*24*o2TSYm{vd{s!cj2hEl?IKnc#g?X;erMUmg0*2IFoZvi zd8~llo&!zy$Wza81S)3A_mOT>(DhI68Qr!pnFT+M7L50}6|-Apu#p4($RojQb3);U zl`;uL$y-GWzKg5E%!^oCzlebEM$*lJLQ1dY%Sn*K4^%O?`$CkLPA4FeEwrS@fMY9p zm@0=KlcgwnImIr6G+&|zfC5+-OpFf@KMj8t`?tc2*2IDfi=;>W_m->=UIbm{Uxy(YUz~RKzgvPG3Y?$ zl1wWmyso#CZ6jr%&Fh4kaE0H&i6~U9Ky<&Y$~!9e$mw*!aCr6VHQ}P7gP7d*#NhOk zF=50^AlHF`;@-iH3UjR3La?%tZUt2LkEQm3ZkB7?@oS&T_XR88y)9$Mx@3AP0i&h(;-CybDHF-Vieb8?Wk75E!mWFRU#P~ zp;2m5u!bBCn1Nl}9A&AoKQ&Gd?1j##i@i38Tm|-m(T&U~5fSQp8;Q!UT@_h!O;6#8 zSk=?#)VO4ZW?fX1CQTSup^&brr3?=xFP6uj8&T4XoXs~UI5fb`hVz_h#H}C@Dx3r| z3WK9XFRECM3$;# zs#K(_g^)qOPJe|_WO7zj0Otfo%}Na=hpi52wL<`Qv1GL!1At6+Ap&73q?0#lrm2-M(>xld;F8Yde0+uYMV~-hEJC z|K)e<&h0bq9-ia)>aGsQfzQ)f8qOJ|D!+~`jhCD?&F{-5*+uLDt#GL7<#B6ZrfDmr z3g4=vl@n2vWcv*Zhd(5zhrdlS2JqrD*XqZA?sfjct8e3@FF#RdauL# zK9Io!3{5wQ8Rvx>SyA*nT4W+eL|wY_Bw{(pySHxscR(z$rJ_Lk)M%&0}zds|B^Si$DyMNWse)e-e^2IOz)USKb%OAz# z*PfV51R?{oaxGRJA7~<0EHmG^{TA=udFwcqQ7^poo= zHqJ9B>{bw?c{=Y_M&W#<+xKVVTx%@4z&9Vs{UJQpm?P9?h>OBv{Qr{>Ra>6S4NK}Q zXk|{Ci0R*Nf`26tZiO%beF0j$zLncph2Y{Rp#ptAQcwqo9w^Re#zQ3*wG z25m(X$eA8)T~Ia?R1rt_r5X>FsY}XK#(ZsbiXt#l9{g$o7^LS5=`bEe79u58CqjN^ zF+;|IW?OQCOHc$tlRtZW^aH;aKla7{uD<6V{_*!aFx5<;B@4_im%yIs|s4DWnfG#!~7y5UVRL zh@0>Qu3v9u1M~7VkW8v0H3(J&#?++C;PkmxBDQsCx|_+LGo!*ela4$Yf#n#8N=bKu z)}%>65S<1Q0xMdrws8sZ8Y;Zz$jJ(F1gXlG0>1n$T)TwcAKa5TRSI>>ud)UPbBBsq zfi>5>b#uk;?m4esIpDW`$IG~T_mpqFdb@7Cd&-A+SjU$Rd3Z1sab5l&vud(JPmdN> zW^LXi?p^07a7M3lkcAmU&6J=Zh#q6C2pgzMX&myRDpeuooT1jo=bw8Fpa12z`9nYc zHGK4aPhzb^y7UxPsG;p%ey-&TTCT+j8EwQ2L@-YFl_%#w&BAVo);gC6pWS3Zqo+}U zNfd{owZW3sKpX4KngoX`y9^?*+DQOJ1Z&RJWOqP>MnR9S6s%Gdh+@$0;HX}G>*sN> zT;ad`TYm z;8u5l+gv1)1Yy-Ul}{*n-O}i2w-9FqN_3@4E{B+9Sr2yy84=^|y&JrB^UdAdKRA2g zJ@5aIKl=Q){9n)aGjH9xt&DjY=t#O1@!a~^nX@IHuK%@CQn?tqJ)*Wb!=hwY;_>U3 z>u?v#Fa6RN|NNc1xBi8rgDd#p2fk%JI=npQeiF!|!n{U1GtG@7ypZg!b^4b?(eCqmh1|4IxC3n`!<3^uTpx}jdW}B zqcsFwJn7?JJF{C4`gf)xhSaSa3E*q}?$7*ssGO|I{rZTkpFRH_1ipY_vclkYdp2FI=Ed*(FnVMmyCYZnZnP_8_pP+geETaieCevK zp)igz)T4ssZ zC%Z;yKl$+dBq!D(=Zn2-QmfA=f5^SR@r_`-kx8?b-4ufPA$A zF)G&;$47w|pTAZ=@I$ZTCqDNMKk%W)_0>KhXnLPa$+!JfW2A|trBPZlU~E4KwY*5P zFk*yYSduc*$DAv;gzC0FvXWJ!{dk6I{Rq0p*?dZstwj$eL(6_Kt)i*{`=t=-I_iVYF+PDu=ebIAp!e5)MjE@O2v|kd+=I)& z?@l3q+wjOq7U z2yNcAI^^j_c;#FY(=zd-!*L_s`?>;mvye)=%?rTrvro-YbJ4 zUK3Gj;eso^g({6DP^?uxG_sIAf1tQ^T?*;3e0330VSyggE|LkZqB=G5neQw*8WO7- zUw%r%5p`2Fs>-AcB|yx92+!gj>O>1k3##ReT{B@Un;HNplA2E%$|NeaS&JMU%ScVe z4id~cb1XX?9vopT%e-^$-aOX#E4xnn0FuC7!OYFpT7LUOUH=PHw4STPO*p`~tu{kF zpMPv6f>dG^kcydYjHm0=m~K@(RJ5y$&jxf|HZhhr;zatMHjLDj>**G`*(!5oiapr} zkcqlZaQ!dge8*Z>mY6DxXUB!=sM8q7@1|h%&c1WZjRJQPV`9M4Aeb63eia@Rj!b{9 zH(U1rI2kHWtN_}(Y@!9v!kRvb%7jm2H06m{8iK+S295^qmd8gbnv|-f#-oT7TRP%m z0qMc`0t=g@B^#h^3+-ZKNmhLUZk4oX%z_q;seon?&BCy_El~HOqE~n#us{Ymnz!ti zU~E!=jSh8Kwc4P1*BDf*ilc=rAfY^~6#~_f+AqBGF+zbyXJKy$0CPODm`sL*4*#MO z!`a+}(G{AmSQ3IUQty~vbFhDb(&WF-wjHQuTD1hBhj|OmhW|DKcVqU!&3b{bi zABAK`kl8sfw()}(seyY%m8q%M-MGeE*MEf0rClg?tjb0Z-s*NFEk6j+%{D;^*HqY~ zngW$`4{uL2TvKnoPu5gwl+98LNzsGNbsq-Z&HanXYaKE0`;Bgh!Ky~jx3fNb(;Gj3t)b5n_RyGY) z*vuP{j1e=*O98C4=l6f*U3~nhr}){w_)p^9*Kg*JfBpflT~T6^2r?>jf|bSHF7Te` zujcpty>IZRf9@7O@S(?*+^rN+DMJ9F)bZ~!A3=G17~y88i=MlnZrj1#NX%peS?=vd z+AF16ff6-qB68Td27zePKTqmP&0!}5dd0jHBP+{QIM9ab6)t%D_)^xY3L^lCd>zPZ zQ~A05u2?maI>nSoaKu2>RISRtqXn+YsFhjzpO&t|56ZkVxdaqI!G_Ua>G4GYFc<=EmTA#H;bi($$bzb%a=%#pj9jxL#CBpI=&Rxv?m>% znz1Pporx+Wvlfm>$Sqngi}B;i5fAOMFF)*aBOfuuPDrUqpwOQ z+tz=nA7Yyf2kEoMO=e}gC1*EuKqqF_g=?SA>x!!CQEign(>jSNB9@WGiu<=tz!5w+ z8hGWyPx84}9>dLV+{c@5-o@?vRR_nxqobpZoRFm3`4ozSIts0fuFTPp$T1y|UK#LT!Ni(-lu&KddE!f8!tgVm|ZKV{v@A!|B;{ai$(wf0Zfu6G2_y z%v7_LOL`CYGfww1RdqRA9`c~cvBU^+2CzdSC;Pbd9(0<^Y=l_`9Ypmc=HOP8D3ckP z($vHVsR^}M5}6wDI-;ubAP#tcf0N&Qcn|;LcmEHlQ`YM@evAjZXHlF5x%W|OCY47u z&N-M9wMh~wE|{Zv;2z8wpho;r*MV$4t7RD45Ws z7rib;k1(Nz66fKZQ%!AU;a+#hk;!s0Nrxy>y2ZHvuf+%q#Wy<;Qck?CSh{jR-lq zm=|V&wIr5uaul5M9f9ti94m*sc&XZZrKe1V}RWD1EDNW9o!AP~rn3@2f!sbPrFD9U{ zHPIhYZkZ-%LwW;+E^U~1%iId=N*etKfo?+(wki{W-QqsQ@Ib4}DrrFAe;hP-MjgrgFXl7rjHpM^@|MT)8jWtHLek&A{Z)WU>t#G^{O~6JC%$ zi2z1pD31U%CJgBQxM3rEJNSE=)LS+}!o3)t8ogDXt=~`uJQFST2L&MP3FDUz3>)e7 zf)XMORn852&=fjEBG90lHfopD8f)xQc@GysX9qj4U!MnyZFEhzqP=O zKlxX`3(s6SfHlMIr2)7oDgK6}j4Uy*5)r*|(`K#OJd|e|E*}8* z);)go)f@O{KlT#-^8fua{H0&Ii7$Qq9v{CXF{X~`RK+P1SB|5ed+rLp|9^coe&*+I zzuM>JzJhJNs)lCCt`9Mr6;L8Hm6MggiNS3P zI!Bm<%n!DGkrKsh<01Fgb7IXsxBTu3-qd> zNI)lC!K%rQG$k{BJ8dU`1+7fB4+KFsZBz*zi6|;m4P<}|+#r}!bz16jRoODRnw>nf zKQfpc<<32&c9K*^bS(D{k@vuWjO$avJ4eK30)PT{nrDCd@Vw4XCJ&ARFMjZPJ@>xr zc=xTleB-s-xOevyyMw^d(Lr$ugxxo)5^HLAhv-+bd;CS+AZK;~QrL4&7nrHheXBB1 zk*c15H-Rfx7hJo1jPLz{U%=tPE}ne+7-whaTNVMTHBv6D=Jybo(}^n;jQ5$mw@>5> z4pq_!FjJ-}MhwX)norYi7zM6ILZn#0)^D9t}fV=nvsToHU$|>q9|V1 zmQi%-(r_CKl2fb#++^V2)e=!_4 zv-f@AL%;8-C!hSjd3KJ|2d8>@%MQdu)`Er3-`dm@YFmxYN&O_;8|J?64u+j@E5B2z z9$)}t>~QJwW&G?|P?9?rRTg6vvs0aX>1RU=p_z3jwNih1q> zvPFF`hWgyb{`$2i=j*q=@-OYr_WuqsZWlE6JF-yiA3~9&4TK1y=3>DqHs zllrY~zz}G%l7WdDC=lo8dvvs61n%Fstz4W7wm=k81u<>J=d&qiw_gX+YF(wGVMUaG z_K|!!r|BXnErJ2KSafHQ8=2}Hzk$0-?V8NxW40^y>@__lN>OE7XY{OL@>ky>VRHzC z`fM(*T9FL_lW1sVXs~ytr-fCavM^wkzzi3AJ2x+#v9kxNHAZprXVsg0w_FrN+FY-T zfWF5PCXDH#HPHns{cPvo7G)yfM6Qf+ymr_%T6ytAx{%lCgWNt-$#!n#owB*(R&b_F zcVh^JIZsv2VqCY8I{tIeE#PgK*d(}@$D+J#!&AN~^%WYq%i*M-yH4ni^crVob*DhLDMSZ}bTzdfQR)oWFaV@v zJ?WFcUCvo(b+J;uiGEx3Dy!Ywv#xsvUi$C@~AL8X$@X}KsuFO3$7&U8Ffil{)V6QT}vgUKT4N*)xXO(Nt z#X-F}nb2H03cP*)9RKLGTljZ><45p4|JirukN)HheD&)O>+ooB5;ZkhggMFc^WxDF z`NZ{OeE$!=%76T`xAB4ZJys)vs|d#`s-`1}FMS3VFjTt-fho40p1Ordy{HJP<5c_} z0JWwEhj`wc=1fW;KUrmmWkjg;eghS#Asj68fT?X>wpxK-zABusTdaD4RYLD`q4$rK zFzh-7DGyt9dWH=hw-Ogf3?NzZ)ipVVM7dcv2M6}6?C`|n$*k{U(@_)P7FFn7(g$iv zRp45B$%>uG%FINrrCi3JlQm`9noWb4+%2Uda?#y<7kta1R{nMUu0FZ#7DJd zg;X62Si+;(yIa>)QEww1YI=Azttfi-i7WYce(Wo}dH1YdeC8U?&exK;WeFVA)^5nH zW5^B-OAIhr=WEs6PgG)zom&f4fDdAtXKGrnDn=@guB48S!VW~Z3a8`52~8>OCp_6J zsbW6|riD37h$I9!s&8FMIRUH$xr>7qbo<3zwiE?`#3p2M-BG~KrG&( z60Fu`Dfd7MB#CuuFvPZU+KON3*8-ttQO+>%4O}|B6kquHpZ*fKd+L>!KYq5uQJkKf zM6UapDyRD&n8%Iw-Al%_xq!nMGaRr90F$s=6$W{b*DgJUo40TN@!i3Y^4Q6;j0{~B zcFw6KRp6*c?qzHl=oC(O@|N3;vY(N$wHA$XZMFX>Z_At*V`mGX71PDHbdnO~k1fZ& zQ2>UW+x=~!YYb4;UKb7fxsL$21eRslRJ15Lz|;RPfUOAD9ud>`80?O=Mr>Wo^j__D zXQI`2YRh&-yP1-r@KJATYY9O@E)EWL941o23I{kx99oN;7uV=QT>m?DU>b{dFT3)R z&iBjL;>T*^VZk93w`I`U%QfzZ?^81LqI|eGt zRH~wlXkw+b*@GI96Gd0nO+=K`6x%**`2stciVZkBlgI4k>4)WpF+G3dI#pe?S;MUz ziI$Rx!ENSCg%~Bg+xql6t~TOJ=(O{*G()0Z9Q5j!VyH#xp!cT(N=}7qf%O77G0h70 zgG`}=3%1Pnnohd2_hI!Y=MFM6d0M~9wM^Ay0ht)X-_|V;O-^|I!)vQmA~K$; zZR2S^vcYYZTRGa#L3>}bm3@?ZjY&cj1{eMJ!-UJ+t}|l0L6aX6V-Xc#R)kHZJbRLf zntG5u)6BcDrTep&S8Nn?mB}7(3pj3Q<|4dOl}J|jR!q#WfLUWZK-4FqL(Xq(L~Rmf zM8)Q!qMk|Z9^2MbfXsQm-p8{~eVR`^_Fnwk|Md$DWWDdX57vI~A%Y?pheNh5dZ0=+ zWJVG_!M14}76uuWC`^{!lp})2yTI4pJ;P7Edjo&spZPfclYj5y_(wl}Gafvcb#%Fu z+*P>ShE;3g+SS1GPhZ8~{(EoZ=e}^iKK#CGg#qTY3x%6z34Nz6$$+e^l!k(X5EM`- zCiP-rNuN~hAv1b{BimN4U#~S4|0-fK`pxr(S5-btPD z5D}5xezVnKi;#C2Cmf%LE30K60{Uj9Z7q+VQgW@IkOV30EEa>gKb@?iLOEwReGq+G zX$E2S{xN~-kp;zJCN`ZiS1e@32yI-etA~h^N&-0{)YDdr71gSr!m65(CJdSRzuQ zR_7DJHhpV-1fiOIVa$|p-!XHt$4ge3azGJN^Fp<98fX%5tF&CQH;;;%Ixk@nLQqT` z)EAz`0#^a%p`@idS*9AP3Q3%u?0NUr8Bb2ueB!w){KU7tI6wIv&)1VrUB<)vC%Ey> zJ)Au_H-ZK*P&1u`)^@6k%gKt=qDFJM?6DW>uSq6q-4_m)f#;vOQa|z&uk!1!->vsQ za}E1-tsYXZcGn5LH8aks{(V&NfCAR%`;3z09V3_~SPHCKWVjj&QM(~a0s(~B^4^6x zm{n5}Os@>NMkt;qRXQ4po$Cu?J)AAWkU3Hk?5frsIqN}Chw_8d*YV(VkALnr{%ySV z<}YAhH!*mL#0o};=JiR*RNGgp6-dROtB_2)QLvn-!gi3=R*Mo&JGPE0#zNT~A^3_Ic`8_=Ls3FJCqX_*;@SyH0jS^ZQ{4>MUgnVvfCXU@2>9-$H$MBmJBv@c%+3^|95sHA5mP99D1ncPPz4a zW7}ob>y;@(juK_!so%Ijn_h%USD>oy@Jn(hS6=)aS(t+uW#HayW)5_sgYe;1eoR3K z#Sl^yNDa1`xP@Mtm^cGlvzfY+vAt~HHqe!Q0h$d!!V--(T~J$VOUxCNqUZzF;ILmd zn_X>#rqZ)a0|{^Hf=ud^DJ4c~N<*9Z_0)lgZbbI8?nvsSS&R@bs&mwP_f$A<#RNkg zi>`1Whr7i6IfdcPFCZ(j0BC=Nfk&>(*RP6E`g!^J_It~z@^<%>v(*dRs`lq>pR}zP znRZndD>wEotPl|x4{r@Wm&9g{rC2gOJJ=h_RWUsuk49lPpFjsYo`Oe*1G$!K1`{=cGbbLqe1IpPIOJdd zTVKO3e)S$d{K_>TOXCAjY8q+{|#%qtbbKYFb_Dtp9P^#p6$Q!lMy+B?6 z3FWQPr?Lh&)aXU=%d2OJ;$YPCwI5{gUQb))Hhw?laphacXt*>H}$g1+3Gp>q) zB-RmFR_OPoKo(M{M2%)U36IKFLyy*ctMb%7LQlT{Fq&`;7Hc957<6Vjk}(kJu1ilZ zQOU}0QZY)8KM63@n=Za3`s`G3{dtoy~rGc#mv+k_FbZKs7;`PLz_+CH{Gm9n5=iQ z#$xw-ni){a7U8pM0cxtGYsD8wVVKlzLJmHX8Vjm#*EvTLGVM&T2K8FPG23C^6*jt8 zOiAJba#qfa2X{{~7LLo8cKFaIpTT=S_(a}#=Rv&n&AYsH`(XuxmyZW_$GeIEYgWw( zIvI+bz*x#@@*vk%ElL6p2392`c;d-peD>$x;!pq5n|SG&tGIuDTA6{22#D+$F$7A8m-hUI7LW3GQOnBEX9CEZjRq zPRWcA%!oZj56gIm6gf^rRWc*EC`4=4Eq$vPwJ_%%Z{2^h{;5y?TfB1cSbgc8zf*B= zB!_opYLwrJjvpCfx$V+LvWO@Vbk4ryaHi#Elssgl8Fclpib$IW5~D^X%O%COxe3{5 zs4U_(hr&=>s`8?drIDO%DC$p8780Gao1lvTYnVq=FpL?%kZFMg5=N-}RCr z8B<3!0$BFDquoTF)w_4!THbu?H9Yb7W1s)l&wR(PiOGBSADrXR(nm@CFB0;F%ce{W z=NWy?Q3MYb=ZLW>7dr2$vV@psU*wYVNcN?@cj0+GR7@U@?!rXvcRbc_c!B#z8;2Q0ZQZD*PuEzl<~s9yIXXDL zd-bu${^Iq=uK!!LBu;9@@Hj^RiUCws3OrIKp$Z8~KebM0&}V{4AQrZfe4WD5aH_S*fLM^ zStJzc4LH%@wwiwc)BT`j6a*`UHn=Ip2?|m zkzOelAPY$XzUOK;A8#nJww9%BtYoMH?*O1{=&j@1(?uH{)4%U4tb*kE4or}{zifm} z+7y+rIL{neDF3(Z?)4mUdw{SBl!K~kv7PO>vVPHupCFXTk)fxrCCBbV67bOKRr)oy zb?M0qg;IwVD>!Fiw5uY$il!%ocxNUls=h!?NKoh<8q6vcLEH3Dea9xmK_Idu@J3G| z^ln<`1713MmLGWUKY>5@SAQ+vc=$TL<(ZFR?k8r+3b=|}P&WRjrDz3!8v1TfwJ~p- zg`?I7X9dYCOW^0`6pk%-}(D*5>>o(95XXznx7iRy`BSCuk7&T z^SYofnh+D;xt96IM!6QC?p{p ziBxNBWVR9`YeWSx3S3zg*ar}+{SY)aR@)EWt|;V8?qby0!%0qI5Ifsa=ZK8=bcP3` z(G!X$cO+9?6m89Gsh!`J4N@ZvBWk6PG$|D`fkmnY%}gsF%#^VnfEgRxQdj|cxwW>lk>|z<%o!N0=IAP5z9Fa50`l6#cTEa%hz!C_8vFi zx`Vghd|3DH-sjQo00&nF#^Fx)o2V5fOa6A{+vZOaD00r?nuQmhzJga@zm0$V^RMHX zr;dSvInRge&T~y-gskGXgjKHl)9+0Ap2f!D(ju5OFh*6mCqw-bD>PykOhuPU%7i9D z42nn*#n(Fz3$NbWVo>oM zCwEWc(;xXG_3E4d2xD0z>XI~h725P0o$XPdfHO9wxh5+rl%Els=@+VOIN)5PnRg8P z)Plt+g0ojMG9qbITAd}BBc;$t8HebLF)<`C(k`?JO0z@K39Q@Hlz*cg8&k1XrZj^k zmP8qdiZ;DY-NFIU^dkae8E0qbXSL0ndE@50(Mb`@K0sBq=%Z~dqo?RZ#?BSep3<&(MIe-GWMwUCk0?||Vl0)J7-M{} zh_5+q+mLiuhLPKe`2XYT&x3uO3X(-3xa+ierkgS(?U96;FZ4!6UOZajp;LEEOmve5t%AdG|tBqSswA)&EKD%D(B zIp@nazkBcRJA3yZYwhp%^PRKLUTd$tw(`R?fx|w zr%x{3s2894BpF;!j|OT%t{3YbH7~z&`<;LL*FW=_|LrF}`O)W(7v4I1c!9CA&zTMb zDhJ4kq=)pmI<0GsTRv(E z4$#rb28f)SZP$rBX^BqyHFJmx+ z?rL#@2$HE{UN%V~&7HA<_g*vgl4JNgHtl1w!Mll@=b660wii*8GCGG~QB}!{XLeCt z?;+2OJ^+B)Ist~qUZPs}Xz<w+F@Qxd&bjxpdMkfYt%R zgq!{fVop~Q{bB8;qo`$bIvD|SlT&PlciW=*!3qZ}*JX!(6B%Z#6i&3bs`{3IbL3_u zW{HuR-G2SY)`$Y0&1eJMWI)IR+cGM*f7flI1i!q#=xeb_geEtv2nd*#rAz`h!lncQ z6Qk|7-Lu^r3+NO+S%}#^t&bZWmZ|{&!oBvh9KIjx4LXo3K=(LGCWVco>R$OmOJjfy z`~wG;FX4*+g3flGM0=Wd6T+S}(5!1&?vf^Gq&HG9-=0Z#x#(bx{@;KruHJlJKLvBI zvmV730Bg7>+j~l_FNwh+INiNTRU4VvN{ZeeiaXjr>DQ*$YI=XPh_2a0g4?CrIg8lg z0FsWIRbaWNErcU#vm->p_ua;sQAe5EXVEXwanaq^2-|8RWsx8CHB z{7-KtbJX#UvFffNiHsPr?hDth4%~S1q`vnDzQixR^{77d!DC;J&K=!{nH6e89>pw4 z$h(t+V5v#eL>txZ%qZr{qV1%#H3j*VfDR2h9B%QDYKMi;!3J4tDdW=;(;KjeqAKV_ z34sCtgUO^uWDShSNMOo>F(YaF0tFtXkF<)8Nw8$~n6L?Wfi+l|Gu>NS%GK1z#mu!9 zPtcTZ8e*(QAV%=v#ae3>#u&BE4BDVzP8IG%xJ~g&X!}7Eyqb=97yuT|O)Q5BHC>{N z%uOD_N>k)CGg`@-RhE}CLp39{S47RJinp}89t_B!W?Ezl-w)BCH5Cy2KdH;7d>cs- zo~R5T{>T&yE4JAq{4@`KlBHqHkbl~$u({n2PQa?oAN25u>e6ErLf8{ZZWp;*bI4NK zZ7SCPJ=Q5QK&(`IO()Cn6&cJ;t)Tr}tjUbBc(++tZGl2fCWe^9Qq_GFF)M|0hif}k zYAf;0q5{4=&qq_W8di=n=V~E?qj}D<8dymp`0*|JK8N`)l`j=iM```3OfR5vM1|YBO7g z>hyCe5Yt3y1CS^^%Fn;T3x?(%Ht8-uQkHsHl5n)Hepd8-VH2UlSg0I zDi>4{70jq&>atOvzA8AxTrp~7YNIeJ)CaCX-@Jd8eDC3`H}0MD+UXI#x}cP=-7dYlJG_EGP|wQ6EfFND_g~T!nGxPdn-hAce$LIZr52`T1 z*wrYO-6MP{ohu_>px{SG$I3l(;T;jh&Mq1+jOd*Qd*Kd7u0 zn@uGEM&R=06MXhJKl?oo-h2O#JbB|8Tz}$5o}J$h1b1iWyoiAXHF6oKo7bP&U%T?e zc>SwyeC~ypZ+_kGINqG=2C2Yseya!Icb>|!y43(1Rmx>Gl~b`EhmATU zpw<0sAkyYmde$arOpukg8ngFg2^XgSH5-`2=R$b5pPef1+z6uTLXKeuYKY4aS4fPQLgm0 zL6RG`w2-4L-PdMdBOimaU?G=u@Y%LJz{FAxihXdJ5!Dm*98VlJT47tSZm(!Eh2U10 z+@7rxQ#4Bx%~i4DBlF?O^5>B@KXbE7ga_fM% zRrC((9-TB%QCnb8o3xW}uWk2R2LQEH_=->?I)?m^d`1JBTCfL;n2_{#W~kLe5-mmy zRIQ~Hyeg|gM#wHJ5&+YsNv-@M@ib-?Ly%t0%0s0h8EKN}3o(FHW;g zcr}D)kztD_oxJ@#!)`goCqMR&*LVN;ALZZu(%;2D^ui|?tlDcJfc#F%9)!Wof3WH? z4!tC44{RbfPMjewia@x~Fh!a9O@;YJk;y&4!7e$lL~W=ac=EyBbKC>5I~sWM$xC?pnQM4- zcER`GeuTH*x{KSl9#tZEd>lAF8Z~x-5rc!oTqjm!z4F3SeEYq7_>rIb9IsqH!Rc{u zuE1D`l%+v2$*D?(d`!65vRUj{L85{l@4yI5DPNdT#(VnA7_5=Nj0M4DWFa$fQ7i79 z0q;MY@$gXsyQo)goZt_C+cS9O<_;f!`VxNTv-jiYzH|@IUcIC~a)H>&g{Y|$G0px? zid#{dwUu;>V2oVqjoV-4H+|@z;wvxw0et2wf4O$!IJl0k3{?f4vTZq}hNqS*h@||Y z4Y>U3P5>er!9=34B+JQ#`qWec@!eXkB#|Q7NY8##^HHM4*y<4q$!ai4a6}abhdLK4 zCpe-JQa4OF?GgrJH2coViV8vIZ8Ct+DI|XiR-%q}$LolJi}SnV?OSgzkM@uD&prF{ zpMCj5PyOF^yOZ;avkTn6`v7D?!Wufs7$~MIzUAD)Iac>fE6^LqxU%SfM~1|g&@J2G zxZCio1XFGW|335x$8LvP@4od^jr}8R^ zmzlgn&ZbmJgN~F-X9keR;{@mP;mcitM@WX{Ud_S`vW~wXADkKhF}Vx^(I4m9M_`wO@GkL$AL0XnlCVaV8*Xu-zt7 z@Xgo06E0Y9dj*x7IB=T8EtIO#+pG`g42lHKfQRo96ZXzY0%Z@GJQ%i{53d4hq{nCr z&t>Ift=FyW_473L(Vm?CE>a?76gUs~)FiMlJg^nu1gL8>ZTEk2D{i9I?GR7uOVK~% z@Kq?`f&`WkKu!}9O(N;7Bs5ff>zuX!cEDNK6d9xc%owgG>uuT~x)9L$T45VyxK?1P zfEBv_{()utlF$vmCC_N-hZuvd^8jHmt%oe>1vW{69yT;$Ayki)0Z$!X*@rqj1E?j6MB)0^AXUAo^Ku#uA)NYq+p048yQO;0(!I0bitxyhLN+Md4jWOJH4(M=zUYfYf8Wtb zRJ43crW#GD#KzGCCN(z}u2E@y>oDO{Qn6SrT5DD?sL`Ch>9r!kWLC_;h$-_yawzc>6Cw$&po+BP zBqf`K<8P?wvt(gnsQi|zf;c)JhKl9}~+ynXL|K|6_ z>FE)E^k-kk>B&+xi?b*jgD6R3m#*(aS%cQsC8Cq%oAgl`{UJe(8(x3 z4-FRiAV+j~bBwYISs-Tt*7XpwBQs}uu4n;MQUYvciE@&w`-MO_Nl|2V$r#OjkRkS! z&PLcFTnWd5fr?qPhI31U|5Qq$gftH#8{@)=QArU$s~VpCZ>GD{xi0v+vT76G4{-baM|k(WGtAo$Dn@YG z1(w~wQ`ax^*86wrC;q1|^Xj!zTsm2B;p$jT)A5ti-(;|+rXSV1ml9UFS*eaatE>MB z&M|7Rf{{DSF$*ZBc2|{JB$n;`u9l;?fQe z9-ZU8yKB93a?Zc{nY(qCJDwgh*IGkn#-lJLYg!A?cu-8P9CrCGs2QeeR_vBz+_5WmZ~gK_UPRUtAlED~GReirKC5OBfuxJ^7R_?8d#of4N7``QUCr2dr=MNv;S>C(z+ET?w&pi8qfA1qtKlks~y2t%T_mLNR zkHtfRMySHCxCn4iK&8@oWoNKDH|!iKm3VfCYoWn|GWs&Te8|&EBlfmVvCa>bUoITD zcklggIXb?k(vAu(!rRCLM5K2@t%ZmIvTQNlj6VrG)d zK+ZTfD}h?Oy|?jHV7Q&x5q~R^b&4T4qLoUcsa66IunheG)Vd<8G)q19MCaO&0lafj z7*qL%LZONFUVI~^Ql)r^IKfxG^o76t{EMHMarp_%^@5d5E{n+P1zaqhDV>3+wera; zPd&^!d+yHNJFi~4bm@z^PAMUh$N^Q?Q~7)Y@FX-k5jc1y7$3;jYbv#-3&d(haz5-$ z<`M2)WMl(VLjJWTrrk2+yN@O%s<)Yn2TqpK?^y=HUDQ65u=~#rs%s#nSm&1ZZb^4H zX=^|Go@wB{@z~RC-g>=N$(UQMVU>Axitkfzsl8uH|KE369Nv@kKB79oLv6zw#BJPw zE}}cH1bQd}VkuztxkNRsiB@NuI+e1*zpvJ6o$7^<MF%9POKE0k#IV5He2IpFK-%%J<4wV7HGKol?*v=h1g zY7ZO2hr5@*f%_)g2?J>lrI%GJ%@U@3hTK!%)Bp?Ujx8FFIeqnPPq$8(4FqC)uxy4( zgY_bwNf@~XgGl_6(vwVS|%RmSyHv!aSXW4gh_I#CI5Uv3skwY9PAR z-rrQR=_$o-Hon;YOrMq`WLBg?eTE0D^$$8pQ6sL~E}4j$f^hT!tg!dHf8qDX#aZHKfBqdDAMZ?D1rsdIvR$yB6F06M;fYH}_^$8yB5vPV`SQ!B+q3D5 zysQvWTeo>`5|xm8$d_Bs>tzswrL}<1KW3@8x8N}lskDl1N>UY-KrDK2S*R!$2XZFD z(@&V86BumrT}*=$5JfssGxk)F?nfgEg}v zLbv{qjB-aHYm`2sI!-5xJ!6|Off@qO9O5$=%$C;YQ2gpUr^&+++*EoI8OV^ZhA6w3 zsZDBb$?Aj}8^AA zSFRlM{+&no!N2nv93L&XGLEs|Pe$0xlQI*HrK|>tLL@T@?sm*XpHnak6ANdlz8P!9 zMJ>5sXFR%ifU~u@UR0gU%cpqZsZ&1p)JfgEe8iIlxX8q<2UYLB z^$-_x&zsjy_~;?#a{g*P9*$=~_G z{|$D#YkBw9PmbVa1Xkxl#0XZoDVVTjjKnnwMx-+!p zF=H7~1?+aqJ{Y|9{?~R7&hDM;c6|TkS3dE_FJF4%uY!rQ2lr4x93LG6HWtv8sCLOY zrzGcv&7;c64KPZs{%*%rF5;U(35Aro1{C^mqF7W#$M#yAR?1mS$LJhx1gu()mJ?Q0 zr0X=HBTA>k$`adFp~E$+ip8pE6EjtY0Kl7~l*=&D4gqcgW9GWwW9;~OMxj>U50r_X z+nl!Qt(b}My-`~srhH4Trc3(vj2rlKoZ8io9&)Vf?&FiV`F?Kk8R;_Xvf zaBz$9LL(J&@#5-S8I5yP$w#)@9L8zFMx7%xA)zI9`-Nm0D6Zb42f*&r;oRK{x?YPm z=54+#TH)czbk(cg8?x6(IS*C=e_zZh=)quJo6pJ--FffAPop>|oVBiQTGZ#z(&NP8Ouhz0WQLUqqZ0wLFMEu@(t0oUzv)S`*9gMDCy5e%rp zfS9iGcKKyv(AW^HfmtH3V9R_rS$qetc03dtqHh(O>#yfRXu-CUh&R9nq77h!-i&r- zFFwYOH1LI48dcDn{OpG;3{SOKLSKj|#MZOkfxx7}As59a4+6H|ZFQ{t0oo*y{ZhVX zdcB+sCC*y?r+ zb`bVqD`j?FNrXr7Bu6WO9q>%dxL#D8GDf656%0m(1?b_PGbsjiM)cXEcMu$vHpVqA zZxgwF_zoR_z5b~rJR>X2Tu^i2mH4RQ;_oT(V?UZX~+ZALkh z8!4Ke(T5AmkJbODkpr}7!IQ5Qlv2*+K7dK3Y-i_Z_~8z-0f8~@#R z*1h-l_y?c4%S%TC(F$vZ@wpBB3r1m!tX~a`2b9K?f;`wGaziL7+1o#Ly4vN6fTjMJ+5&BUkz6flF1T zswQMR@mmHa@>%7@H9O&k8>c*;(TO&4bjS?PML_EtfT$6AKQsXe$9xV%Rl2J+1aP$1 zvXYnqEUPu}x|>YeYLARU*%_Cx3;?Cl1qM44VgU&mTQUz8M@k2XB-xd$g4`TK)|H!~ zBotX1%7f_n%qFeDu)jkAicJ9fF75QnijM4b1ju2+0^kzmk(rv2N%uj8uTU0(3XZ+JR?_l*z6*S>HIfBmoj zdVTQurx+ahVE-sC&XF@S=32s@MJ2hkdtyCuL~)&3%RcE0kRd*?VmPgF2j6=Mw49QfdkBYgkg ze;@0bdF>K0S0%722(eiC1gpH{@T%% zqqQiFQbRrxP@=)Tpde)l8Wp-Gx9L!Y>Q+Vq8G*$)3Z*!c!Fm*kg>jU1>-OtMZ@>En zo_gZ?+aLJghyL*8OHcgtet&`Uvj=wTcMj5^x)K-$P-ve$I5(a4UMd&axK;MGdY#O{ z6-}atm;@UL0o#Q^5yVD`h$c@g1wXX97LHGkfAzur3#>D15Um@IVyL4ZRe|K>knLKb zQb344Lgo8G8aL(iG7li+N!LZGsu&zVMeOJKJUu=6+5NgoPZwPtnLCG)mN*?GF%7M~Q@Vav_dIt!f13htVEQeVZkWq&M zU%4ARUAt<~xjn=NN)C!Pq#r~h62G^@-NXiLa}b!18A|yhaa(C_paY3b>MO#-o2_K3 zROxD4yyqGv^iguWVWLj@}}Y%?3bl=(=468 zANqZWwobv4DdM2|`T$6}k2bc?XMa~nDZBvo(2$C%FK7R6c40G6Ra0hPlORizfC`lQ z)8#gODbsYM+O5&4kM3>_0I$&^H!veHaj}BkW%&{gAhGEdY8)f zmVoV`RPDg|*=>CIg?}9TyjB14U;oGW+_e)tb^J0f_UB6KeO|P@tJ4KokkK@p?DC}e zXU(V>nln;S$4mU!=U&58yCeMOU;KTz{mvu)gWtT5%a>T0sxo+*Kw<_j*2$Nj zIp%&W_;cU)WyC)5f#)yPTva?u6q%}q)?*#0648?})hQvVc4Bs4*kk0Arm_V%BumdJ z^@*QRRdJ{Wg=9%yEu2vz_v|cH>Jvo_Sq;?|7i|G&7v7leT19}VPKnS~;kstUkXK)O zYIQvcCOWH_5fxpE@wagTw3w-v#TUUYdYJQ7|G%nu?(1tKm)4p`)EI$BkMuWvZJCjKhyV)?)<|}OsF3}tJgRL zBBTUd9G=yMVYVj=shl+cHMQhH=k0sa{s>Tl$*Kwtwg#+Wic9`t4ib=6qc6Nd6(%JL zAyFOz040=q5Tq(17Iv{G1mH6ay*O19r*IAFwF8JttkF$#lo?Yy5|KY4ja9$Lv5^EOL zsieVw$K@*vk56`WybKXbjhDlVLO z>-IbNN5B40V&N(7Jo>HLEl)_5;5tDTIjhEq&Ih^zDFCkdX+rLbK^hi4i(@#+Mv{xn zfiRaEEgnW~#RkxfG=y|2Bye!G88c2|6g`~C@S;SbBKS~~`I7i!0Z*HX6 zwB+F=HZY2zU)_y?eeH4Q&TF`H=S^OD>e?5-?Ni_WhhkU1e!dPoxPK2zsIW3CRGl~a z>7MiM4+a3;v`}8?G?vi~t@?RQToGC*Ohmay8^eaiT}2$ft@y<)@3sd-WV3=8X;z#IbsxPmPwTQ!GETq4{IPr7KsMEwGCjjDU^y}7%>EetHOdn#8|9wJ_2Gws_bt^(*0@!YVLZ{#dkrM9&h zpkTG9pDj!xct1&vJ)B>ZCp_TLrqGh}CL;)~&EcyZ&^geu8)KJ45xu-FEf8T@d)pYW z_jkWr#n$k%*9Ri!Al#3aQeQ1;gK5AvTf>K8?UJkjk}-#h}~5SrVS$isLIz|u>jsM>vq^9 zdY)FEKx2F0b?WTQ$s$Alcaq2tg z0G8~>wfz(_t&oXGwTBF#)=18Z)hfU2YIK<+FSSMsI<6m|;`4v$ z_u=-P`}oDr-p!MfUG$P?PE{BNd4a;SH;)iY;4ggNXR%%sUU~i!_A@CHm|~)GVMNWx zj;zkI+YmA-Uw^P9uT+aw`XZ5ZSVrKkbP}DY6{sqA8X`wq6qQx&)~BSa4H&4Y7RqqF zy*6{7NH7J!!E9B04HMv5RU;mgz#?E@xyJuaY&tOmvK^*~E!dk&gA2Ew9~o2`yij9` zm!~Rgm0-zAyc-DlDQAVTd* z$2&=Ys_*7Vk@~5f_|fNg_22&F+qiQ^e#7%8oHdKJRLL_mhzXCHH@He|U#JQak+ova zqvaHL9=u-5Lj2B;{2%MJ_x~prkELeS4wM629;$VyngNbE3Nh6J5>)}`e4#7Gc0cUH zf<(42Q);lMDs}QK02Ary84?7|U*%`*|_X9^KyEet0|XJiL?7J^SL1edr^<^AF`(kJdFY&ww$2CBk>d z5q7PqY7l3jB`|BS3IiNX)qqTWq*4gyMI0(-4GQEdM{ds}0E>YCOtopDb707$38<}a zouRy1!xME+?9a|{+SFu7zL(mwEYU+1-EiKGwpIGIqIJch0U7Rl_kaw|rFje=CP!K&89K zkKO_@v7uTS(HquzSYcOQd!?+dX`&OwEzJ#JbPS5tG&mnNAl=g8E%jD4dc^M6Zr^!` zlau59+5Jbc+nvNRu$x*6sp_R_%UUDI#GK4|agO85N9&cFN528aBYo3>;_7j76qZ0^ zq*APA=e$k%8-%kZ`(42w*ro>%+ujH^NkS)+&0N%05bnNr)NJA$t|_5a(^cCiXd=*O z@1UZk^Ysbkv9-;@gLALPKzKplIpx7)2&nHtfH8kNwe0_ER>Ag1=&|O-!rZ`sJb6+; zLZ8p(tW^YT2-y2;TirSkA3gmIy|>EWH~qPBz`EJjHAOPfT>@T3^faCJCu>|NXr()F z0qXDvk5$ThiIuoDU{-FEhyyFp8`p`1_PnYqs;u|zzzwW#Ttk(Yz=5d>)vl3Aj1Z`* zN>~29SZ0R0swtxge@4QKu5?2^O_onvlHP2}0iZaOuhk%8Yzq%yt2TE+nky?(?69Tx!{6AST?`jq*o`tadFAg_1lPX?PB#%tUNN(|zOxZfz%MB19`2WFl4XMHp}ip+u!@ zY}>T~JsUp5?UJc+Uf~5Cj4G@e_bdZ=NjP=Jgd2Qi4hVpyGbP6!&9MV3$E;9kG`7BF ziUg5Tgn4msCoZ2pix0i>567SV{(q?6J-^Rydf`)eba4+oi(E5D!u?j0Im0{A2qV;Z zsyNOq!IzIxVOG@1DDFn(kALy)cy76d-~8V1%y-{-$Y1!42RuDqH{xcX6_3r=MIp?B}b&IIr2Azrf~*j**tJ4J}V4kB)f})P)*Py zDw^@jLJm_tGTTEEL&a5r@k;gx2DTQlnqe#MwZLpE*0Y-=G2ABg>9l#@()BFkAa5| z);3D1R(DJh&XpAi1#-Dwv^xSeJm)&F+OR58(|)odl3CQ1tT?1BEt+IYRz$MWikLA0 zBfG-X)D;qAEpX||fbO`?ftO-UCr};KLm<&n&LyJGF;h*RsX28W?^zyqIyJ)##cl-< zgUU)>Pid?JT?CxN9-TTnwV5mdu3s_jT&77)0_z(2PM~I`wD`3nSv?DWBr4Tk5LuiH zW!Vjoaj^Q3&!bBgp<4>64j8UsMNgV;?mLiD{xyZ<(5xPKpfY zY$6NTNh+fYxG$c~#U+Eqp$$`63UdjKj8E5Pg38$-s|NKT4RIV08Q2NGj-X@~K_FPQ zPEU*oNx3XHK8fr`XL1vx5W!5eqpsF21-Jc^6=N>??|RJq>@C(2c* zq24VjZ|)4xb3+V-Nqr2+_aqpFtkRFLyTU@cAruxSYu3!-?&ydpT->|&-qEA8`%A8m z=Jlta{qvvrhHv@TF3!*A#e;K{bLduy65S%|{4oh^MGd;A2U}j-Cd%!BQd|sJAIs$& zbJVJ+pQ=J}=zP|ih~=P?-5HRP1+GqrIGz6xjLjjyIV4dzu^Y>4@85a%BQHF2d7a2z zV3|c|Y;hrSfxeID5Gh|2SsIp!kJ=Km#6Wnulk2W1aMaQ9;Q2hS*KfUc{`?EC{O^&0 zD=*gfjJKNR-4L7FVG|p~NUmept&)|@;7YP0iy1XCUI1fY)q+pz-K94=@bjtmJ1l%myAQ;q(~BapD0jYo`)Akl`WyIO9|L@ zGB$#Ny~qU=4laq&$>R1|{PUR`xJ^Hp%@Jf8S8gnk9%8zZUO~)~xkq168vr$s6e#cN zf}oG2LJl0d3^Z)SY4+?sd*gT-%ykarqO-n>4AGG}#WpEF25KO;?_yr1KQ}F@N&h^D zd)XDBx-Y9r2XvG1cxTmcAaVZLvr5aU7|j5u>VWz_1d19Onx&%abl@yVveezQO^VRv zVD+HsvQZUM2igx)(lb!L1bvEe0RPsPY!{x92D;5;JDd=qqfM;fZRTD-XLOKO931wS zf1o^9b0cQFpVDd#U$*5sb{5>`PWKR&m*?apeQQ zVFlkF)N^S0w4Q0$uf!clmZWzG%33s3$1GBfX@-{6Z%C4t!|EbByO2o_BQzB)WalKJ z&CCR?-BbBQ{i?{ytTemI&H+&foL!uOaVbCX!GEm&hoAUY@M~}WIzDydqjSxRes;pq ziYc{LVb)^$2goX-W==&&s31D40$2$!1Bu8T$z@0U_!n=*vzM>T&-|s|U2nejFn<2i zck0qHG6FSA+J*?!(Gu970Z%?jobE>bf4}<+%(d|H^Ou;jfL+s1dd3-9z>1c>`OHTd zXY}1!j*7&27w7BnH`z*8qsmmO6xvK;qVm%1_!caf0Jb~9Fc(~ zL11t}6*eCT!*W3846?nDIbV$ka8vxSJV%|Zsn1rzK~sBCoNtkuAyJu<=c={3616yL zQj1>tC!OyQ>Ps?~Bk+7b=e`Qdh@$4#*I;=4r)BrLaFvK@2tarAJB8KB&GJJ5z>=CB zgJx4IFFev6y_frW5Xmj`7sx7W$ zjfyEiO^ifq-Jt4zvI3j}CY)uxjj@=tI!=l(qpHg7@oF?q7wd|Q7+c$s3ej2mPHc=A z9nXi_owUt-|F{jw{zQ6Y>3;S7Og#JCRje0#{J>xRbS=lDu3x`|Tqk$dLxKzrsoQtE zh~wjdquqjKcZ6jL1Q`q>LZ#+iM2!KC5o#_AU|GncW#n!r*YG8xVz|a#LwLEij7P^i zqFT3AGmOhn-HZ0&=Z@#!_~nOqxCWj$9<|@2EKsV_+>gmd%T-Ja%RqFO4;KkXEWG>R zbuyy93KKYt(w{ICsZA4zz|gRks+_@bQm3bv z&hFj6#jk$#(?<`^-#fec?DaqO(XaoOt4~~i=8xZb|NVJ>_7GrTunnQPDfb4{+iyLq zK8o|5t~M(?(ia3bf0`ck?U<_@W&n_?5XZ+moSt5)t5>h!Ffbqox%u`er}sR>4ZXPi0w#kQ8<-%Cu}A^^Adt~;Oa^%-9s0I!_*V(kW*A1N z?X!5gdwBh+>%aMxul?3IJ~_@@_vt93{hYxm*BoWtwP%f(yW<^i-Fa`EoLu^|+t_x$ zFAUndJyjc5d1q)pyDIL_Vu;&owK>C3cT)CqrxFyU+K!2x$2N*eeMc9!E-vr2UPr4P(br@)-t7#svmFN9>MDY# z4$t~9N7gIv;^9~pfRiLD=xf3+Y8-A!Zt@_`s%FR38@*o0*(RT!F#ufEz$V53Xh1 zMX5TJ$2XJF*;$&|D_1}b=p+|_1DR9O^xfi!xHLS*(O|fl2SgL?>dqVVi ztM(J{`;Xp?Zp^5Hj*%1t;n$1?qdsUG$mQXpIaCm~*tlIbF3xHNDFR6kto-^7yWxa( z`fK-*_Ti|OZ~|K+ICEPk$$DMc^GVQ}>$?w@U(`u}x_RD>IT zO5OcRY{WfE_NT5`BsHkHU;+p#O?zB)OVnY;Mm$E9(?MVR}fy)==se zXGrSv%Ulp~ati#!7vI8-%UA1*f90PXZ@qKIU;M3mxOTc@f-$#{9WU#Vk(k8+%2YJTN-T}L=&@NO z76skP;Umqd0+Xgj_y{JEz)~iOL;$6}v!t6~^BkTipl=b3P$52?H0hp5MAj04{Y8z7 zvx%|Lx_xM!kjk&9_!~ZSfvJl?F8%a8YgIBq! z7N+vD48>tPb5uzoVrE;Bl1%q=G3YLd{)g+*OCBluU?2?Ig z$YVXfo>oft8d5}e1Ae4C3-ud%eTPr7W|>#fXoYTkaT}hV)Y$F-I`9WHfjvu(jtX%w zal?@iGodE0ks^5LGCHXIylP)F$&yio#P^!Es5`2rL<_;HZD@LuwOw~W1!4pomeLBe zdhz9Jb@ph*_kH)TVi^mczIj=rA=z#0tt43)9X=8;}?gqEU8b7m@{ z`<`8Ix70~{dzNYI8hGQ*oAphv{v8YlSFtQ7mFbD4lHp`d$G?ItZOXx-+Nqu; z4+!yPl|Ex-ad=hfS}H^$fFbV&osmc;4Xy?lE4-#>eJ z?@^xbakT6(xNJFK8y~j#ubi<6kcq4c|9j_w;6Vx78)2&wk{BUpnmH#Th$o(W5?7wQ zd4j}0`sQ1&|7V~3;^%(#b6@=2SHJkB*S_?|*S_+T`}2q2wL3ogeb;Y1$;&5~m3NjJ znxZSfv?5}qM0Z(^y!mWOT=z@sjgN%=< zgCLm{V_B9u-i>*=j%lMFw?3fXc^J3}|Mnrz>oP&4#o)*``&jDoD-qJ-+^TeEXky z?X@rR?!&t{J-LFp&i$G*W+pO{sFceQC|M0*?2EA)Jeh2NLD6t_2qtoy$cbtX_!j&863bje)UY7U4)u1k&B)9qE`7 zQbb`;7lXkQ_a$i2(t;9=G816NUC$2ABmGowV8Y{7e)H zL;`N2wY;*WL4c*sXdvkf+N6Uj?+JF>fI(tUEZC?g2OM8m0+{f>t1r)ry>MT)WvT^8 z2sE8A4#X-C0@BsC)NAxOlEoDgK>dK}sxy4yY=|BQuYhV`i&lRWPy?F{i2Hg(LOFoK z$^`F?t%num3)HM)9@e~NFHladzxR6xSjiW_1|e_)6g8lfLpTNQZ&K!y00%}k_%%3p z08cAgs?7_%-%-|)y2rjrEfmxj=*xv>{@6E--*=GMLuW~s^T15FRw5AO>{`!9sgQ`J zNnbtDK%i<{?5wICiB-B~iN%r1{91+!jc-8Bs&K*}WP)JH%2Z_qi6yrpqe_~k59UBU zvxiECu}xu}N&^$7=N-TbRh)9|Sm)#?Kk!f0uYUdq`Q1PJr})X6&){fW;XW_a$B7Dg zpd@79u1!r*(B;|E>S=eZnBns{BUe=H7VtQ#{`Ti@@!HAd`r7yZe!l*dJM~Myaks9X z?yzPuf)=h6P&FzG*Pqzo$*Y6^zwi4p9zB}*!Iw^AT~WzmEJUT$`c^-Hqc$lda0F%Z zklUGV*8&4;6_z4v6&fXged@}gZG!cj2vk(Xl=-i#;ugBX+%K(2i43a;C6q>+yOg;Y zpp$UVcuiOPP3D4;46z^LPYRPQhL1w5xKqkrpAP< zM0@jD0@@rL3y<|o2EbxXPL&u@YQ^`lQeCn+NUdME9Mc^>*ZlciRhnjz1{mO8lJT(0eP@&pJscl8Boy-KSgh8avSFlu0xfMm-7-5N)5 zAft#%WDPV+OsujC8ezfu6ndFrc-EPC_PNWvd-n`K_`RRT?sSLiPadOkqGBP-<6cWv zF^u3$9nz{Wm`)O-O2!-<99SKFy8_YpRu1-l-kLP2g(un+#z~69zX=^(B`%yJ88MJ^ z^20ZG^}RoLhxgB;u3s8?G1n0otgw_$L5iN};VWDDP>cCdV2oWpI(wh%O8)MT{X6yA zyT2sA7|J$+rJ`Dz7pt5=Xv_i++XGF7>!G^)T)R;dF@+?2W^^EAMUqsfT!u?6B#;pd z%wd%)#UZf<=&_I`)0IYPDzc22s2Xyru^`rWIF;vx?f6cPjtV*R;raXZ`kP-mS(fqe zBOm+Z|MiI{pZd=qJ-opAg9ko?JLS?oIL=CqT51E5)+;pH(BBMXQO;RP)S9d`tP-5^ zK|xK_EbwT-&FeRCaen@Lzw(8z{AX{y`?asTbm{8y1tQZT%$;rj}*@N-UJFoB7%-{Ub$3F7A zk>kCd$Pox^ zx!MW?Ix>l}H&%Z-5kCFRwF5LXyO~7_Ph{eWCyu}W!yo?WfB74~{`dZkkAM7konN|g zT62Ga$*I|$V=P4A^ypHaUOidw-MMxA)i*xBf9b^+fA73lom@v(a5~Y0D^RHLL@Q{& z!0C*9E6d4eKzbYB-N^0l;nzlN<}L9Nxl_h!+1^`JyC`}BLX$P7+MPcoxZy7N`?krSGoJ%ik#+?WRFp#n9r|4Yf$bP(oKj#6Z@-q(}fU#KT{T7--WL zg=dXZGrLoCuZT^GI1aPPEwS%J$iQO*%(R`9(oez)8T~u|XTNV5eKlVIC(O#~L3r}8 zmU?3ns~@!K3)&sT?bzKc0+!g_gM*`9$yqEmgSg<<9;`dGl?&#iqPEQqvv8_Fb2~%G z1P9U8W0XIaL3xW7Hk_S4^v;Py0??lgmY(S|=truX%$0t5XZZ3)vz*zRHJdsB`qa~h znML=f9S)f%{G!z*u;PNiW(?x;bw6AM9yA*$BA~3dVI~HJK%8>msV6+vCj(i z>Q+F$cP8Rok4K>Qf#I>KmH75qTTDgf*;o_dP1g)jYF`GA$NjEdEQ^?*y{O36$Q zfQ(U5bC8i0WrJC8We=u8t3c>J4mC;ZmvxV6l;VKM`~)in(PWmWk5p(Q#55%oy+KN? z1=B@E>v|m59>e;2z9V3!*wrCB@pqmKITTrMGX3FcKL(9Kn=EALpNXnT)*xF~Y=qcE zESnV86?9Nb0Z}GaHNdBUSVXLv55n!e9ab*MoHN-F_19KLhI306|5#RSq@7D)!9!oOS zOuRlsnY(kEs>i@4^jL*jP)r7Q@!o^i@$nbF1J_Pp#vAYbEEreBNRMC^i;-m7y1QNe z!A%Wzhyvs|hyr?zSOiB^7FAKQaf*nUKvgO>Xf&d;gxnIBCPbjuH)wzWTdzKAp==QY zMaBq|ECCxaWR0m#jy!xS6fB0*!fB6^ReCM@qe(8k|&Gll&h>;kY zBihWMEBn~O+mKz!WY&nKmRLyH1J}FneD(PJ;axoY$_M_~wQE=Y!ui8<<--xi9dJIc z+;nul)>jm}=%^eppfhgj7Pakc8?HoXT($w_NT3#Ai3J?qkn5~O?R(r_t2ke}ZXJ$R(h%&b;Fn~X&l35Rq0&634%Kv<$| zK2pwe#+I)(Y2qTqws#woD5+EH@B)>RGxU9X?BwIl{<_7C%UtN|qMOrZLwrX7kIqw) z8kz}RTI;f!L9NYn%Y(~m?80Jye&<4N*$AsWTFu^h68!m8EA4X_sG2664rf%KRWd_i zLce31N)``=^YFqou0SDUU}VZLpFqyl8x{UsDsB>D<@H<+ih-VX=SnuZkS=p&D*)t| z(p6I}dOv9F{Qz0pVwK8uPK@L5;JZRvn)%qq11(uVKZH_n-N+MfMpb+*Q)rVss7T4- z`*IdC4iX4<)x%(`&BSs`1`f=N#&N`E?A9QXqmu(AQ96{d0bo65&yf!{j@}Y~#P&gz zvf3fM%(@Ifhj@Ao`foOmD2Ba3h%ga=2E@Ml^wInf!|!wW8V7?u1NapsQ@Oqm`t-0b z61Zp4mk)YOgd{xE!C`EDXZpU`#78%%lmeCt!VMCfJZ*2;bI!M;w*7g6SR7dCAJ`JP zA^D*1`Gbdr50P>(jj%L50Bpm&+&1ALOh_H1BAZk0>EJ3W89xty?kx%MHf~KSO2$A&07Lp zIJZ)?NfA}>s}p6$R3EHNctwo4W}rz4DN~VN5C?S?b)9RggagJnV2XQ|;^MM_-tT zCCE!B13&%w_bLK>?XUfQy!qv`{QJLgXIwi8q5a2lxe4Gr5jU4Wy69ix~l$#iKM8sn^}1pqV`I7)%BcpK$RsEP}1Eap#|v$G{(k+x2sd3 zlFTtPj3E&*TooyZ7Z&^-OjH{U0Yp}1>lfvJr>2l5h4n|3X|L`A@z^-SX6DM(;%e(b z#~ICOv`fsC`;3mfgs=cz1f+0M71f7s_);YR6eH56VPPV13E>X2lfFrRGI!R}zZIlDqc+u%-b6QDG3Wr3OS+$>u1S(VisR z<>1>)wOaV@l23~JP)qim-Ki#iztueI2yi@6CQ&QFk=l?o1q!PMX_<~T(sc1#3?91* zoJBJ^2o;o@wI?*L*3gYmd1t7AEdrjP^-j6h^F%m7+musU7FFjm0{&6AfCLa#IkRH8 z;+zGlVMJ}=bOW=~<4RfbJ6R}St)-n5!yRPF!T@HHNYy=61ZR&k@dyHz5PJp5=qgV2 z<5G5zl4*ch+*iO|1IXo&D`^%sog27hQR#X2x)f%$!A4>3C!Tx$DjwWD!w>wm-@>Ki z9j`xm8FNb3Us0Ipns>&WBZCnbm8i{CO2k8Lbr&UU8h2HwMZO&NMNr|5B2iq*K&FT5 z5*|z&6fdmLS+brHsYMAY9+>KX<4u)pg}Ayyn2hIivl9ZQ5f9n@o-EeP&4VtBuUn& z-O)+zj!tmr&Rfen?|ua{@#v!;`PiShdhO<4I)8M5dw1?)83f0ITyx7|+ELx!hvJkJ zbGBlXGb59emH~&2gRwZ=g!0)=s-e7j7fW8fehm-q-}@84^2@*cXUW~=C$2m>kFT8W z3lT@l@jTm~lZ8=(GYSJ3F>_5MF~*L^amtsUd-dV__uf1G;upU7*$=$*>N7mzqy5?5 z`BLYqUL>}nAu%U#|Neb^%eVcWZ~J?H?FY1<_cO;xPK2~q-P_!X&Y(72 zVN?X?hyilMod@qN@4WL>oL)Zti4T9>NB)^wmwRXT&oPD*oW4T4An4q;`|jbKu&cb@ z4qXHX(38s0ZR`MB4PcBk0?TgTcnRLVfBzYfFOl)mGIqC)k9VIrTH@XPe#QBP4_O~; zlN$t(J*gkCeu=4lDKMDud zFm^?}x>!gP6^2e?mgR-Dsg+&yMm$E?SQV|M2VJa&3)9^~*A%!gfPQzg=JEI1u$gVf(8(*{~%uE6izAy@iGIL#uCDTT z&O_gRCL)$?cD*cb+G9Wmrdunv!4x^Q zwA{O`4}7nu8hW>cn(pC2Rx8_eM7GQGx3j0^gRS4|<3NLjIPAeZkQUp6EMKMo<|c1~ zo>eEB-GU(S<`#nOo1^P$+AG7~i`!>u(n52UhrhpDUnb?(kiJ$`Rohq1Lmn4}nuS0l zaz=>+Er7?+hB*NK&N^NNuBH*!$CxheWlsdJlR=D9SQkXwbVt(g17(h3b<4>RcWz z#G^-d>V>Djl{c<`5P$Z2KVEZLNe_sxSN({{-*9Ir)pfaX+3q4a_8`K6pk*vb|>F=IO#GpFEx4 z|3j~h*WWqE2VcHS84R1!fZMdwmAF#1TAkMkRmEeA39i!DRG+vj48pqDb|aFL(E|(B z8^@X~kK>g<&=Wl*J{?ia0tdNb#WD*y24a?m_farDhUK);Cupk=ikBd((zKp4nux6S ztH>e-S6`FbngfyT;K-7JRtCA)E>!nKSnR$eQk+|=k=X?m$;J}{$kmxPAP7Ivw|!Nz z5;!>`9^7Any_&Ovs7X8W33Dl%Pp2NkaK0c?z&!vpN?1s5pvYR%_UqLGg{nby4o@1} zRkCVea=PZ&l8DNM@8g!^-L~XDpF56ATUsd(1tyYD#cTNhRF&kb_b&6%}H_J`I~g zusl9&6bi5^BVtI1qoFw2t|Q6VVt431DPF!{95-nH5U1%mFa7ZzmZkAwkaS?(>9ysm?%&72>QCN>`3Lb zNaS8KiW4Qaxc~+u;YeFZR(O&E15^$;B=RMy${k1rCab$ioJWcAlfr;ma+hdy$dVhq zattGjK@pb98jCpEEtqrVgNL`qt*^Zm7wg5t554;Ff8mAap8w%ncVEZ-d-pL$VF?d7 znNuczNas+U`xpw*>NlrMQO>8G4}uLCOXjcQxN-Qbdc|45l_##?!QK1+_OJfxum6Wf zN8{2{*PcE<8kg5Z0WwZ_7*>DC~SScWHKpn%#xyr>)3p5eQ7Nh=gq%$ z{plOui6K?v1hDwx*amNNK(@XG{2SUV0>1(u`0?r7P4HWCvCFfz)F+?}SR+}&| zP8&T|SEjo-fxuE-@vMZLY-!1N1NPd!Q1=JRNCD_M;e;pbHV|!G1#GhWB)~X;uxene z3)w7(s*>A=30EomQ@BUp8fA27-!kpAor?@Wn*ad-07*naR8l}@HDSYttc{)8Kx0f~ zZaubWxpzVB3z)RSUsp;RY?_rR|1*6vRHG^_!Enzxy$*mY$I$1mjSACi6F?0s97wMT zHr2%R#n5JT_1MJ_{jj|m5KozCzU9J7Sd_c z`ujkas(6w1ti+)L>Vp!MAQv4xZ|tqOZWf{fi0SX-zcRWdK!HUn1U*x`5c>g|3C;tH z+q-U)bd=_yWt6|8o)GQrU<0CV;}_HB^4-?AN*um>=0+H>s6p#~Zj63)pnLt&+84&U=bUmc(O;$3{~)hjh;ai|;~hv2DN{(uS;H4gO9x;FZ- zcrYbKqb#S`X9?>6P61PoJSsVN$li}alvNCtrPyl4#ZoBsV3k^{7EWeH$_K!tua2$; z-|Agwp<)zAvSrFMS27m58zI%Y8w~qK*gt}vTWK_bXen)J!~;h~s)9*%hM~BXRthkcDAouK9{Aw9KmgzWWH0AHMm%&m<`Avq-4 zh8Tm|;&y8**}a$A(-m?-)VT|3<d(J zVSbO#M3tJI$dYYlxh|z{EwwqL7F}ymzyyLJfeAV%Bk=5VSMccJ1^&ig`3#nm;8WKx zBjqQuu%afCQ6q~pGDejT z5UfZni1R$dgNsM_z2Eq+$JwKYxnJ++vK#{_<_Ki2B~gEv@2Wz>^axaGhHwys^VE&p zDbx%`OogE?+)V?w@-q+TUrSV^?_Gf4aD6y4YbtaFvu2EBI>vM^T@^${4AeT;iMim? z$)$X7exL8%{_5`R?C$>Zwd=q7iLd*{KYTQnFFkzt5VzjHjm%T*hWUgDt1G!FPl5J? zLW5s`bOqRe7y^CV5^5i~p*(FFer*(Ip)OxK#?i^;-P>=!`4>O?>976^PoCb`-F)&H zjNNet<4C8@;}OARdYCCzg_p_RJGCZ@!612p1vooD!<9=<&Nps-@%P+(=J|gj#-qPA z(Z#FS$VK{EFH&A$6}ivEvK-y|)YpIGM}Ge2f9ki+&+Z<*`24F_ADo<%#btLCW7*X* z#mUB)C;!00<*keJ75DF+p>QI6CiRR|%$>S7=5S7~ z!lD+s8=QI^tV5^1wDY?_VfDeN0QQ+UIlY8+fBw-=fBMruedYAhGtb|=xjuXSr8+t} z;mrL^PbI85cW>Q#`wx8P*M95%6W31v^0U{U{g<#yoS!8|3;{9*rWLp7J>f|K5vmri zRZ@^4r4|KHSkY~U7c+6?$`e0+`SO(?fA0Bb{}TlU07yJMI~VdDowvu9d!}NG^P zAk{Tg$P4GqCGWGIX_VCkmqPa?b&Qa*DYpuKMRziV3eL(ScQ$quPHDX=!>ql z=chz`^+q)ij>9$ws@1UDbCu^85SY!}YtJR&a*!9P&^14xg|SkQgujnjAROB2r+pc8 zNkm@?k_dvc54=IK6EHoVent*%s4cK<=dlh<<}guE+vN_ZUgo~FW@8gT4z$#GyIdt~ zme|3}YPxWlzE7MA2MQcA)~^awft8H5ME0{UNk3I0%&J7z=6s=SKB1(t0aPnr7?4pF zGzHI6m&8Tw2!7;+e=`2o&;Q@?b6@%?eEriO!Ntr3CXOOPCZ(CQiI-;Iwgf?A36W9V zjf9A5i=8pDW`QRQ>fQSneCy&NzU$xoRKEPumHe@vdW%;tFW|_W4vMv}8UwYiiQ`@1 zh37BVU;U9c>$AUgzdroxRm>^%f?LB{8sPvIsI|m*O*t`O0hiH6F92=SZHE-4Z`sjq zbeT?2_%RxgEThle`TmrxJO2mL^+G<>uPte zmfMPkXu5M{9qk78Yu4=t7uX$*LTTpUBr7W_f)#@Vv&uH^(|2yoLQJ)LA=|(xkt^kY zm}(Om5E@SfFCtd95o_eyLZXu7aiXRGsBO;rvPw`R=Rmbo29Z${atI?ra~6=8L=ZCt zesgLTV0O#}UE5_1MxSS)p(2n?2RC#P4NXc;kw|FLqI&lUln7Yu<19y{1dwZW@1Z{& zea}?{YjdDX#Zx&glSnbV!M;r6=^@Rz^)SFk(Y)%ELFn6pqdL*usfn&31_P>;f@ zjhK}g?%w09?iW?NMS+#+Ihsh2sOe;R^F?>9j$~o@;K8MZqIW={C^gGSIjl5jNd!)p z!c$jI^1FWK9-g{%Tt~Z!tP&*_LpAj<-Bp&`+D-!{@Q;vQBe3An*_(CcDDcr2{z$%k z=hGZRG#H?ZCyIopUHPu@c)pOARS`__89t&)Fk?Um(#aNBsNwEM5*!Q$M$;d`niUGk z8tI~~l0;Snhpbf!K_oL|jqW`+be4$B$`~#5mD#_z z0>{Uv_2jkFtM9yf`+I)CCT zvJL7-3Dht|t*l(@`1G`HJo!R>;Y+{u-(9+VRheBX&(Zy(d?>Av-VD{&!g_Izqvhl) z-}bHFdF}GmYk%|gcV0)u#c|fz?&9J>oISe52M^vpzI*oerFU+BX?OeHn{jk>@?F2< zQ{VE`$3F3Ozhj=wx6aPaQ8h8Q_cRDYN*&Z^9gQIpt<+V)Og~)*q)a`Js9B%8haV{87Gh>#h0z?e}=^{;hoQ@ILmr z&u6bcm#;qe5$3x4U;f%>KlABjIa0B(&6-^m&?B@NA!YttsDo;`4A=^)ivXpmiG^)$+Orbfv~&^sNo_}82sv*Q`XW;BO<^E5H9y2I#&uPGf9QDsIgV1 zp`=vbbNU7&fi^GkXDmmt9ur>(dBleS=6Bp(pEB04Cop&v~5$@pU_QE$_; z3V))BE}1r)7t3=j?c4@L#58kGB;N zYu_55{w{2y!spL#u=Sq0MzN6^ey$BjwXpTrP6>ao@PD4zZXL%MD2Z*#+n-T?SleY( z+lK6oq(svJ)F21+3|3doQLxOW4+al%5?>O!kbH5(LHb4xG*FqwVmcTF)Imm92t+w> zdcXDQ%y5sU0WEzdz{eC94R8tKV(ClBrDOIQ zW?`BDF*%901&eH%xrnXAm?4>SpB}ddNs~NX(}eYOleO_lXd%B{_ovOsA_Njz7Hj&0 zZW5O(h`|qmG}|hpGhubf_z~*nJG;QgKlsn!SHAR@>Tmw)pR12O^J3I;1mszOfi>aK z4H;3Tj>SYUnl46xBZxUm6XE1Yo+|6&l;jyFe)-lr^{4-tPwE^HmZn2+!<}rPI;hDJbIra}ij|y2 zL`135uK5CCZFOKGbNCWr$b+mOksJ zO|oLhO97i~qs#Fqnbzq*Qr;6P4^fOt&N8Q(nf$e z9l1-oA)WTEiVg-4$vQ9Kr6&e{^|dqn${P>x?Ju8jPMrWC7@4}Klp%*am!y4=S3;H@ zgD5k*Biwp$pI`UNKZ4zIH6ES6Q%B?2y)@9^TCh}T*WEg(>4K?=E#Bm=2+<~nB-MvN zWKFv+Dxa{L0kWwNu6}Y=@}?k4{uk8ES%DlRAM=|^x^X2m>gweu@b-IOeHzJUK)ekS?L8oFTLf$Yht|_j za{TD*j7!A%hd%g;KX`Wb@YP%IzVUxKTQ9!j;^D*R*W8cYa`NEh`1r@3y7tr$JazNw zzde@Id*>G`_V>@6*VzLB>3h$hl!e)TV~sN*1*D1=hkPGMS3Vnz0zG6#Rbb-}gTZAP zc=w&R{?KoI_KSc1n}5ge&ZA|=2lws;28#=0Fer1Ah{B4qi}Ph4`#jkl#nV@x*`wws z-+b$xKlR+R&;EOl?mxs3@1;jQ{n@H~e<}RvSy%U`IltgWw2Y3TXw2eM)IhVVU#{en z1v*^L_PR1~7JRa$eecfuT8C6N!f@^WdiM~K%nmt_;Yl{@y3r-nEe^Fas66a7-;zt9(B5)~;G>As#&YGX$?79?&x{)Ubr|KBtiGQS zKym`pfi=p6)MLQU0Glnxs0Lwucp>GY`tKeKeg#hWe>hsILzqC2lqaI|WO#88Aih6aHHV6WoLc<5!C;sXr0a+_j8pwX z64?L_av;l^n$>AZ5F<2o+Vx18rWcwI1>gqOad-|0^z7KOOr;+}6AO|H(FThmFPaQ! zrmM5VHVIS@=T=8X*CV#IK?f$Hr%ZqVw?UcJlnMMICUEqxqoqYYXytX${B`ZMt9KaDVeAmO1mtej}%tPk)Z)PF;nO>cc5uua)|tMJ9x+ z6Y6zZ9BH{-2H``kLF$_3S(vI0LlV}hLc9v2-6KsOBoK%RDRXJ-b=h5Nnq;;Nwx%8M z+qz5+8!K9%F0p)iU1Z=>m0V(AzrTx@Zhi+J?%$h#{wM!;!}1?-g*SU#8?8*#A%{dg|2evDcu5Z zzc$~B@(gH)Z1pG^9TW6yvIWf5j=N2ds%ndh(R)*ShBJ%9$@vIys;$0!?BQ~oILriP zc0WUd9qqoY9jaBVpzug9FrYwbSzvP(h7-avYOKj8MK~=i20kV`yu; zcW|jh7VI6TN=c{*2|gRQZ(|NN+vRp3-EP^E&uo)JUxt~o(009R4& zw!_x;L|-1ROu)}s#e>A^C`8z(IEJpoetg&_X++f!i=h%u*rOCBiK};Q4ko4K&H`f2 zVvPaLs>w1`2mDq1K19S6+ft(nmQiIPmK|Px`7*xz#XI=1zxkW>J^qO1E@%>COB}#DewW5zg0H zAARm$;k}1n&ce##2~s_Jse+c8v1txyCCJKRj3V2TEVZAtcq;HwVI=^yF@;WKOP*!b zDd!(#$h#(!7$co7>3*-$(3ybMOgdFuSy%!)9u<%9Xn*$b==HZ>JGyvumd`)`(tq>u zPrmeT%v|>m&hA27FgK9zagbQ7s&(P6)eG=PLTs^?&N^w(R5rYJ02&cxR5TD+GqI33 zx^flAN6Ry}Z{7K`uf6uxzm$`_e(lBm$;qY4{p5-|YO#i>NCht)yAq9wP&rIsF%|$7 z6UgDa8EPHw1FqhpK`#4imv0`u_0Bv0?v*Q-|0L#$wsTX46r0&v8T_}BF@VY`eco<& z{Ke;=fA!yB05KM*kp%*C>x%uH*sqVUFp$H2^d$qujBu4)`So;9cNeVW`4`w806VWw zA4Rx0=yp=*o){tr4U&j47F4WHeeO4Z?FYX38-GtSck|)H2gG1hREEucnIyRuF$nIe zW@3-?I^La*XP$ii{By7U`k#5`=Cl94)r|H~SN_WZtWzAFYVi#?-42Tb3;}IQ>& zmHzuycxoQ3l>Z<0o?ch@%m8rLW(2`i$}MtU&F zDQN7k!FnN(F&~p6JAbFxOJSpcv{=2De5PmT$HtLe@h@mP#%#71p4=;frkta+v6i5& zF82(TL2E1h2?zHx-S&4Id-p9i+6=ze$#)tQ5@YuDWb=fUwuU8W(QJ>6*19$lT_W*A zM!Nsqo44#Z7_`n%EJ7EK59rN{9Vi2V^5@Zh@JOSLNiw4G0Raxib2ePtD$V}$K2lCH zv{)@q_eE)G1h+N_m<1~XQTq18)@vCveRz6ZV7Be>9n`w1w+NUea%Rypu$k@psBcvz zr~OfMVl)q!JY6mCHS~XP;v&|zl~&d_fbFnkwd_bpx&;E;6bJ{7-v;mzHcxToy~-8z z?ZIt9e`A&W)ua07{NhMm#`4qVt9hBn{sE$XX%fAZ0w}?LH)@lk6w~19G#-eGXCqYFmfg4l2qrRZ326 zoduG(Z^)w|^+jAb^~PT0 zf;7WQUsaoOmJ8(sg-?ccARGJ4QR#}cgT9HkkW*a5D5Oe}MYLfe*{PP`YT59!Asag; ziWd;!n9L9g4Hjp|5_K5+qK7w!{2(Yqxl$Q2rj=UOZRiYS(Q1?Q$PN_^^ch{bR{Rl! zwo`La7)M^)?t3~p9(DW9o;j0C%e*P-s?C5vq|L8vZz%+X60T9bR|!@X)u2Ie^~FW& z8M_CZeHFO7nLb`X$%!~J3W=NvuA0@NRvX8aAP`a9oVqGC!NgH1<=P%YVNoOKlvpb! zEd`I(Ovu%}RJc}{Ccx_6ucpzJlqn`F>Z(gkKh%CPS>$k~O!=V_pE6^hs?uOl^Ss@R zjg2T>k-dv6StaX%Q0x8xONDYJJXM*60JA5qfGZfGQ5S-7PNf1Kj9E!m`hr)*y1=xb zJgQ_$l7-T6$8@%?Esz_W1XbhFRxCi>2sKI80x%JkE2hOVLoQd~pnC1q$U)yxC1_D8 zFALe$E5ECX9r5DxSMY`3xK%&;H$RUXH!t(rwIgEAiXjj*66&%}AT+=sI#FOaIYy$^ z-hVEORAE_QiFg}DRz*aNp*^F}1*e3jXAESp0OXX?hC0eNCnagmpuX5DYQGXUuLmAH zT32x~4Q~*6mGgas%BjBDRF6$_eO*z}@rLasBuSzIfyJ=KJ@4lOs+D zVCXKd&^22o!jV}ATQOE?LW5RaKA0%7%6HdbW+^ddWC578Ex+QUpp;6F($Ph+a|z){FgEBL^cU8O^PEXTeahA6{n>wr9(*Ihv4R zDI}`ENeu_DnV9PyPhP!=+jrmpSGgJLwdA~WtlaXq>RfQkrR0rO8DuPA?U3t4T_kb> z>%|)DKE*)NgVkyaAgCGwqDm;7hr7_`byqj-V+Ewz5?1AM-ru_BCq`Ht=|Z9YCvbeS z!t6sD~a+3OW9roi2R}qn2Xw%gx zlw?2=!hHQYDgx%#o>HA7Kv_t43aVAk?^}}LD_P;DV1bu58+t`lf4 zy#4(g!&4Jb<`fz`)d8PSxd%G^=;A2LIC#Ut1$hREcoZgA+5x(QFWvH8fixh==u1Xv z3IhoDdm?&Hee@e_Cs&uh=X{1^M{N(mnH?KY`t`iwrXlkN%H7kU`==kqL~+5!Eg7H} ztU5shVg(Y>xPD;1Q@1M5mTa2UQ|1CCS6=8=H^;*}-Ci0O zMxZ{A4l_#P`Zc)ynLbNsB7@}}GMy8F5`L}84$H3g(-ratz&^jo04tExc!+K-Xy9%L z;N2J-uuUqBYz=ZtP?$2((YneIOsjY3wby@h&V7jLKD!C|n0ZYqEOlX1{ab>mGsLZ* z6Qv7vYZVgM5b*Bc;}f6bpym*RY4t}Qen&3`)$P{Ik$ws`Od)Df6*43KKXXROm{uQvJ!4 zxY55yGO3COGNsZSv{mGWCUo@|0JtxV-4hi6z)~5L>br0`@G&L=M3#HNSlIJqT*3!F z_)p?5{?H%9`}=cz`1(h4pAP`k&^QA<|A?q5aRHOFijkZNj0`!c0@W&S2f(mw`eZlw z3txH2LDtXx7vG9s|IDp?^Sz6@bj*@vvu1Gg_pAxL^87J>?Kkh>M}OvBeDI}9I64Zf z7s`jS1>|CMD>{kO1y@meK3LNiY|4i&LY2;p%E-b@YBZ|IyK+6wW|Bn8HU~K{9nAZ3 zsgmE>Haf;*R4R*4YHfQWLjO_fw+*c3f9z{WjFghU0paXK!Fv8=Qv zMq#Yemr|=ha#N~%7r}-JB3-ojRv+jy=`P!XL~#|qNol->?ohL$1haJt=onH6CBKu@ z{LvCc>xjmyR*D z3E8b_83abz5L4%H5xP=3SHNB~+$)MD>VY+amDS#neG!#9BC{}x%phjRgM`38J%waN z0mHsSuur#hs_4wpIc9*FiI3ep#drUM+sH{gc@kK&GO92`kw<5%6UZ!3jTDmuZFa29 zGmu5xJHMMBdj1b#H%@uJf2YXB?I;0|Sv5)|2q?OGAmvNxY{D7=NCQ)7PaG5}*S=12 zXmTPu+hSB@%E3|(SUG?qK_#*R#Ze*)MO++XQQHR}oZT8{etxIhNS5&15NIm_~pbEdgE! z<@X2GD1nhR*A;VKU?wnga;_K1eWgqqJ7*2B7Hl!!i3+Do)l8T>H+Er;31Y>;lPL}$ zVQSqQQ~`mB;T*6U7rQufj@!51{=F|e|KauQ;iK3sBZPMVdH3D_;Al~NoLo@q3zI)pD_=LDdca5xw#DB*b{3;B+3h3#ZcHgwyJ&;{ z?m(2uDU(p}%~w%KF2HvB0Mz2gH+<)K0++G|Ja&Nr5hzbOVDC2X;Y0Dw)plh^BD)|m zTC3<%iA|@AJdED%92&+K1=wh4%m00dV5gJc#^Z$U#{Od49~`b(k?Ar&HqUO%RS;$s zhH(|WlMUCEq1fVgXev*6zJ(L$KH?@Qlz8g(<)rKQ#RCa1f@l#IW&fg8?^PHq%$zFF z`I&T{pb+mdl4#1kP#H)}Y$FXTJ5ih#X~G|({DOpvb4rX_wo%z*1qFO1F766vVs08r zZVCbbAcu@AyvBO}0bt+X@9<~QU&bjHwDH9-sbL9yTwUl_pO6lMN Jhd4L~wLyy) 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Ξ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�oZY&&luE5Iv&urv1WXKQDJeH5EbTLB?}7Y5g$> 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

*w5TY?q=rwR_v(NFck@{4A8d!8)Ys_CXMT- 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�+p5H&>Gb_MuT#a(bx=dO>a0ZDF|F#aqtmmj}{S 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

>`NbA-K}iG@iH`UgE2*owj9V?Myhu1< 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 i=QTD{D)d9gmi|B7j4;u-c3-~$0000 Date: Wed, 1 Jul 2026 08:42:15 +0000 Subject: [PATCH 10/15] Port engine layer: options, config, transforms, workflow, folder, CLI Large milestone landing the core engine on top of the interpreter (10 agents + consolidation pass). Whole solution builds 0/0; 15 tests pass; tool packs. Engine spine (namespace Copybara): IOption/Options, GeneralOptions, WorkflowOptions/WorkflowMode, ModuleSet/ModuleSupplier, and the domain interfaces IOrigin/IDestination/ITransformation, TransformWork/TransformResult, CheckoutPath/CheckoutFileSystem, Metadata/Info/MigrationInfo, IEndpoint/ITrigger, ChangeMessage, LabelFinder, etc. Packages ported: - config/ (Config, ConfigFile, PathBasedConfigFile, SkylarkParser wired to the interpreter, ConfigLoader/IConfigLoaderProvider, ConfigValidator) - transform/ + transform/metadata/ + transform/debug/ (Replace, CopyOrMove, Sequence, ExplicitReversal, FilterReplace, VerifyMatch, TodoReplace, scrubbers) - Workflow + Workflow.Create factory + WorkflowRunHelper + mode runners - CoreModule/CoreGlobal + struct/datetime/random Starlark modules - folder/ (FolderOrigin/FolderDestination/FolderModule) - action/, approval/, treestate/, version/, profiler/, effect/, monitor/ - util DiffUtil; CLI Main + ArgParser + migrate/info/validate commands Consolidation reconciled cross-module drift faithfully to the Java source: Workflow generic-arity via a reflective non-generic factory, EndpointProvider raw-type analogue (IEndpointProvider), Module.OfInnermostEnclosingStarlarkFunction added to the interpreter, Starlark-facing enums kept in SCREAMING_CASE for copy.bara.sky config compatibility, Preconditions value-type overloads. Also: capybara logo wired as NuGet PackageIcon + PackageReadmeFile (verified embedded in the .nupkg) and shown in README. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- TODO.md | 41 +- src/Copybara.Cli/ArgParser.cs | 322 ++++ src/Copybara.Cli/CommandEnv.cs | 104 ++ src/Copybara.Cli/Copybara.Cli.csproj | 8 + src/Copybara.Cli/DurationConverter.cs | 59 + src/Copybara.Cli/FuncConfigLoaderProvider.cs | 43 + src/Copybara.Cli/ICopybaraCmd.cs | 36 + src/Copybara.Cli/InfoCmd.cs | 281 ++++ src/Copybara.Cli/Main.cs | 565 +++++++ src/Copybara.Cli/MainArguments.cs | 149 ++ src/Copybara.Cli/MigrateCmd.cs | 141 ++ src/Copybara.Cli/Program.cs | 11 +- src/Copybara.Cli/ValidateCmd.cs | 110 ++ src/Copybara.Common/ImmutableSetMultimap.cs | 134 ++ src/Copybara.Common/Preconditions.cs | 31 + src/Copybara.Core/Action/ActionContext.cs | 195 +++ src/Copybara.Core/Action/ActionResult.cs | 82 + src/Copybara.Core/Action/IAction.cs | 49 + src/Copybara.Core/Action/StarlarkAction.cs | 89 + src/Copybara.Core/ActionMigration.cs | 203 +++ src/Copybara.Core/ActionMigrationContext.cs | 152 ++ .../Approval/ChangeWithApprovals.cs | 79 + .../Approval/IApprovalsProvider.cs | 69 + .../Approval/NoneApprovedProvider.cs | 33 + .../Approval/StatementPredicate.cs | 81 + src/Copybara.Core/Approval/UserPredicate.cs | 77 + .../AutoPatchfileConfiguration.cs | 78 + .../BaselinesWithoutLabelVisitor.cs | 60 + src/Copybara.Core/ChangeMessage.cs | 251 +++ src/Copybara.Core/ChangeVisitable.cs | 121 ++ src/Copybara.Core/CheckoutFileSystem.cs | 156 ++ src/Copybara.Core/CheckoutPath.cs | 275 +++ src/Copybara.Core/CheckoutPathAttributes.cs | 55 + .../Config/CapturingConfigFile.cs | 114 ++ src/Copybara.Core/Config/Config.cs | 114 ++ src/Copybara.Core/Config/ConfigFile.cs | 100 ++ src/Copybara.Core/Config/ConfigValidator.cs | 40 + .../Config/ConfigWithDependencies.cs | 42 + src/Copybara.Core/Config/GlobalMigrations.cs | 66 + .../Config/ILabelsAwareModule.cs | 61 + src/Copybara.Core/Config/IMigration.cs | 74 + .../Config/IOptionsAwareModule.cs | 28 + src/Copybara.Core/Config/MapConfigFile.cs | 76 + .../Config/MigrationValidator.cs | 42 + .../Config/PathBasedConfigFile.cs | 120 ++ .../Config/ResolveDelegateConfigFile.cs | 76 + src/Copybara.Core/Config/SkylarkParser.cs | 371 ++++ src/Copybara.Core/Config/SkylarkUtil.cs | 177 ++ src/Copybara.Core/Config/ValidationResult.cs | 123 ++ src/Copybara.Core/ConfigFileArgs.cs | 60 + src/Copybara.Core/ConfigItemDescription.cs | 62 + src/Copybara.Core/ConfigLoader.cs | 84 + .../ConsistencyFileConfiguration.cs | 41 + src/Copybara.Core/ContextProvider.cs | 40 + src/Copybara.Core/ConvertEncoding.cs | 57 + src/Copybara.Core/CoreGlobal.cs | 105 ++ src/Copybara.Core/CoreModule.cs | 1493 +++++++++++++++++ src/Copybara.Core/Destination.cs | 173 ++ src/Copybara.Core/DestinationInfo.cs | 27 + src/Copybara.Core/DestinationReader.cs | 110 ++ src/Copybara.Core/DestinationStatusVisitor.cs | 64 + src/Copybara.Core/Effect/DestinationEffect.cs | 262 +++ src/Copybara.Core/Endpoint.cs | 90 + src/Copybara.Core/EndpointProvider.cs | 65 + src/Copybara.Core/FlagAttribute.cs | 50 + src/Copybara.Core/Folder/FolderDestination.cs | 186 ++ .../Folder/FolderDestinationOptions.cs | 28 + .../Folder/FolderDestinationReader.cs | 104 ++ src/Copybara.Core/Folder/FolderModule.cs | 205 +++ src/Copybara.Core/Folder/FolderOrigin.cs | 153 ++ .../Folder/FolderOriginOptions.cs | 46 + src/Copybara.Core/Folder/FolderRevision.cs | 48 + src/Copybara.Core/GeneralOptions.cs | 508 ++++++ src/Copybara.Core/IConfigLoaderProvider.cs | 25 + src/Copybara.Core/Info.cs | 167 ++ src/Copybara.Core/LabelFinder.cs | 160 ++ src/Copybara.Core/LazyResourceLoader.cs | 79 + src/Copybara.Core/LocalParallelizer.cs | 95 ++ src/Copybara.Core/MapMapper.cs | 50 + src/Copybara.Core/MergeImportConfiguration.cs | 75 + src/Copybara.Core/Metadata.cs | 90 + src/Copybara.Core/MigrationInfo.cs | 37 + src/Copybara.Core/ModuleSet.cs | 54 + src/Copybara.Core/ModuleSupplier.cs | 136 ++ .../Monitor/ConsoleEventMonitor.cs | 66 + src/Copybara.Core/Monitor/EventMonitor.cs | 190 +++ src/Copybara.Core/Option.cs | 24 + src/Copybara.Core/Options.cs | 72 + src/Copybara.Core/Origin.cs | 326 ++++ src/Copybara.Core/PathOps.cs | 144 ++ .../Profiler/ConsoleProfilerListener.cs | 43 + src/Copybara.Core/Profiler/Listener.cs | 33 + .../Profiler/LogProfilerListener.cs | 55 + src/Copybara.Core/Profiler/Profiler.cs | 217 +++ .../Profiler/RecordingListener.cs | 55 + src/Copybara.Core/Profiler/Task.cs | 114 ++ src/Copybara.Core/Profiler/Ticker.cs | 42 + .../ReadConfigFromChangeWorkflow.cs | 207 +++ src/Copybara.Core/SkylarkContext.cs | 29 + src/Copybara.Core/StarlarkDateTimeModule.cs | 168 ++ src/Copybara.Core/StarlarkRandomModule.cs | 64 + src/Copybara.Core/StructModule.cs | 121 ++ .../Transform/CopyMoveRegexVisitor.cs | 182 ++ .../Transform/CopyMoveVisitor.cs | 101 ++ src/Copybara.Core/Transform/CopyOrMove.cs | 349 ++++ .../Transform/Debug/DebugOptions.cs | 68 + .../Transform/Debug/TransformDebug.cs | 293 ++++ .../Transform/ExplicitReversal.cs | 58 + src/Copybara.Core/Transform/FilterReplace.cs | 198 +++ .../Transform/IReversibleFunction.cs | 35 + .../Transform/IntentionalNoop.cs | 36 + .../Metadata/ExposeLabelInMessage.cs | 124 ++ .../Transform/Metadata/MapAuthor.cs | 211 +++ .../Transform/Metadata/MetadataModule.cs | 634 +++++++ .../Transform/Metadata/MetadataSquashNotes.cs | 167 ++ .../Transform/Metadata/MetadataVerifyMatch.cs | 66 + .../Transform/Metadata/ReferenceMigrator.cs | 240 +++ .../Metadata/RemoveLabelInMessage.cs | 53 + .../Metadata/RestoreOriginalAuthor.cs | 84 + .../Transform/Metadata/SaveOriginalAuthor.cs | 48 + .../Transform/Metadata/Scrubber.cs | 88 + .../Transform/Metadata/TemplateMessage.cs | 85 + .../Transform/Metadata/UseLastChange.cs | 92 + src/Copybara.Core/Transform/Remove.cs | 64 + src/Copybara.Core/Transform/Rename.cs | 125 ++ src/Copybara.Core/Transform/Replace.cs | 276 +++ src/Copybara.Core/Transform/ReplaceMapper.cs | 80 + src/Copybara.Core/Transform/Sequence.cs | 245 +++ src/Copybara.Core/Transform/SkylarkConsole.cs | 108 ++ .../Transform/SkylarkTransformation.cs | 145 ++ src/Copybara.Core/Transform/TodoReplace.cs | 261 +++ .../Transform/Transformations.cs | 61 + .../Transform/VerifyDirIsEmptyVisitor.cs | 58 + src/Copybara.Core/Transform/VerifyMatch.cs | 176 ++ src/Copybara.Core/TransformResult.cs | 268 +++ src/Copybara.Core/TransformWork.cs | 677 ++++++++ src/Copybara.Core/Transformation.cs | 59 + src/Copybara.Core/TransformationStatus.cs | 97 ++ src/Copybara.Core/TreeState/TreeState.cs | 161 ++ src/Copybara.Core/TreeState/TreeStateUtil.cs | 31 + src/Copybara.Core/Trigger.cs | 38 + src/Copybara.Core/Util/DiffFile.cs | 73 + src/Copybara.Core/Util/DiffUtil.cs | 364 ++++ .../Version/CorrectorVersionSelector.cs | 119 ++ .../Version/CustomVersionSelector.cs | 143 ++ .../Version/LatestVersionSelector.cs | 170 ++ .../Version/OrderedVersionSelector.cs | 73 + .../Version/RequestedExactMatchSelector.cs | 37 + .../Version/RequestedVersionSelector.cs | 28 + src/Copybara.Core/Version/VersionList.cs | 45 + src/Copybara.Core/Version/VersionResolver.cs | 28 + src/Copybara.Core/Version/VersionSelector.cs | 116 ++ src/Copybara.Core/Workflow.cs | 861 ++++++++++ src/Copybara.Core/WorkflowMode.cs | 49 + src/Copybara.Core/WorkflowModeRunner.cs | 672 ++++++++ src/Copybara.Core/WorkflowOptions.cs | 358 ++++ src/Copybara.Core/WorkflowRunHelper.cs | 1046 ++++++++++++ src/Copybara.Core/WriterContext.cs | 55 + src/Starlark/Eval/Module.cs | 21 + src/Starlark/Eval/StarlarkThread.cs | 22 + 160 files changed, 23161 insertions(+), 24 deletions(-) create mode 100644 src/Copybara.Cli/ArgParser.cs create mode 100644 src/Copybara.Cli/CommandEnv.cs create mode 100644 src/Copybara.Cli/DurationConverter.cs create mode 100644 src/Copybara.Cli/FuncConfigLoaderProvider.cs create mode 100644 src/Copybara.Cli/ICopybaraCmd.cs create mode 100644 src/Copybara.Cli/InfoCmd.cs create mode 100644 src/Copybara.Cli/Main.cs create mode 100644 src/Copybara.Cli/MainArguments.cs create mode 100644 src/Copybara.Cli/MigrateCmd.cs create mode 100644 src/Copybara.Cli/ValidateCmd.cs create mode 100644 src/Copybara.Common/ImmutableSetMultimap.cs create mode 100644 src/Copybara.Core/Action/ActionContext.cs create mode 100644 src/Copybara.Core/Action/ActionResult.cs create mode 100644 src/Copybara.Core/Action/IAction.cs create mode 100644 src/Copybara.Core/Action/StarlarkAction.cs create mode 100644 src/Copybara.Core/ActionMigration.cs create mode 100644 src/Copybara.Core/ActionMigrationContext.cs create mode 100644 src/Copybara.Core/Approval/ChangeWithApprovals.cs create mode 100644 src/Copybara.Core/Approval/IApprovalsProvider.cs create mode 100644 src/Copybara.Core/Approval/NoneApprovedProvider.cs create mode 100644 src/Copybara.Core/Approval/StatementPredicate.cs create mode 100644 src/Copybara.Core/Approval/UserPredicate.cs create mode 100644 src/Copybara.Core/AutoPatchfileConfiguration.cs create mode 100644 src/Copybara.Core/BaselinesWithoutLabelVisitor.cs create mode 100644 src/Copybara.Core/ChangeMessage.cs create mode 100644 src/Copybara.Core/ChangeVisitable.cs create mode 100644 src/Copybara.Core/CheckoutFileSystem.cs create mode 100644 src/Copybara.Core/CheckoutPath.cs create mode 100644 src/Copybara.Core/CheckoutPathAttributes.cs create mode 100644 src/Copybara.Core/Config/CapturingConfigFile.cs create mode 100644 src/Copybara.Core/Config/Config.cs create mode 100644 src/Copybara.Core/Config/ConfigFile.cs create mode 100644 src/Copybara.Core/Config/ConfigValidator.cs create mode 100644 src/Copybara.Core/Config/ConfigWithDependencies.cs create mode 100644 src/Copybara.Core/Config/GlobalMigrations.cs create mode 100644 src/Copybara.Core/Config/ILabelsAwareModule.cs create mode 100644 src/Copybara.Core/Config/IMigration.cs create mode 100644 src/Copybara.Core/Config/IOptionsAwareModule.cs create mode 100644 src/Copybara.Core/Config/MapConfigFile.cs create mode 100644 src/Copybara.Core/Config/MigrationValidator.cs create mode 100644 src/Copybara.Core/Config/PathBasedConfigFile.cs create mode 100644 src/Copybara.Core/Config/ResolveDelegateConfigFile.cs create mode 100644 src/Copybara.Core/Config/SkylarkParser.cs create mode 100644 src/Copybara.Core/Config/SkylarkUtil.cs create mode 100644 src/Copybara.Core/Config/ValidationResult.cs create mode 100644 src/Copybara.Core/ConfigFileArgs.cs create mode 100644 src/Copybara.Core/ConfigItemDescription.cs create mode 100644 src/Copybara.Core/ConfigLoader.cs create mode 100644 src/Copybara.Core/ConsistencyFileConfiguration.cs create mode 100644 src/Copybara.Core/ContextProvider.cs create mode 100644 src/Copybara.Core/ConvertEncoding.cs create mode 100644 src/Copybara.Core/CoreGlobal.cs create mode 100644 src/Copybara.Core/CoreModule.cs create mode 100644 src/Copybara.Core/Destination.cs create mode 100644 src/Copybara.Core/DestinationInfo.cs create mode 100644 src/Copybara.Core/DestinationReader.cs create mode 100644 src/Copybara.Core/DestinationStatusVisitor.cs create mode 100644 src/Copybara.Core/Effect/DestinationEffect.cs create mode 100644 src/Copybara.Core/Endpoint.cs create mode 100644 src/Copybara.Core/EndpointProvider.cs create mode 100644 src/Copybara.Core/FlagAttribute.cs create mode 100644 src/Copybara.Core/Folder/FolderDestination.cs create mode 100644 src/Copybara.Core/Folder/FolderDestinationOptions.cs create mode 100644 src/Copybara.Core/Folder/FolderDestinationReader.cs create mode 100644 src/Copybara.Core/Folder/FolderModule.cs create mode 100644 src/Copybara.Core/Folder/FolderOrigin.cs create mode 100644 src/Copybara.Core/Folder/FolderOriginOptions.cs create mode 100644 src/Copybara.Core/Folder/FolderRevision.cs create mode 100644 src/Copybara.Core/GeneralOptions.cs create mode 100644 src/Copybara.Core/IConfigLoaderProvider.cs create mode 100644 src/Copybara.Core/Info.cs create mode 100644 src/Copybara.Core/LabelFinder.cs create mode 100644 src/Copybara.Core/LazyResourceLoader.cs create mode 100644 src/Copybara.Core/LocalParallelizer.cs create mode 100644 src/Copybara.Core/MapMapper.cs create mode 100644 src/Copybara.Core/MergeImportConfiguration.cs create mode 100644 src/Copybara.Core/Metadata.cs create mode 100644 src/Copybara.Core/MigrationInfo.cs create mode 100644 src/Copybara.Core/ModuleSet.cs create mode 100644 src/Copybara.Core/ModuleSupplier.cs create mode 100644 src/Copybara.Core/Monitor/ConsoleEventMonitor.cs create mode 100644 src/Copybara.Core/Monitor/EventMonitor.cs create mode 100644 src/Copybara.Core/Option.cs create mode 100644 src/Copybara.Core/Options.cs create mode 100644 src/Copybara.Core/Origin.cs create mode 100644 src/Copybara.Core/PathOps.cs create mode 100644 src/Copybara.Core/Profiler/ConsoleProfilerListener.cs create mode 100644 src/Copybara.Core/Profiler/Listener.cs create mode 100644 src/Copybara.Core/Profiler/LogProfilerListener.cs create mode 100644 src/Copybara.Core/Profiler/Profiler.cs create mode 100644 src/Copybara.Core/Profiler/RecordingListener.cs create mode 100644 src/Copybara.Core/Profiler/Task.cs create mode 100644 src/Copybara.Core/Profiler/Ticker.cs create mode 100644 src/Copybara.Core/ReadConfigFromChangeWorkflow.cs create mode 100644 src/Copybara.Core/SkylarkContext.cs create mode 100644 src/Copybara.Core/StarlarkDateTimeModule.cs create mode 100644 src/Copybara.Core/StarlarkRandomModule.cs create mode 100644 src/Copybara.Core/StructModule.cs create mode 100644 src/Copybara.Core/Transform/CopyMoveRegexVisitor.cs create mode 100644 src/Copybara.Core/Transform/CopyMoveVisitor.cs create mode 100644 src/Copybara.Core/Transform/CopyOrMove.cs create mode 100644 src/Copybara.Core/Transform/Debug/DebugOptions.cs create mode 100644 src/Copybara.Core/Transform/Debug/TransformDebug.cs create mode 100644 src/Copybara.Core/Transform/ExplicitReversal.cs create mode 100644 src/Copybara.Core/Transform/FilterReplace.cs create mode 100644 src/Copybara.Core/Transform/IReversibleFunction.cs create mode 100644 src/Copybara.Core/Transform/IntentionalNoop.cs create mode 100644 src/Copybara.Core/Transform/Metadata/ExposeLabelInMessage.cs create mode 100644 src/Copybara.Core/Transform/Metadata/MapAuthor.cs create mode 100644 src/Copybara.Core/Transform/Metadata/MetadataModule.cs create mode 100644 src/Copybara.Core/Transform/Metadata/MetadataSquashNotes.cs create mode 100644 src/Copybara.Core/Transform/Metadata/MetadataVerifyMatch.cs create mode 100644 src/Copybara.Core/Transform/Metadata/ReferenceMigrator.cs create mode 100644 src/Copybara.Core/Transform/Metadata/RemoveLabelInMessage.cs create mode 100644 src/Copybara.Core/Transform/Metadata/RestoreOriginalAuthor.cs create mode 100644 src/Copybara.Core/Transform/Metadata/SaveOriginalAuthor.cs create mode 100644 src/Copybara.Core/Transform/Metadata/Scrubber.cs create mode 100644 src/Copybara.Core/Transform/Metadata/TemplateMessage.cs create mode 100644 src/Copybara.Core/Transform/Metadata/UseLastChange.cs create mode 100644 src/Copybara.Core/Transform/Remove.cs create mode 100644 src/Copybara.Core/Transform/Rename.cs create mode 100644 src/Copybara.Core/Transform/Replace.cs create mode 100644 src/Copybara.Core/Transform/ReplaceMapper.cs create mode 100644 src/Copybara.Core/Transform/Sequence.cs create mode 100644 src/Copybara.Core/Transform/SkylarkConsole.cs create mode 100644 src/Copybara.Core/Transform/SkylarkTransformation.cs create mode 100644 src/Copybara.Core/Transform/TodoReplace.cs create mode 100644 src/Copybara.Core/Transform/Transformations.cs create mode 100644 src/Copybara.Core/Transform/VerifyDirIsEmptyVisitor.cs create mode 100644 src/Copybara.Core/Transform/VerifyMatch.cs create mode 100644 src/Copybara.Core/TransformResult.cs create mode 100644 src/Copybara.Core/TransformWork.cs create mode 100644 src/Copybara.Core/Transformation.cs create mode 100644 src/Copybara.Core/TransformationStatus.cs create mode 100644 src/Copybara.Core/TreeState/TreeState.cs create mode 100644 src/Copybara.Core/TreeState/TreeStateUtil.cs create mode 100644 src/Copybara.Core/Trigger.cs create mode 100644 src/Copybara.Core/Util/DiffFile.cs create mode 100644 src/Copybara.Core/Util/DiffUtil.cs create mode 100644 src/Copybara.Core/Version/CorrectorVersionSelector.cs create mode 100644 src/Copybara.Core/Version/CustomVersionSelector.cs create mode 100644 src/Copybara.Core/Version/LatestVersionSelector.cs create mode 100644 src/Copybara.Core/Version/OrderedVersionSelector.cs create mode 100644 src/Copybara.Core/Version/RequestedExactMatchSelector.cs create mode 100644 src/Copybara.Core/Version/RequestedVersionSelector.cs create mode 100644 src/Copybara.Core/Version/VersionList.cs create mode 100644 src/Copybara.Core/Version/VersionResolver.cs create mode 100644 src/Copybara.Core/Version/VersionSelector.cs create mode 100644 src/Copybara.Core/Workflow.cs create mode 100644 src/Copybara.Core/WorkflowMode.cs create mode 100644 src/Copybara.Core/WorkflowModeRunner.cs create mode 100644 src/Copybara.Core/WorkflowOptions.cs create mode 100644 src/Copybara.Core/WorkflowRunHelper.cs create mode 100644 src/Copybara.Core/WriterContext.cs diff --git a/TODO.md b/TODO.md index 57b6c0b05..a5c3ae280 100644 --- a/TODO.md +++ b/TODO.md @@ -80,21 +80,24 @@ builtins), `lib` (json, proto, etc.), `spelling`. ## Phase 3 — Config model & core module (needs Phases 1–2) -- ⬜ `config/` → `Copybara.Config`: `Config`, `ConfigFile`, - `PathBasedConfigFile`, `MapConfigFile`, `Migration`, `ConfigValidator`, - `SkylarkParser`, `Config(Loader/Provider)`, `LabelsAwareModule`. -- ⬜ Top-level engine types in `com.google.copybara/`: - `Option`/`Options`, `GeneralOptions`, `ModuleSet`/`ModuleSupplier`, - `Core`(`CoreModule`, `CoreGlobal`), `Workflow`, `WorkflowOptions`, - `WorkflowMode`, `WorkflowRunHelper`, `Migration`, `Origin`, `Destination`, - `Transformation`, `TransformWork`, `TransformResult`, `CheckoutPath`, - `CheckoutFileSystem`, `Metadata`, `Info`, `MigrationInfo`. +- ✅ `config/` → `Copybara.Config`: `Config`, `ConfigFile`, `PathBasedConfigFile`, + `MapConfigFile`, `IMigration`, `ConfigValidator`, `SkylarkParser` (wired to the + ported interpreter), `ConfigLoader`/`IConfigLoaderProvider`, `ILabelsAwareModule`, + `ConfigWithDependencies`. +- ✅ Top-level engine types: `IOption`/`Options`, `GeneralOptions`, `ModuleSet`/ + `ModuleSupplier`, `CoreModule`/`CoreGlobal`, `Workflow` (+ non-generic + `Workflow.Create` factory), `WorkflowOptions`, `WorkflowMode`, `WorkflowRunHelper`, + `IMigration`, `IOrigin`/`IDestination`, `ITransformation`, `TransformWork`, + `TransformResult`, `CheckoutPath`, `CheckoutFileSystem`, `Metadata`, `Info`, + `MigrationInfo`, plus `profiler/`, `effect/`, `monitor/`, `action/`, `approval/`, + `treestate/`, `version/`. ## Phase 4 — Transformations (`transform/`) -- ⬜ `Replace`, `Move`/`Copy`/`Remove`, `Sequence`, `TransformationRegistry`, - `SkylarkTransformation`, `ExplicitReversal`, `TodoReplace`, `Scrubber`, - `metadata/*` (message/label manipulation), `debug/*`, `patch/*`. +- ✅ `Replace`, `CopyOrMove`/`Remove`, `Sequence`, `ExplicitReversal`, + `IReversibleFunction`, `SkylarkConsole`, `FilterReplace`, `VerifyMatch`, + `TodoReplace`, `SkylarkTransformation`, `transform/metadata/*`, `transform/debug/*`. +- ⬜ `transform/patch/*` (needs DiffUtil/patch tooling). ## Phase 5 — Git support (`git/`) — uses LibGit2Sharp @@ -113,7 +116,7 @@ Largest single module (175 files). Port in slices: ## Phase 6 — Other origins/destinations & modules -- ⬜ `folder/` (FolderOrigin/FolderDestination) — good early integration target. +- ✅ `folder/` (FolderOrigin/FolderDestination/FolderModule) — first ported origin/destination pair. - ⬜ `remotefile/`, `archive/`, `hashing/`, `http/`, `format/` (buildifier), `hg/` (Mercurial), `go/`, `rust/`, `python/`, `tsjs/`, `toml/`, `json/`, `xml/`, `html/`, `re2/`, `buildozer/`, `checks/`, `approval/`, `feedback/`, @@ -122,13 +125,13 @@ Largest single module (175 files). Port in slices: ## Phase 7 — CLI (`src/Copybara.Cli`) -- ⬜ Arg parsing (replace JCommander). Options contributed per-module, à la - `Options.getAll()`. Lightweight custom parser matching upstream flag names. -- ⬜ `Main` orchestration (mirror `Main.java`): console setup, logging config, +- ✅ Arg parsing (custom `ArgParser` replacing JCommander, reading `[Flag]` + attributes off option objects), matching upstream flag names. +- ✅ `Main` orchestration (mirrors `Main.java`): console setup, logging config, module set creation, command dispatch, exit codes, error handling. -- ⬜ Commands: `MigrateCmd`, `InfoCmd`, `ValidateCmd`, `HelpCmd`, `VersionCmd`, - `RegenerateCmd`, `OnboardCmd`/`GeneratorCmd`. -- ⬜ `PackAsTool` metadata, `build-data` version embedding. +- ✅ Commands: `MigrateCmd`, `InfoCmd`, `ValidateCmd` (+ version/help). +- ⬜ Commands not yet ported: `RegenerateCmd`, `OnboardCmd`/`GeneratorCmd`. +- ✅ `PackAsTool` + package icon/readme. ⬜ `build-data` version embedding. ## Phase 8 — Tests, docs, polish diff --git a/src/Copybara.Cli/ArgParser.cs b/src/Copybara.Cli/ArgParser.cs new file mode 100644 index 000000000..ca94ae93d --- /dev/null +++ b/src/Copybara.Cli/ArgParser.cs @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using System.Reflection; +using Copybara; +using Copybara.Exceptions; + +namespace Copybara.Cli; + +///

+/// A lightweight command-line argument parser that replaces JCommander. It discovers +/// -annotated members on a set of option objects and binds +/// --flag=value / --flag value / boolean flags onto them, collecting anything not +/// recognized as a flag into a list of positional (unnamed) arguments. +/// +/// This is intentionally minimal: it supports the flag surface Copybara actually uses +/// (string, bool, int, , enum-as-string, list and map flags) but does not +/// aim to reproduce every JCommander feature. +/// +public sealed class ArgParser +{ + /// A single registered flag: the member it binds to and the object owning it. + private sealed class Flag + { + public required object Owner { get; init; } + public required MemberInfo Member { get; init; } + public required FlagAttribute Attribute { get; init; } + + public Type MemberType => + Member is PropertyInfo p ? p.PropertyType : ((FieldInfo)Member).FieldType; + + /// Whether this is a "switch" flag that takes no value (unless arity forces one). + public bool IsBooleanSwitch + { + get + { + var t = MemberType; + // A bool flag with explicit arity 1 (e.g. --noprompt=true) still expects a value. + return (t == typeof(bool) || t == typeof(bool?)) && Attribute.Arity != 1; + } + } + + public void SetValue(object? value) + { + switch (Member) + { + case PropertyInfo p: + p.SetValue(Owner, value); + break; + case FieldInfo f: + f.SetValue(Owner, value); + break; + } + } + + public object? GetValue() => + Member is PropertyInfo p ? p.GetValue(Owner) : ((FieldInfo)Member).GetValue(Owner); + } + + private readonly Dictionary _flagsByName = new(StringComparer.Ordinal); + private readonly List _allFlags = new(); + + /// Registers every -annotated member found on the object. + public void AddObject(object optionObject) + { + const BindingFlags bindingFlags = + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + foreach (MemberInfo member in optionObject.GetType() + .GetMembers(bindingFlags) + .Where(m => m is PropertyInfo or FieldInfo)) + { + var attr = member.GetCustomAttribute(inherit: true); + if (attr == null) + { + continue; + } + + var flag = new Flag { Owner = optionObject, Member = member, Attribute = attr }; + _allFlags.Add(flag); + foreach (string name in attr.Names) + { + _flagsByName[name] = flag; + } + } + } + + /// Registers every option in the collection. + public void AddObjects(IEnumerable optionObjects) + { + foreach (var o in optionObjects) + { + AddObject(o); + } + } + + /// All registered flag names, used for "did you mean" hints and usage output. + public IReadOnlyList AllFlagNames => + _allFlags.SelectMany(f => f.Attribute.Names).Distinct().OrderBy(n => n, StringComparer.Ordinal) + .ToImmutableArray(); + + /// Descriptions of all non-hidden flags for usage output. + public IReadOnlyList<(string Names, string Description)> Descriptions => + _allFlags.Where(f => !f.Attribute.Hidden) + .Select(f => (string.Join(", ", f.Attribute.Names), f.Attribute.Description)) + .ToImmutableArray(); + + /// + /// Parses , binding recognized flags onto the registered option objects + /// and returning the positional (unnamed) arguments in order. + /// + /// on unknown flags or malformed values. + public IReadOnlyList Parse(string[] args) + { + var unnamed = ImmutableArray.CreateBuilder(); + int i = 0; + while (i < args.Length) + { + string arg = args[i]; + + // Only tokens beginning with '-' are candidate flags. Everything else is positional. + if (arg.Length < 2 || arg[0] != '-') + { + unnamed.Add(arg); + i++; + continue; + } + + string name = arg; + string? inlineValue = null; + int eq = arg.IndexOf('='); + if (eq >= 0) + { + name = arg.Substring(0, eq); + inlineValue = arg.Substring(eq + 1); + } + + if (!_flagsByName.TryGetValue(name, out Flag? flag)) + { + // Not a known flag. Treat as positional so Main can warn about likely typos, matching + // upstream's behavior of surfacing "looks like a flag" arguments to the command. + unnamed.Add(arg); + i++; + continue; + } + + if (flag.IsBooleanSwitch && inlineValue == null) + { + flag.SetValue(true); + i++; + continue; + } + + string rawValue; + if (inlineValue != null) + { + rawValue = inlineValue; + i++; + } + else + { + if (i + 1 >= args.Length) + { + throw new CommandLineException($"Missing value for flag '{name}'."); + } + rawValue = args[i + 1]; + i += 2; + } + + AssignValue(flag, name, rawValue); + } + + return unnamed.ToImmutable(); + } + + private static void AssignValue(Flag flag, string name, string rawValue) + { + Type type = flag.MemberType; + Type target = Nullable.GetUnderlyingType(type) ?? type; + + try + { + if (target == typeof(string)) + { + flag.SetValue(rawValue); + } + else if (target == typeof(bool)) + { + flag.SetValue(ParseBool(rawValue)); + } + else if (target == typeof(int)) + { + flag.SetValue(int.Parse(rawValue, CultureInfo.InvariantCulture)); + } + else if (target == typeof(long)) + { + flag.SetValue(long.Parse(rawValue, CultureInfo.InvariantCulture)); + } + else if (target == typeof(TimeSpan)) + { + flag.SetValue(DurationConverter.Convert(rawValue)); + } + else if (target.IsEnum) + { + flag.SetValue(Enum.Parse(target, rawValue, ignoreCase: true)); + } + else if (IsListType(target, out Type? elementType)) + { + AppendToList(flag, rawValue, elementType!); + } + else if (IsDictType(target)) + { + AssignDict(flag, rawValue); + } + else + { + throw new CommandLineException( + $"Unsupported flag type '{type}' for flag '{name}'."); + } + } + catch (CommandLineException) + { + throw; + } + catch (Exception e) + { + throw new CommandLineException($"Invalid value '{rawValue}' for flag '{name}': {e.Message}"); + } + } + + private static bool ParseBool(string value) + { + if (bool.TryParse(value, out bool b)) + { + return b; + } + throw new CommandLineException($"Expected 'true' or 'false' but got '{value}'."); + } + + private static bool IsListType(Type type, out Type? elementType) + { + if (type.IsGenericType) + { + Type def = type.GetGenericTypeDefinition(); + if (def == typeof(List<>) || def == typeof(IReadOnlyList<>) + || def == typeof(IList<>) || def == typeof(ImmutableArray<>)) + { + elementType = type.GetGenericArguments()[0]; + return true; + } + } + elementType = null; + return false; + } + + private static bool IsDictType(Type type) => + type.IsGenericType + && (type.GetGenericTypeDefinition() == typeof(ImmutableDictionary<,>) + || type.GetGenericTypeDefinition() == typeof(Dictionary<,>)); + + private static void AppendToList(Flag flag, string rawValue, Type elementType) + { + // List flags accept comma-separated values, matching JCommander's default splitter. + var pieces = rawValue.Split(',', StringSplitOptions.RemoveEmptyEntries); + var current = flag.GetValue(); + var listType = typeof(List<>).MakeGenericType(elementType); + var list = (System.Collections.IList)Activator.CreateInstance(listType)!; + if (current is System.Collections.IEnumerable existing and not string) + { + foreach (var item in existing) + { + list.Add(item); + } + } + foreach (var piece in pieces) + { + list.Add(elementType == typeof(string) + ? piece + : Convert.ChangeType(piece, elementType, CultureInfo.InvariantCulture)); + } + flag.SetValue(list); + } + + private static void AssignDict(Flag flag, string rawValue) + { + // Map flags are 'k1:v1,k2:v2'. Only is used in practice. + var builder = ImmutableDictionary.CreateBuilder(); + if (flag.GetValue() is ImmutableDictionary existing) + { + foreach (var kv in existing) + { + builder[kv.Key] = kv.Value; + } + } + foreach (var entry in rawValue.Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + int colon = entry.IndexOf(':'); + if (colon < 0) + { + throw new CommandLineException( + $"Expected 'key:value' pairs but got '{entry}'."); + } + builder[entry.Substring(0, colon)] = entry.Substring(colon + 1); + } + flag.SetValue(builder.ToImmutable()); + } +} diff --git a/src/Copybara.Cli/CommandEnv.cs b/src/Copybara.Cli/CommandEnv.cs new file mode 100644 index 000000000..67a3a1ea2 --- /dev/null +++ b/src/Copybara.Cli/CommandEnv.cs @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara; +using Copybara.Common; +using Copybara.Exceptions; + +namespace Copybara.Cli; + +/// +/// Environment information for command execution: arguments, workdir, etc. +/// +public sealed class CommandEnv +{ + private readonly string _workdir; + private readonly Options _options; + private readonly MainArguments? _mainArgs; + private readonly ImmutableArray _args; + private ConfigFileArgs? _configFileArgs; + + public CommandEnv( + string workdir, Options options, IEnumerable args, MainArguments? mainArgs) + { + _workdir = Preconditions.CheckNotNull(workdir); + _options = Preconditions.CheckNotNull(options); + _args = args.ToImmutableArray(); + _mainArgs = mainArgs; + } + + /// + /// Instantiate a new CommandEnv. Meant for use with construction of new ICopybaraCmd objects. + /// + public CommandEnv(string workdir, Options options, IEnumerable args) + : this(workdir, options, args, null) + { + } + + /// + /// Get the arguments parsed as config [migration [source_ref]...] if the command uses that + /// format. + /// + public ConfigFileArgs? GetConfigFileArgs() => _configFileArgs; + + public MainArguments? GetMainArgs() => _mainArgs; + + /// Parse the CLI arguments as config [workflow [source_ref]...]. + /// + public ConfigFileArgs ParseConfigFileArgs(ICopybaraCmd cmd, bool usesSourceRef) + { + Preconditions.CheckState( + _configFileArgs == null, + "ParseConfigFileArgs was already called. Only one invocation allowed."); + if (_args.IsDefaultOrEmpty) + { + throw new CommandLineException( + $"Configuration file missing for '{cmd.Name}' subcommand."); + } + + string configPath = _args[0]; + + if (_args.Length < 2) + { + _configFileArgs = new ConfigFileArgs(configPath, workflowName: null); + return _configFileArgs; + } + + string workflowName = _args[1]; + if (_args.Length < 3) + { + _configFileArgs = new ConfigFileArgs(configPath, workflowName); + return _configFileArgs; + } + + if (!usesSourceRef) + { + throw new CommandLineException( + $"Too many arguments for subcommand '{cmd.Name}'"); + } + + _configFileArgs = new ConfigFileArgs( + configPath, workflowName, _args.Skip(2)); + return _configFileArgs; + } + + public string GetWorkdir() => _workdir; + + public Options GetOptions() => _options; + + public IReadOnlyList GetArgs() => _args; +} diff --git a/src/Copybara.Cli/Copybara.Cli.csproj b/src/Copybara.Cli/Copybara.Cli.csproj index 1b2690274..018245c3b 100644 --- a/src/Copybara.Cli/Copybara.Cli.csproj +++ b/src/Copybara.Cli/Copybara.Cli.csproj @@ -9,10 +9,18 @@ ./nupkg 0.1.0 A tool for transforming and moving code between repositories (.NET port of Google Copybara). + icon.png + README.md + copybara;git;migration;vcs;starlark;code-transformation + + + + + diff --git a/src/Copybara.Cli/DurationConverter.cs b/src/Copybara.Cli/DurationConverter.cs new file mode 100644 index 000000000..83b05ba20 --- /dev/null +++ b/src/Copybara.Cli/DurationConverter.cs @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Globalization; +using Copybara.Exceptions; + +namespace Copybara.Cli; + +/// +/// Converts strings like 10s/10m/10h/10d to a . +/// Port of com.google.copybara.jcommander.DurationConverter. +/// +public static class DurationConverter +{ + public static TimeSpan Convert(string value) + { + if (value.Length < 2) + { + throw DurationException(value); + } + + if (!int.TryParse( + value.AsSpan(0, value.Length - 1), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int num) + || num < 0) + { + throw DurationException(value); + } + + char unit = value[^1]; + return unit switch + { + 's' => TimeSpan.FromSeconds(num), + 'm' => TimeSpan.FromMinutes(num), + 'h' => TimeSpan.FromHours(num), + 'd' => TimeSpan.FromDays(num), + _ => throw DurationException(value), + }; + } + + private static CommandLineException DurationException(string value) => + new(string.Format( + "Invalid value for duration '{0}', valid value examples: 10s, 10m, 10h or 10d", value)); +} diff --git a/src/Copybara.Cli/FuncConfigLoaderProvider.cs b/src/Copybara.Cli/FuncConfigLoaderProvider.cs new file mode 100644 index 000000000..118aad76d --- /dev/null +++ b/src/Copybara.Cli/FuncConfigLoaderProvider.cs @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara; +using Copybara.Config; + +namespace Copybara.Cli; + +/// +/// A backed by a delegate. Mirrors the lambda-based +/// implementation returned by upstream's Main.newConfigLoaderProvider. +/// +/// Note: this assumes the config agent defines ConfigLoaderProvider in the +/// Copybara namespace as an interface exposing +/// ConfigLoader NewLoader(string configPath, string? sourceRef) (faithful to Java's +/// ConfigLoaderProvider.newLoader). If it lands as a delegate instead, this adapter should +/// be replaced during consolidation. +/// +public sealed class FuncConfigLoaderProvider : IConfigLoaderProvider +{ + private readonly Func _factory; + + public FuncConfigLoaderProvider(Func factory) + { + _factory = factory; + } + + public ConfigLoader NewLoader(string configPath, string? sourceRef) => + _factory(configPath, sourceRef); +} diff --git a/src/Copybara.Cli/ICopybaraCmd.cs b/src/Copybara.Cli/ICopybaraCmd.cs new file mode 100644 index 000000000..9b8bb0790 --- /dev/null +++ b/src/Copybara.Cli/ICopybaraCmd.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Util; + +namespace Copybara.Cli; + +/// +/// A Copybara command like 'info', 'migrate', etc. +/// +public interface ICopybaraCmd +{ + /// Run the command. + /// Command environment: params, workdir, etc. + /// Result exit code. + /// + /// + /// + ExitCode Run(CommandEnv commandEnv); + + /// Command name. + string Name { get; } +} diff --git a/src/Copybara.Cli/InfoCmd.cs b/src/Copybara.Cli/InfoCmd.cs new file mode 100644 index 000000000..53e654c98 --- /dev/null +++ b/src/Copybara.Cli/InfoCmd.cs @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara; +using Copybara.Common; +using Copybara.Config; +using Copybara.Revision; +using Copybara.Util; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Cli; + +/// +/// Reads the last migrated revision in the origin and destination. +/// +public sealed class InfoCmd : ICopybaraCmd +{ + private const int RevisionMaxLength = 15; + private const int DescriptionMaxLength = 80; + private const int AuthorMaxLength = 40; + private const string DateFormat = "yyyy-MM-dd HH:mm:ss"; + + private readonly IConfigLoaderProvider _configLoaderProvider; + private readonly IContextProvider _contextProvider; + + public InfoCmd(IConfigLoaderProvider configLoaderProvider, IContextProvider contextProvider) + { + _configLoaderProvider = Preconditions.CheckNotNull(configLoaderProvider); + _contextProvider = Preconditions.CheckNotNull(contextProvider); + } + + public ExitCode Run(CommandEnv commandEnv) + { + ConfigFileArgs configFileArgs = commandEnv.GetConfigFileArgs()!; + GeneralOptions generalOptions = commandEnv.GetOptions().Get(); + Console console = generalOptions.GetConsole(); + bool includeDefinitions = generalOptions.InfoIncludeDefinition; + ConfigWithDependencies config = _configLoaderProvider + .NewLoader(configFileArgs.GetConfigPath(), configFileArgs.GetSourceRef()) + .LoadWithDependencies(console); + + if (generalOptions.InfoListOnly) + { + ListMigrations(commandEnv, config.GetConfig(), includeDefinitions); + return ExitCode.Success; + } + + if (configFileArgs.HasWorkflowName()) + { + IReadOnlyDictionary context = _contextProvider.GetContext( + config, configFileArgs, _configLoaderProvider, commandEnv.GetOptions(), console); + bool hasAvailableChanges = + InfoWithFailureHandling( + commandEnv.GetOptions(), + config.GetConfig(), + configFileArgs.GetWorkflowName(), + context); + return hasAvailableChanges ? ExitCode.Success : ExitCode.NoOp; + } + + ShowAllMigrations(commandEnv, config.GetConfig(), includeDefinitions); + return ExitCode.Success; + } + + private static string GetShortFileName(string? path) + { + if (string.IsNullOrEmpty(path)) + { + return ""; + } + int idx = path.LastIndexOf('/'); + return idx >= 0 ? path.Substring(idx + 1) : path; + } + + private static string FormatStackEntry( + ImmutableArray callStack, int stackIndex, bool extraSpacing) + { + Preconditions.CheckArgument(stackIndex > 0, "Index must be greater than 0"); + string definitionName = callStack[stackIndex].Name; + int line = callStack[stackIndex - 1].Location.Line; + if (line != 0) + { + string callerFile = GetShortFileName(callStack[stackIndex - 1].Location.File); + string mainFile = GetShortFileName(callStack[0].Location.File); + if (!callerFile.Equals(mainFile, StringComparison.Ordinal)) + { + string spacing = extraSpacing ? " " : ""; + return $"{definitionName}@{line}{spacing}[{callerFile}]"; + } + return $"{definitionName}@{line}"; + } + return definitionName; + } + + private static void ListMigrations( + CommandEnv commandEnv, Config.Config config, bool includeDefinitions) + { + Console console = commandEnv.GetOptions().Get().GetConsole(); + if (includeDefinitions) + { + var entries = new List(); + foreach (string name in config.GetMigrations().Keys.OrderBy(n => n, StringComparer.Ordinal)) + { + IMigration m = config.GetMigration(name); + var callStack = m.GetDefinitionStack(); + if (!callStack.IsDefaultOrEmpty && callStack.Length > 1) + { + var fullStack = new System.Text.StringBuilder(FormatStackEntry(callStack, 1, false)); + for (int i = 2; i < callStack.Length; i++) + { + fullStack.Append("->").Append(FormatStackEntry(callStack, i, false)); + } + entries.Add(name + ":" + fullStack); + } + } + console.InfoFmt("MIGRATIONS+DEFINITIONSTACK: %s", string.Join(",", entries)); + } + else + { + console.InfoFmt( + "MIGRATIONS: %s", + string.Join( + ",", config.GetMigrations().Keys.OrderBy(n => n, StringComparer.Ordinal))); + } + } + + private static void ShowAllMigrations( + CommandEnv commandEnv, Config.Config config, bool includeDefinitions) + { + TablePrinter table; + var sortedMigrations = config.GetMigrations().Values + .OrderBy(m => m.GetName(), StringComparer.Ordinal) + .ToImmutableArray(); + + if (includeDefinitions) + { + table = new TablePrinter("Name", "Definition", "Description"); + foreach (IMigration m in sortedMigrations) + { + var callStack = m.GetDefinitionStack(); + if (!callStack.IsDefaultOrEmpty && callStack.Length > 1) + { + table.AddRow( + m.GetName(), + FormatStackEntry(callStack, 1, true), + m.GetDescription() ?? ""); + for (int i = 2; i < callStack.Length; i++) + { + table.AddRow("", "↳ " + FormatStackEntry(callStack, i, true), ""); + } + } + } + } + else + { + table = new TablePrinter("Name", "Origin", "Destination", "Mode", "Description"); + foreach (IMigration m in sortedMigrations) + { + table.AddRow( + m.GetName(), + PrettyOriginDestination(m.GetOriginDescription()), + PrettyOriginDestination(m.GetDestinationDescription()), + m.GetModeString(), + m.GetDescription() ?? ""); + } + } + + Console console = commandEnv.GetOptions().Get().GetConsole(); + foreach (string line in table.Build()) + { + console.Info(line); + } + console.Info( + "To get information about the state of any migration run:\n\n" + + " copybara info " + config.GetLocation() + " [workflow_name]" + + "\n"); + } + + private static string PrettyOriginDestination(ImmutableListMultimap desc) + { + string type = desc["type"].Single(); + var urls = desc["url"]; + return type + (urls.Length > 0 ? " (" + urls[0] + ")" : ""); + } + + /// Retrieves the info of the migration and prints it to the console. + private static bool InfoWithFailureHandling( + Options options, Config.Config config, string migrationName, + IReadOnlyDictionary context) + { + // TODO(port): dispatch InfoFailedEvent via eventMonitors() once the monitor package is + // ported. Currently only the info body is executed. + _ = context; + return Info(options, config, migrationName); + } + + private static bool Info(Options options, Config.Config config, string migrationName) + { + Info info = GetInfo(migrationName, config); + Console console = options.Get().GetConsole(); + int outputSize = 0; + bool hasAvailableChanges = false; + foreach (MigrationReference migrationRef in info.MigrationReferences) + { + console.Info(string.Format( + "'{0}': last_migrated {1} - last_available {2}.", + migrationRef.GetLabel(), + migrationRef.LastMigrated != null ? migrationRef.LastMigrated.AsString() : "None", + migrationRef.GetLastAvailableToMigrate() != null + ? migrationRef.GetLastAvailableToMigrate()!.AsString() + : "None")); + + var availableToMigrate = migrationRef.GetAvailableToMigrate(); + int outputLimit = options.Get().GetOutputLimit(); + if (availableToMigrate.Count > 0) + { + hasAvailableChanges = true; + console.InfoFmt( + "Available changes %s:", + availableToMigrate.Count <= outputLimit + ? $"({availableToMigrate.Count})" + : $"(showing only first {outputLimit} out of {availableToMigrate.Count})"); + var table = new TablePrinter("Date", "Revision", "Description", "Author"); + foreach (var change in availableToMigrate.Take(outputLimit)) + { + outputSize++; + table.AddRow( + change.GetDateTime().ToString(DateFormat), + Truncate(change.GetRevision().AsString(), RevisionMaxLength, ""), + Truncate(change.FirstLineMessage(), DescriptionMaxLength, "..."), + Truncate(change.GetAuthor().ToString(), AuthorMaxLength, "...")); + } + foreach (string line in table.Build()) + { + console.Info(line); + } + } + if (outputSize > 100) + { + console.InfoFmt( + "Use %s to limit the output of the command.", GeneralOptions.OutputLimitFlag); + } + } + + // TODO(port): dispatch InfoFinishedEvent via eventMonitors() once the monitor package is + // ported. + return hasAvailableChanges; + } + + private static Info GetInfo(string migrationName, Config.Config config) => + config.GetMigration(migrationName).GetInfo(); + + /// Truncates to , appending the truncation + /// indicator (equivalent to Guava's Ascii.truncate). + private static string Truncate(string value, int maxLength, string indicator) + { + if (value.Length <= maxLength) + { + return value; + } + int truncationLength = maxLength - indicator.Length; + return value.Substring(0, Math.Max(0, truncationLength)) + indicator; + } + + public string Name => "info"; +} diff --git a/src/Copybara.Cli/Main.cs b/src/Copybara.Cli/Main.cs new file mode 100644 index 000000000..f04f2bca4 --- /dev/null +++ b/src/Copybara.Cli/Main.cs @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara; +using Copybara.Common; +using Copybara.Config; +using Copybara.Exceptions; +using Copybara.Profiler; +using Copybara.Util; +using Copybara.Util.Console; +using Microsoft.Extensions.Logging; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Cli; + +/// +/// Main class that invokes Copybara from the command line. +/// +/// This class should only know about how to validate and parse command-line arguments in order +/// to invoke Copybara. +/// +public class Main +{ + public const string BuildLabel = "Build label"; + + // These flags are read before the arg parser is initialized, because of the console lifecycle. + // They mirror the (internal) constants on GeneralOptions. + private const string ConsoleFilePathFlag = "--console-file-path"; + private const string ConsoleFileFlushIntervalFlag = "--console-file-flush-interval"; + private static readonly TimeSpan DefaultConsoleFileFlushInterval = TimeSpan.FromSeconds(30); + + /// + /// Commands whose config-file arguments should be parsed, mapping to whether they consume a + /// source ref. + /// + private static readonly ImmutableDictionary + CommandNamesThatUseConfigFilesToUseSourceRef = + new Dictionary + { + ["migrate"] = true, + ["info"] = false, + ["validate"] = false, + }.ToImmutableDictionary(); + + /// The environment, typically the process environment variables. Injected for tests. + protected readonly IReadOnlyDictionary Environment; + + protected Profiler.Profiler? Profiler; + protected ArgParser? ArgParser; + + private Console? _console; + + public Main() + : this(GetSystemEnvironment()) + { + } + + public Main(IReadOnlyDictionary environment) + { + Environment = Preconditions.CheckNotNull(environment); + } + + public static int Main_(string[] args) => (int)new Main().Run(args); + + public ExitCode Run(string[] args) + { + // We need a console before parsing the args because it could fail with wrong arguments and + // we need to show the error. + _console = GetConsole(args); + Console console = _console; + + console.StartupMessage(GetVersion()); + console.VerboseFmt("Running: %s", string.Join(' ', args)); + + CommandResult result = RunInternal(args, console); + try + { + Shutdown(result); + } + catch (Exception e) + { + HandleUnexpectedError(console, "Execution was interrupted.", args, e); + } + + return result.ExitCode; + } + + /// Finds out about verbose output before the arg parser has been initialized. + protected static bool IsVerbose(string[] args) => + args.Any(s => s == "-v" || s == "--verbose"); + + /// Finds out if logging is enabled before the arg parser has been initialized. + protected static bool IsEnableLogging(string[] args) => !args.Contains("--nologging"); + + /// + /// Finds a flag value before the arg parser is initialized. Returns null if the flag is not + /// present or has no value ('=' and ' ' accepted as separators). Does not support arity 0 flags. + /// + protected static string? FindFlagValue(string[] args, string flagName) + { + for (int index = 0; index < args.Length; index++) + { + if (args[index] == flagName) + { + if (index < args.Length - 1 && !args[index + 1].StartsWith('-')) + { + return args[index + 1]; + } + return null; + } + if (args[index].StartsWith(flagName + "=", StringComparison.Ordinal)) + { + return args[index].Substring(flagName.Length + 1); + } + } + return null; + } + + /// The exit code and the command executed. + protected sealed record CommandResult( + ExitCode ExitCode, ICopybaraCmd? Command, CommandEnv? CommandEnv); + + /// + /// Runs the command and returns the . Also responsible for the exception + /// handling/logging. + /// + private CommandResult RunInternal(string[] args, Console console) + { + CommandEnv? commandEnv = null; + ICopybaraCmd? subcommand = null; + + try + { + ModuleSet moduleSet = NewModuleSet(Environment, console); + + var mainArgs = new MainArguments(args); + Options options = moduleSet.GetOptions(); + + ArgParser = new ArgParser(); + ArgParser.AddObjects(options.GetAll().Cast()); + ArgParser.AddObject(mainArgs); + mainArgs.Unnamed = ArgParser.Parse(args).ToList(); + + string version = GetVersion(); + + IConfigLoaderProvider configLoaderProvider = NewConfigLoaderProvider(moduleSet); + + ImmutableDictionary commands = + GetCommands(moduleSet, configLoaderProvider) + .ToImmutableDictionary(c => c.Name, c => c); + + MainArguments.CommandWithArgs cmdToRun = + mainArgs.ParseCommand(commands, commands["migrate"]); + subcommand = cmdToRun.Subcommand; + + WarnAboutPossibleFlags(cmdToRun, console); + + InitEnvironment(options, cmdToRun.Subcommand, args); + + GeneralOptions generalOptions = options.Get(); + string baseWorkdir = mainArgs.GetBaseWorkdir(generalOptions, generalOptions.GetFileSystem()); + + commandEnv = new CommandEnv(baseWorkdir, options, cmdToRun.Args, mainArgs); + if (CommandNamesThatUseConfigFilesToUseSourceRef.TryGetValue(subcommand.Name, out bool useSourceRef)) + { + commandEnv.ParseConfigFileArgs(subcommand, useSourceRef); + } + + console.VerboseFmt("Current working directory: %s", generalOptions.GetCwd()); + generalOptions.GetConsole().ProgressFmt("Running %s", subcommand.Name); + + ExitCode exitCode = subcommand.Run(commandEnv); + return new CommandResult(exitCode, subcommand, commandEnv); + } + catch (CommandLineException e) + { + Consoles.PrintCauseChain(LogLevel.Warning, console, args, e); + console.Error("Try 'copybara help'."); + return new CommandResult(ExitCode.CommandLineError, subcommand, commandEnv); + } + catch (EmptyChangeException e) + { + // This is not necessarily an error. Maybe the tool was run previously and there are no + // new changes to import. (EmptyChangeException derives from ValidationException, so this + // must be caught before ValidationException.) + console.Warn(e.Message); + return new CommandResult(ExitCode.NoOp, subcommand, commandEnv); + } + catch (ValidationException e) + { + Consoles.PrintCauseChain(LogLevel.Warning, console, args, e); + return new CommandResult(ExitCode.ConfigurationError, subcommand, commandEnv); + } + catch (RepoException e) + { + Consoles.PrintCauseChain(LogLevel.Error, console, args, e); + if (e.InnerException is OperationCanceledException) + { + return new CommandResult(ExitCode.Interrupted, subcommand, commandEnv); + } + return new CommandResult(ExitCode.RepositoryError, subcommand, commandEnv); + } + catch (IOException e) + { + HandleUnexpectedError(console, e.Message, args, e); + return new CommandResult(ExitCode.EnvironmentError, subcommand, commandEnv); + } + catch (Exception e) + { + // This usually indicates a serious programming error that will require Copybara team + // intervention. Print stack trace without concern for presentation. + System.Console.Error.WriteLine(e); + HandleUnexpectedError(console, "Unexpected error: " + e.Message, args, e); + return new CommandResult(ExitCode.InternalError, subcommand, commandEnv); + } + } + + private void WarnAboutPossibleFlags(MainArguments.CommandWithArgs cmdToRun, Console console) + { + var possibleFlags = cmdToRun.Args + .Where(arg => arg.StartsWith("--", StringComparison.Ordinal)) + .ToImmutableArray(); + if (possibleFlags.IsEmpty) + { + return; + } + + IReadOnlyList allNames = ArgParser?.AllFlagNames ?? Array.Empty(); + foreach (string possibleFlag in possibleFlags) + { + var candidates = allNames + .Where(s => FlagDistance(s, possibleFlag) <= 1) + .OrderBy(s => s, StringComparer.Ordinal) + .Distinct() + .ToImmutableArray(); + if (candidates.IsEmpty) + { + console.WarnFmt( + "Argument '%s' looks like a flag, but was not parsed as one, is this" + + " intentional?", + possibleFlag); + } + else + { + console.WarnFmt( + "Argument '%s' looks like a flag, but was not parsed as one, did you mean one" + + " of %s?", + possibleFlag, "[" + string.Join(", ", candidates) + "]"); + } + } + } + + /// Naive algorithm to propose similar flags (dropped pre-/suffixes). + private static int FlagDistance(string flag, string input) + { + var flagSet = flag.Split('_', '-').Select(s => s.ToLowerInvariant()).ToHashSet(); + var inputSet = input.Split('_', '-').Select(s => s.ToLowerInvariant()).ToHashSet(); + return inputSet.Count - inputSet.Intersect(flagSet).Count(); + } + + public IReadOnlyList GetCommands( + ModuleSet moduleSet, IConfigLoaderProvider configLoaderProvider) + { + ConfigValidator validator = GetConfigValidator(moduleSet.GetOptions()); + Action consumer = GetMigrationRanConsumer(); + // TODO(port): OnboardCmd, GeneratorCmd, RegenerateCmd are not ported yet and are omitted. + return new ICopybaraCmd[] + { + new MigrateCmd(validator, consumer, configLoaderProvider, moduleSet), + new InfoCmd(configLoaderProvider, NewInfoContextProvider()), + new ValidateCmd(validator, consumer, configLoaderProvider), + new HelpCmd(this), + new VersionCmd(this), + }; + } + + /// Returns a short string representing the version of the binary. + protected virtual string GetVersion() + { + var buildInfo = GetBuildInfo(); + return buildInfo.TryGetValue(BuildLabel, out var label) ? label : "Unknown version"; + } + + private static ImmutableDictionary GetBuildInfo() + { + // TODO(port): upstream loads /build-data.properties from resources. Not wired up yet. + return ImmutableDictionary.Empty; + } + + /// Returns a string describing who and when the binary was built. + protected virtual string GetBinaryInfo() => + string.Join("\n", GetBuildInfo().Select(kv => $"{kv.Key}: {kv.Value}")); + + protected virtual Action GetMigrationRanConsumer() => _ => { }; + + protected virtual ConfigValidator GetConfigValidator(Options options) => + new DefaultConfigValidator(); + + private sealed class DefaultConfigValidator : ConfigValidator + { + } + + /// Returns a new module set. + protected virtual ModuleSet NewModuleSet( + IReadOnlyDictionary environment, Console console) + { + string fsRoot = Path.GetPathRoot(Directory.GetCurrentDirectory()) ?? "/"; + return new ModuleSupplier(environment, fsRoot, console).Create(); + } + + protected virtual IConfigLoaderProvider NewConfigLoaderProvider(ModuleSet moduleSet) + { + GeneralOptions generalOptions = moduleSet.GetOptions().Get(); + return new FuncConfigLoaderProvider((configPath, sourceRef) => + new ConfigLoader( + moduleSet, + CreateConfigFileWithHeuristic( + ValidateLocalConfig(generalOptions, configPath), + generalOptions.GetConfigRoot()), + generalOptions.GetStarlarkMode())); + } + + protected virtual IContextProvider NewInfoContextProvider() => new InfoContextProvider(); + + private sealed class InfoContextProvider : IContextProvider + { + public IReadOnlyDictionary GetContext( + Copybara.Config.Config config, + ConfigFileArgs configFileArgs, + IConfigLoaderProvider configLoaderProvider, + Console console) => + new Dictionary { ["copybara_config"] = config.GetLocation() }; + } + + /// + /// Validates that the passed config file is correct (exists, right filename, etc.) and returns + /// its absolute path. + /// + /// + /// + protected virtual string ValidateLocalConfig(GeneralOptions generalOptions, string configLocation) + { + string configPath = Path.GetFullPath(configLocation); + string? fileName = Path.GetFileName(configPath); + ValidationException.CheckCondition( + !string.IsNullOrEmpty(fileName), + "The configuration path '{0}' is not a file.", + configPath); + ValidationException.CheckCondition( + fileName == MainArguments.CopybaraSkylarkConfigFilename, + "Copybara config file filename should be '{0}' but it is '{1}'.", + MainArguments.CopybaraSkylarkConfigFilename, + fileName!); + + if (!File.Exists(configPath)) + { + throw new CommandLineException("Configuration file not found: " + configPath); + } + return configPath; + } + + /// + /// Finds the root path for resolving configuration file paths. Assumes that the .git-containing + /// directory is the root path. + /// + protected virtual PathBasedConfigFile CreateConfigFileWithHeuristic( + string configPath, string? commandLineRoot) + { + if (commandLineRoot != null) + { + return new PathBasedConfigFile(configPath, commandLineRoot, identifierPrefix: null); + } + string? parent = Path.GetDirectoryName(configPath); + while (parent != null) + { + if (Directory.Exists(Path.Combine(parent, ".git"))) + { + return new PathBasedConfigFile(configPath, parent, identifierPrefix: null); + } + parent = Path.GetDirectoryName(parent); + } + return new PathBasedConfigFile(configPath, rootPath: null, identifierPrefix: null); + } + + protected virtual Console GetConsole(string[] args) + { + bool verbose = IsVerbose(args); + Console console; + if (System.Console.IsOutputRedirected || System.Console.IsInputRedirected) + { + console = LogConsole.WriteOnlyConsole(System.Console.Error, verbose); + } + else if (args.Contains(GeneralOptions.Noansi)) + { + console = LogConsole.ReadWriteConsole(System.Console.In, System.Console.Error, verbose); + } + else + { + console = new AnsiConsole(System.Console.In, System.Console.Error, verbose); + } + + string? noPrompt = FindFlagValue(args, GeneralOptions.Noprompt); + if (noPrompt == "true") + { + console = new NoPromptConsole(console, true); + } + + string? maybeConsoleFilePath = FindFlagValue(args, ConsoleFilePathFlag); + if (maybeConsoleFilePath == null) + { + return console; + } + + try + { + string? dir = Path.GetDirectoryName(maybeConsoleFilePath); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + } + catch (IOException) + { + // Could not create parent directories; disable redirecting. + return console; + } + return new FileConsole(console, maybeConsoleFilePath, GetConsoleFlushRate(args)); + } + + /// Returns the console flush rate from the flag, if valid, or the default otherwise. + protected virtual TimeSpan GetConsoleFlushRate(string[] args) + { + string? value = FindFlagValue(args, ConsoleFileFlushIntervalFlag); + return value != null ? DurationConverter.Convert(value) + : DefaultConsoleFileFlushInterval; + } + + /// + /// Hook to allow setting variables that are not run/validation specific, based on options. Called + /// after command-line options are parsed but before a file is read or a run started. + /// + protected virtual void InitEnvironment(Options options, ICopybaraCmd copybaraCmd, string[] rawArgs) + { + GeneralOptions generalOptions = options.Get(); + Profiler = generalOptions.Profiler(); + var profilerListeners = new List + { + new LogProfilerListener(), + new ConsoleProfilerListener(generalOptions.GetConsole()), + }; + Profiler.Init(profilerListeners); + CleanupOutputDir(generalOptions); + } + + protected virtual void CleanupOutputDir(GeneralOptions generalOptions) + { + generalOptions.IoRepoTask( + "clean_outputdir", + () => + { + if (generalOptions.IsNoCleanup()) + { + return null; + } + generalOptions.GetConsole().Progress("Cleaning output directory"); + generalOptions.GetDirFactory().CleanupTempDirs(); + return null; + }); + } + + /// Performs cleanup tasks after executing Copybara. + protected virtual void Shutdown(CommandResult result) + { + if (_console != null) + { + _console.Dispose(); + } + if (Profiler != null) + { + Profiler.Stop(); + } + } + + protected virtual void HandleUnexpectedError( + Console console, string msg, string[] args, Exception e) + { + console.Error(msg + " (" + e + ")"); + } + + internal string Usage() + { + var sb = new System.Text.StringBuilder(); + sb.Append("Copybara version: ").Append(GetVersion()).Append('\n'); + sb.Append("Usage: copybara [subcommand] ").Append(MainArguments.CopybaraSkylarkConfigFilename) + .Append(" [migration_name [source_ref]]\n\n"); + sb.Append("Available subcommands: migrate, info, validate, version, help\n\n"); + if (ArgParser != null) + { + sb.Append("Flags:\n"); + foreach (var (names, description) in ArgParser.Descriptions.OrderBy(d => d.Names, StringComparer.Ordinal)) + { + sb.Append(" ").Append(names).Append("\n ").Append(description).Append('\n'); + } + } + sb.Append("\nExample:\n copybara ").Append(MainArguments.CopybaraSkylarkConfigFilename) + .Append(" origin/main\n"); + return sb.ToString(); + } + + private static IReadOnlyDictionary GetSystemEnvironment() + { + var dict = new Dictionary(StringComparer.Ordinal); + foreach (System.Collections.DictionaryEntry e in System.Environment.GetEnvironmentVariables()) + { + dict[(string)e.Key] = (string?)e.Value ?? ""; + } + return dict; + } + + /// Prints the Copybara version. + private sealed class VersionCmd : ICopybaraCmd + { + private readonly Main _main; + + public VersionCmd(Main main) => _main = main; + + public ExitCode Run(CommandEnv commandEnv) + { + commandEnv.GetOptions().Get().GetConsole().Info(_main.GetBinaryInfo()); + return ExitCode.Success; + } + + public string Name => "version"; + } + + /// Prints the help message. + private sealed class HelpCmd : ICopybaraCmd + { + private readonly Main _main; + + public HelpCmd(Main main) => _main = Preconditions.CheckNotNull(main); + + public ExitCode Run(CommandEnv commandEnv) + { + commandEnv.GetOptions().Get().GetConsole().Info(_main.Usage()); + return ExitCode.Success; + } + + public string Name => "help"; + } +} diff --git a/src/Copybara.Cli/MainArguments.cs b/src/Copybara.Cli/MainArguments.cs new file mode 100644 index 000000000..e291cd335 --- /dev/null +++ b/src/Copybara.Cli/MainArguments.cs @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; + +namespace Copybara.Cli; + +/// +/// Arguments which are unnamed (i.e. positional) or must be evaluated inside . +/// +public sealed class MainArguments +{ + public const string CopybaraSkylarkConfigFilename = "copy.bara.sky"; + + private readonly ImmutableArray _rawArgs; + + public MainArguments(IEnumerable rawArgs) + { + _rawArgs = rawArgs.ToImmutableArray(); + } + + /// The positional arguments parsed off the command line (config, migration, refs). + public List Unnamed { get; set; } = new(); + + [Flag( + "--work-dir", + "Directory where all the transformations will be performed. By default a temporary" + + " directory.")] + public string? BaseWorkdir { get; set; } + + public IReadOnlyList GetRawArgs() => _rawArgs; + + /// + /// Returns the base working directory. This method should not be accessed directly by any other + /// class but . + /// + public string GetBaseWorkdir(GeneralOptions generalOptions, string fileSystemRoot) + { + _ = fileSystemRoot; + string workdirPath = BaseWorkdir == null + ? generalOptions.GetDirFactory().NewTempDir("workdir") + : Path.GetFullPath(BaseWorkdir); + + if (File.Exists(workdirPath) && !Directory.Exists(workdirPath)) + { + throw new IOException($"'{workdirPath}' exists and is not a directory"); + } + + if (Directory.Exists(workdirPath) && !IsDirEmpty(workdirPath)) + { + System.Console.Error.WriteLine($"WARNING: {workdirPath} is not empty"); + } + + return workdirPath; + } + + private static bool IsDirEmpty(string directory) => + !Directory.EnumerateFileSystemEntries(directory).Any(); + + /// + /// Resolves the subcommand and its remaining args from the positional arguments, mirroring + /// upstream's config-vs-command disambiguation logic. + /// + /// + public CommandWithArgs ParseCommand( + IReadOnlyDictionary commands, ICopybaraCmd defaultCmd) + { + if (Unnamed.Count == 0) + { + return new CommandWithArgs(defaultCmd, ImmutableArray.Empty); + } + + string firstArg = Unnamed[0]; + // Default command might take a config file as param. + if (firstArg.EndsWith(CopybaraSkylarkConfigFilename, StringComparison.Ordinal)) + { + return new CommandWithArgs(defaultCmd, Unnamed.ToImmutableArray()); + } + + if (firstArg.Contains(CopybaraSkylarkConfigFilename + ':', StringComparison.Ordinal)) + { + var args = ImmutableArray.CreateBuilder(); + args.AddRange(SplitConfigArg(firstArg)); + args.AddRange(Unnamed.Skip(1)); + return new CommandWithArgs(defaultCmd, args.ToImmutable()); + } + + string key = firstArg.ToLowerInvariant(); + if (!commands.ContainsKey(key)) + { + var available = new SortedSet(commands.Keys, StringComparer.Ordinal); + throw new CommandLineException( + $"Invalid subcommand '{firstArg}'. Available commands: [{string.Join(", ", available)}]"); + } + + if (Unnamed.Count == 1) + { + return new CommandWithArgs(commands[key], ImmutableArray.Empty); + } + + var rest = ImmutableArray.CreateBuilder(); + rest.AddRange(SplitConfigArg(Unnamed[1])); + rest.AddRange(Unnamed.Skip(2)); + return new CommandWithArgs(commands[key], rest.ToImmutable()); + } + + private static IReadOnlyList SplitConfigArg(string arg) + { + int idx = arg.IndexOf("copy.bara.sky:", StringComparison.Ordinal); + if (idx < 0) + { + return ImmutableArray.Create(arg); + } + string head = arg.Substring(0, idx) + "copy.bara.sky"; + string tail = arg.Substring(idx + "copy.bara.sky:".Length); + return ImmutableArray.Create(head, tail); + } + + /// A subcommand and the remaining (config/migration/ref) arguments for it. + public sealed class CommandWithArgs + { + internal CommandWithArgs(ICopybaraCmd subcommand, ImmutableArray args) + { + Subcommand = Preconditions.CheckNotNull(subcommand); + Args = args; + } + + public ICopybaraCmd Subcommand { get; } + + public IReadOnlyList Args { get; } + } +} diff --git a/src/Copybara.Cli/MigrateCmd.cs b/src/Copybara.Cli/MigrateCmd.cs new file mode 100644 index 000000000..6f0da0b38 --- /dev/null +++ b/src/Copybara.Cli/MigrateCmd.cs @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara; +using Copybara.Common; +using Copybara.Config; +using Copybara.Exceptions; +using Copybara.Util; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Cli; + +/// +/// Executes the migration for the given config. +/// +public sealed class MigrateCmd : ICopybaraCmd +{ + private readonly ConfigValidator _configValidator; + private readonly Action _migrationRanConsumer; + private readonly IConfigLoaderProvider _configLoaderProvider; + private readonly ModuleSet _moduleSet; + + public MigrateCmd( + ConfigValidator configValidator, + Action migrationRanConsumer, + IConfigLoaderProvider configLoaderProvider, + ModuleSet moduleSet) + { + _configValidator = Preconditions.CheckNotNull(configValidator); + _migrationRanConsumer = Preconditions.CheckNotNull(migrationRanConsumer); + _configLoaderProvider = Preconditions.CheckNotNull(configLoaderProvider); + _moduleSet = moduleSet; + } + + public ExitCode Run(CommandEnv commandEnv) + { + ConfigFileArgs configFileArgs = commandEnv.GetConfigFileArgs()!; + IReadOnlyList sourceRefs = configFileArgs.GetSourceRefs(); + string workflowName = configFileArgs.GetWorkflowName(); + UpdateEnvironment(workflowName); + GeneralOptions generalOptions = commandEnv.GetOptions().Get(); + Console console = generalOptions.GetConsole(); + console.VerboseFmt("Executing workflow '%s'", workflowName); + Run( + commandEnv.GetOptions(), + _configLoaderProvider.NewLoader( + configFileArgs.GetConfigPath(), + sourceRefs.Count == 1 ? sourceRefs[0] : null), + workflowName, + commandEnv.GetWorkdir(), + sourceRefs); + return ExitCode.Success; + } + + /// Runs the migration specified by . + private void Run( + Options options, + ConfigLoader configLoader, + string migrationName, + string workdir, + IReadOnlyList sourceRefs) + { + Config.Config config = LoadConfig(options, configLoader, migrationName); + + IMigration migration = config.GetMigration(migrationName); + + if (!options.Get().IsReadConfigFromChange()) + { + _migrationRanConsumer(migration); + migration.Run(workdir, sourceRefs); + return; + } + + ValidationException.CheckCondition( + configLoader.SupportsLoadForRevision(), + "{0} flag is not supported for the origin/config file path", + "--read-config-from-change"); + + // A safeguard, mirror workflows are not supported in the service anyway. + ValidationException.CheckCondition( + migration is Workflow, + "Flag --read-config-from-change is not supported for non-workflow migrations: {0}", + migrationName); + _migrationRanConsumer(migration); + + // TODO(port): ReadConfigFromChangeWorkflow is not ported yet. When it lands, replace the + // fallthrough below with the equivalent of: + // new ReadConfigFromChangeWorkflow(workflow, options, configLoader, configValidator) + // .run(workdir, sourceRefs); + throw new ValidationException( + "--read-config-from-change is not yet supported in the .NET port."); + } + + private Config.Config LoadConfig(Options options, ConfigLoader configLoader, string migrationName) + { + GeneralOptions generalOptions = options.Get(); + Console console = generalOptions.GetConsole(); + Config.Config config = configLoader.Load(console); + console.Progress("Validating configuration"); + ValidationResult result = _configValidator.Validate(config, migrationName); + if (!result.HasErrors()) + { + return config; + } + + foreach (string error in result.GetErrors()) + { + console.Error(error); + } + console.Error("Configuration is invalid."); + throw new ValidationException( + "Error validating configuration: Configuration is invalid."); + } + + private void UpdateEnvironment(string migrationName) + { + foreach (object module in _moduleSet.GetModules().Values) + { + // We mutate the module per file loaded. Not ideal but it is the best we can do. + if (module is ILabelsAwareModule m) + { + m.SetWorkflowName(migrationName); + } + } + } + + public string Name => "migrate"; +} diff --git a/src/Copybara.Cli/Program.cs b/src/Copybara.Cli/Program.cs index 827627b78..67eb8b1b1 100644 --- a/src/Copybara.Cli/Program.cs +++ b/src/Copybara.Cli/Program.cs @@ -14,9 +14,10 @@ * limitations under the License. */ -// Entry point for the `copybara` .NET tool. -// The full CLI (arg parsing, command dispatch, Main orchestration) is ported in Phase 7. -// This is a placeholder that keeps the tool runnable while the engine is built out. +// Entry point for the `copybara` .NET tool. Delegates to Copybara.Cli.Main, which mirrors the +// orchestration flow of the upstream com.google.copybara.Main class. -Console.WriteLine("Copybara (.NET port) — under construction. See TODO.md for status."); -return 0; +using Copybara.Cli; +using Copybara.Util; + +return (int)new Main().Run(args); diff --git a/src/Copybara.Cli/ValidateCmd.cs b/src/Copybara.Cli/ValidateCmd.cs new file mode 100644 index 000000000..a0fcb6920 --- /dev/null +++ b/src/Copybara.Cli/ValidateCmd.cs @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara; +using Copybara.Common; +using Copybara.Config; +using Copybara.Exceptions; +using Copybara.Util; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Cli; + +/// +/// Validates that the configuration is correct. +/// +public sealed class ValidateCmd : ICopybaraCmd +{ + private readonly ConfigValidator _configValidator; + private readonly IConfigLoaderProvider _configLoaderProvider; + + public ValidateCmd( + ConfigValidator configValidator, + Action migrationRanConsumer, + IConfigLoaderProvider configLoaderProvider) + { + _ = migrationRanConsumer; + _configValidator = Preconditions.CheckNotNull(configValidator); + _configLoaderProvider = Preconditions.CheckNotNull(configLoaderProvider); + } + + public ExitCode Run(CommandEnv commandEnv) + { + ConfigFileArgs configFileArgs = commandEnv.GetConfigFileArgs()!; + ConfigLoader configLoader = + _configLoaderProvider.NewLoader( + configFileArgs.GetConfigPath(), configFileArgs.GetSourceRef()); + ValidationResult result = + Validate(commandEnv.GetOptions(), configLoader, configFileArgs.GetWorkflowName()); + + Console console = commandEnv.GetOptions().Get().GetConsole(); + foreach (var message in result.GetAllMessages()) + { + switch (message.GetLevel()) + { + case ValidationResult.Level.WARNING: + console.Warn(message.GetMessage()); + break; + case ValidationResult.Level.ERROR: + console.Error(message.GetMessage()); + break; + } + } + + if (result.HasErrors()) + { + console.ErrorFmt("Configuration '%s' is invalid.", configLoader.Location()); + return ExitCode.ConfigurationError; + } + + console.InfoFmt("Configuration '%s' is valid.", configLoader.Location()); + return ExitCode.Success; + } + + /// + /// Validates that the configuration is correct and that there is a valid migration specified by + /// . + /// + /// Note that, besides validating the specific migration, all the configuration will be + /// validated syntactically. + /// + private ValidationResult Validate(Options options, ConfigLoader configLoader, string migrationName) + { + Console console = options.Get().GetConsole(); + var resultBuilder = new ValidationResult.Builder(); + try + { + Config.Config config = configLoader.Load(console); + resultBuilder.Append(_configValidator.Validate(config, migrationName)); + } + catch (ValidationException e) + { + // The validate subcommand should not throw Validation exceptions but log a result. + var error = new System.Text.StringBuilder(e.Message).Append('\n'); + Exception? cause = e.InnerException; + while (cause != null) + { + error.Append(" CAUSED BY: ").Append(cause.Message).Append('\n'); + cause = cause.InnerException; + } + resultBuilder.Error(error.ToString()); + } + + return resultBuilder.Build(); + } + + public string Name => "validate"; +} diff --git a/src/Copybara.Common/ImmutableSetMultimap.cs b/src/Copybara.Common/ImmutableSetMultimap.cs new file mode 100644 index 000000000..376ead69f --- /dev/null +++ b/src/Copybara.Common/ImmutableSetMultimap.cs @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections; +using System.Collections.Immutable; + +namespace Copybara.Common; + +/// +/// An immutable multimap where the values for each key form a set (no duplicate values per key), +/// mirroring the subset of Guava's ImmutableSetMultimap that Copybara relies on. Key insertion +/// order is preserved; per key, values keep their first-seen insertion order and duplicates are +/// dropped. +/// +public sealed class ImmutableSetMultimap : IEnumerable> + where TKey : notnull +{ + private readonly ImmutableArray> _entries; + private readonly ImmutableDictionary> _index; + + private ImmutableSetMultimap( + ImmutableArray> entries, + ImmutableDictionary> index) + { + _entries = entries; + _index = index; + } + + private static readonly ImmutableSetMultimap EmptyInstance = + new(ImmutableArray>.Empty, + ImmutableDictionary>.Empty); + + public static ImmutableSetMultimap Empty => EmptyInstance; + + /// All values associated with (empty if none). + public ImmutableHashSet this[TKey key] => + _index.TryGetValue(key, out var values) ? values : ImmutableHashSet.Empty; + + /// All values associated with (empty if none). + public ImmutableHashSet Get(TKey key) => this[key]; + + public bool ContainsKey(TKey key) => _index.ContainsKey(key); + + public bool ContainsEntry(TKey key, TValue value) => + _index.TryGetValue(key, out var values) && values.Contains(value); + + public IEnumerable Keys => _index.Keys; + + public int Count => _entries.Length; + + public bool IsEmpty => _entries.IsEmpty; + + /// Returns the entries as a map from key to its set of values. + public ImmutableDictionary> AsMap() => _index; + + public IEnumerator> GetEnumerator() => + ((IEnumerable>)_entries).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public static Builder CreateBuilder() => new(); + + /// Builder that accumulates entries, dropping duplicate (key, value) pairs. + public sealed class Builder + { + private readonly List> _entries = new(); + private readonly HashSet<(TKey, TValue)> _seen = new(); + + public Builder Put(TKey key, TValue value) + { + if (_seen.Add((key, value))) + { + _entries.Add(new KeyValuePair(key, value)); + } + return this; + } + + public Builder PutAll(TKey key, IEnumerable values) + { + foreach (var value in values) + { + Put(key, value); + } + return this; + } + + public Builder PutAll(ImmutableSetMultimap multimap) + { + foreach (var entry in multimap) + { + Put(entry.Key, entry.Value); + } + return this; + } + + public ImmutableSetMultimap Build() + { + var entries = _entries.ToImmutableArray(); + var perKey = new Dictionary.Builder>(); + var order = new List(); + foreach (var entry in entries) + { + if (!perKey.TryGetValue(entry.Key, out var setBuilder)) + { + setBuilder = ImmutableHashSet.CreateBuilder(); + perKey[entry.Key] = setBuilder; + order.Add(entry.Key); + } + setBuilder.Add(entry.Value); + } + + var indexBuilder = ImmutableDictionary.CreateBuilder>(); + foreach (var key in order) + { + indexBuilder[key] = perKey[key].ToImmutable(); + } + + return new ImmutableSetMultimap(entries, indexBuilder.ToImmutable()); + } + } +} diff --git a/src/Copybara.Common/Preconditions.cs b/src/Copybara.Common/Preconditions.cs index 33c9d1bae..fb2b39300 100644 --- a/src/Copybara.Common/Preconditions.cs +++ b/src/Copybara.Common/Preconditions.cs @@ -55,6 +55,37 @@ public static T CheckNotNull([NotNull] T? reference, string format, params ob return reference; } + /// Ensures a nullable value type is not null, returning its value. + public static T CheckNotNull([NotNull] T? reference) where T : struct + { + if (reference is null) + { + throw new ArgumentNullException(); + } + return reference.Value; + } + + /// Ensures a nullable value type is not null, returning its value. + public static T CheckNotNull([NotNull] T? reference, object? errorMessage) where T : struct + { + if (reference is null) + { + throw new ArgumentNullException(null, errorMessage?.ToString()); + } + return reference.Value; + } + + /// Ensures a nullable value type is not null, returning its value. + public static T CheckNotNull([NotNull] T? reference, string format, params object?[] args) + where T : struct + { + if (reference is null) + { + throw new ArgumentNullException(null, string.Format(format, args)); + } + return reference.Value; + } + /// Ensures the truth of an expression involving parameters to the calling method. public static void CheckArgument([DoesNotReturnIf(false)] bool expression) { diff --git a/src/Copybara.Core/Action/ActionContext.cs b/src/Copybara.Core/Action/ActionContext.cs new file mode 100644 index 000000000..b660507a8 --- /dev/null +++ b/src/Copybara.Core/Action/ActionContext.cs @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Config; +using Copybara.Effect; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Transform; +using Starlark.Annot; +using Starlark.Eval; +using ConsoleT = Copybara.Util.Console.Console; +using StarlarkRt = Starlark.Eval.Starlark; +using Sequence = Starlark.Eval.Sequence; + +namespace Copybara.Action; + +/// +/// A StarlarkContext for running Actions. Port of com.google.copybara.action.ActionContext. +/// +public abstract class ActionContext : ISkylarkContext, IStarlarkValue + where T : ISkylarkContext +{ + protected readonly List NewDestinationEffects = new(); + private ActionResult? _actionResult; + + protected ActionContext( + IAction action, + SkylarkConsole console, + IReadOnlyDictionary labels, + Dict @params) + { + Action = Preconditions.CheckNotNull(action); + Console = Preconditions.CheckNotNull(console); + Labels = Preconditions.CheckNotNull(labels); + Params = Preconditions.CheckNotNull(@params); + } + + /// The action this context runs. + protected IAction Action { get; } + + /// The console used to report errors or warnings. + protected SkylarkConsole Console { get; } + + /// The CLI labels passed to the migration. + protected IReadOnlyDictionary Labels { get; } + + private Dict Params { get; } + + [StarlarkMethod( + "action_name", + Doc = "The name of the current action.", + StructField = true)] + public string GetActionName() => Action.GetName(); + + [StarlarkMethod( + "console", + Doc = "Get an instance of the console to report errors or warnings", + StructField = true)] + public ConsoleT GetConsole() => Console; + + [StarlarkMethod( + "params", + Doc = "Parameters for the function if created with core.action", + StructField = true)] + public Dict GetParams() => Params; + + [StarlarkMethod("success", Doc = "Returns a successful action result.")] + public ActionResult Success() => ActionResult.SuccessResult(); + + [StarlarkMethod( + "noop", + Doc = "Returns a no op action result with an optional message.")] + public ActionResult Noop( + [Param( + Name = "msg", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Doc = "The no op message", + DefaultValue = "None")] + object? noopMsg) => + ActionResult.NoopResult(SkylarkUtil.ConvertFromNoneable(noopMsg, null)); + + [StarlarkMethod( + "error", + Doc = "Returns an error action result.")] + public ActionResult Error( + [Param(Name = "msg", Doc = "The error message")] string errorMsg) => + ActionResult.ErrorResult(errorMsg); + + /// Return the new s created by this context. + public IReadOnlyList GetNewDestinationEffects() => + NewDestinationEffects.ToImmutableArray(); + + [StarlarkMethod( + "cli_labels", + Doc = "Access labels that a user passes through flag '--labels'. " + + "For example: --labels=foo:value1,bar:value2. Then it can access in this way:" + + "cli_labels['foo'].", + StructField = true)] + public Dict GetCliLabels() => + Dict.ImmutableCopyOf( + Labels.Select(kv => new KeyValuePair(kv.Key, kv.Value))); + + [StarlarkMethod( + "record_effect", + Doc = "Records an effect of the current action.")] + public void RecordEffect( + [Param(Name = "summary", Doc = "The summary of this effect", Named = true)] + string summary, + [Param( + Name = "origin_refs", + AllowedTypes = new[] { typeof(ISequence) }, + Doc = "The origin refs", + Named = true)] + ISequence originRefs, + [Param(Name = "destination_ref", Doc = "The destination ref", Named = true)] + DestinationEffect.DestinationRef destinationRef, + [Param( + Name = "errors", + AllowedTypes = new[] { typeof(ISequence) }, + DefaultValue = "[]", + Doc = "An optional list of errors", + Named = true)] + ISequence errors, + [Param( + Name = "type", + Doc = "The type of migration effect (CREATED, UPDATED, NOOP," + + " NOOP_AGAINST_PENDING_CHANGE, INSUFFICIENT_APPROVALS, ERROR, STARTED).", + DefaultValue = "\"UPDATED\"", + Named = true)] + string typeStr) + { + var type = SkylarkUtil.StringToEnum("type", typeStr); + NewDestinationEffects.Add( + new DestinationEffect( + type, + summary, + Sequence.Cast(originRefs, "origin_refs"), + destinationRef, + Sequence.Cast(errors, "errors"))); + } + + public abstract T WithParams(Dict @params); + + public void OnFinish(object? result, object context) + { + ValidationException.CheckCondition( + result != null, + "Actions must return a result via built-in functions: success(), " + + "error(), noop() return, but '{0}' returned: None", Action.GetName()); + ValidationException.CheckCondition( + result is ActionResult, + "Actions must return a result via built-in functions: success(), " + + "error(), noop() return, but '{0}' returned: {1}", Action.GetName(), result!); + _actionResult = (ActionResult)result!; + switch (_actionResult.GetResult()) + { + case ActionResult.Result.Error: + Console.ErrorFmt( + "Action '{0}' returned error: {1}", Action.GetName(), _actionResult.GetMsg()!); + break; + case ActionResult.Result.NoOp: + Console.InfoFmt( + "Action '{0}' returned noop: {1}", Action.GetName(), _actionResult.GetMsg()!); + break; + case ActionResult.Result.Success: + Console.InfoFmt("Action '{0}' returned success", Action.GetName()); + break; + } + + // Populate effects registered in the action context. This is required because StarlarkAction + // makes a copy of the context to inject the parameters, but that instance is not visible from + // the caller. + if (context is ActionContext other) + { + NewDestinationEffects.AddRange(other.NewDestinationEffects); + } + } + + public ActionResult? GetActionResult() => _actionResult; +} diff --git a/src/Copybara.Core/Action/ActionResult.cs b/src/Copybara.Core/Action/ActionResult.cs new file mode 100644 index 000000000..a3d9a323c --- /dev/null +++ b/src/Copybara.Core/Action/ActionResult.cs @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Action; + +/// +/// Represents the result returned by an . Port of +/// com.google.copybara.action.ActionResult. +/// +[StarlarkBuiltin( + "dynamic.action_result", + Doc = "Result objects created by actions to tell Copybara what happened.")] +public sealed class ActionResult : IStarlarkPrintableValue +{ + private readonly Result _result; + private readonly string? _msg; + + private ActionResult(Result result, string? msg) + { + _result = result; + _msg = msg; + } + + /// Result kind. The Starlark-facing name is the upstream-compatible upper snake case form. + public enum Result + { + Success, + Error, + NoOp, + } + + public static ActionResult SuccessResult() => new(Result.Success, msg: null); + + public static ActionResult ErrorResult(string msg) => new(Result.Error, msg); + + public static ActionResult NoopResult(string? msg) => new(Result.NoOp, msg); + + public Result GetResult() => _result; + + [StarlarkMethod( + "result", + Doc = "The result of this action", + StructField = true)] + public string GetResultForSkylark() => ResultName(_result); + + [StarlarkMethod( + "msg", + Doc = "The message associated with the result", + StructField = true, + AllowReturnNones = true)] + public string? GetMsg() => _msg; + + /// Upstream-compatible name of the result (SUCCESS, ERROR, NO_OP). + private static string ResultName(Result result) => result switch + { + Result.Success => "SUCCESS", + Result.Error => "ERROR", + Result.NoOp => "NO_OP", + _ => result.ToString(), + }; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"ActionResult{{result={ResultName(_result)}, msg={_msg ?? "null"}}}"; +} diff --git a/src/Copybara.Core/Action/IAction.cs b/src/Copybara.Core/Action/IAction.cs new file mode 100644 index 000000000..bb276fb69 --- /dev/null +++ b/src/Copybara.Core/Action/IAction.cs @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Action; + +/// +/// Actions are Starlark functions that receive a context object (that is different depending on where +/// it is used) that expose an API to implement custom logic in Starlark. Port of +/// com.google.copybara.action.Action. +/// +[StarlarkBuiltin( + "dynamic.action", + Doc = + "An action is an Starlark piece of code that does part of a migration. It is used" + + "to define the logic of migration for feedback workflow, on_finish hooks, git.mirror," + + " etc.", + Documented = false)] +public interface IAction : IStarlarkValue +{ + /// + /// Runs the action against the given context. + /// + /// if failure is attributable to user setup. + /// if access to the origin/destination fails. + void Run(ActionContext context) + where T : ISkylarkContext; + + string GetName(); + + /// Returns a key-value list of the options the action was instantiated with. + ImmutableListMultimap Describe(); +} diff --git a/src/Copybara.Core/Action/StarlarkAction.cs b/src/Copybara.Core/Action/StarlarkAction.cs new file mode 100644 index 000000000..60af40ab5 --- /dev/null +++ b/src/Copybara.Core/Action/StarlarkAction.cs @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Action; + +/// +/// An implementation of that delegates to a Starlark function. Port of +/// com.google.copybara.action.StarlarkAction. +/// +public sealed class StarlarkAction : IAction +{ + private readonly string _name; + private readonly IStarlarkCallable _function; + private readonly Dict _params; + private readonly StarlarkThread.PrintHandler _printHandler; + + public StarlarkAction( + string name, + IStarlarkCallable function, + Dict @params, + StarlarkThread.PrintHandler printHandler) + { + _name = name; + _function = Preconditions.CheckNotNull(function); + _params = Preconditions.CheckNotNull(@params); + _printHandler = Preconditions.CheckNotNull(printHandler); + } + + public void Run(ActionContext context) + where T : ISkylarkContext + { + T actionContext = context.WithParams(_params); + using var mu = Mutability.Create("dynamic_action"); + try + { + var thread = StarlarkThread.CreateTransient(mu, StarlarkSemantics.DEFAULT); + thread.SetPrintHandler(_printHandler); + object? result = StarlarkRt.Fastcall( + thread, _function, new object?[] { actionContext }, Array.Empty()); + context.OnFinish(result, actionContext); + } + catch (EvalException e) + { + var cause = e.InnerException; + string error = string.Format( + "Error while executing the skylark transformation {0}: {1}.", + _function.Name, e.Message); + if (cause is RepoException) + { + throw new RepoException(error, cause); + } + throw new ValidationException(error, cause); + } + } + + public string GetName() => _name; + + public ImmutableListMultimap Describe() + { + var builder = ImmutableListMultimap.CreateBuilder(); + foreach (var paramKey in _params.Keys) + { + builder.Put( + paramKey?.ToString() ?? "null", + (paramKey is null ? null : _params.Get(paramKey))?.ToString() ?? "null"); + } + return builder.Build(); + } + + public override string ToString() => $"StarlarkAction{{name={_function.Name}}}"; +} diff --git a/src/Copybara.Core/ActionMigration.cs b/src/Copybara.Core/ActionMigration.cs new file mode 100644 index 000000000..0649f0531 --- /dev/null +++ b/src/Copybara.Core/ActionMigration.cs @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Action; +using Copybara.Common; +using Copybara.Config; +using Copybara.Effect; +using Copybara.Exceptions; +using Copybara.Monitor; +using Copybara.Transform; +using Starlark.Eval; + +namespace Copybara; + +/// A migration that can move code or metadata between endpoints. +public class ActionMigration : IMigration +{ + public const string DestinationEndpointName = "destination"; + + private readonly string _name; + private readonly string? _description; + private readonly ConfigFile _configFile; + private readonly ITrigger _trigger; + private readonly IStructure _endpoints; + private readonly IReadOnlyList _actions; + private readonly GeneralOptions _generalOptions; + private readonly string _mode; + private readonly bool _fileSystem; + private readonly ImmutableArray _definitionStack; + + public ActionMigration( + string name, + string? description, + ConfigFile configFile, + ITrigger trigger, + IStructure endpoints, + IReadOnlyList actions, + GeneralOptions generalOptions, + string mode, + bool fileSystem, + ImmutableArray definitionStack) + { + _name = Preconditions.CheckNotNull(name); + _description = description; + _configFile = Preconditions.CheckNotNull(configFile); + _trigger = Preconditions.CheckNotNull(trigger); + _endpoints = endpoints; + _actions = Preconditions.CheckNotNull(actions); + _generalOptions = Preconditions.CheckNotNull(generalOptions); + _mode = mode; + _fileSystem = fileSystem; + _definitionStack = definitionStack; + } + + public void Run(string workdir, IReadOnlyList sourceRefs) + { + var allResultsBuilder = ImmutableArray.CreateBuilder(); + string suffix = string.Join('_', sourceRefs) + .Replace('/', '_') + .Replace(' ', '_'); + string root = "run/" + _name + "/" + suffix.Substring(0, Math.Min(suffix.Length, 20)); + using (Profiler().Start(root)) + { + foreach (var action in _actions) + { + var effects = new List(); + using (Profiler().Start(action.GetName())) + { + try + { + var console = new SkylarkConsole(_generalOptions.GetConsole()); + EventMonitors().DispatchEvent( + m => m.OnChangeMigrationStarted(new IEventMonitor.ChangeMigrationStartedEvent())); + var context = new ActionMigrationContext( + this, action, _generalOptions.CliLabels(), sourceRefs, console); + if (_fileSystem) + { + context = context.WithFileSystem(workdir); + } + action.Run(context); + effects.AddRange(context.GetNewDestinationEffects()); + var actionResult = context.GetActionResult(); + allResultsBuilder.Add(actionResult); + // First error aborts the execution of the other actions. + ValidationException.CheckCondition( + actionResult.GetResult() != ActionResult.Result.Error, + "{0} migration '{1}' action '{2}' returned error: {3}. Aborting execution.", + Capitalize(_mode), _name, action.GetName(), actionResult.GetMsg()); + } + finally + { + EventMonitors().DispatchEvent(m => m.OnChangeMigrationFinished( + new IEventMonitor.ChangeMigrationFinishedEvent( + effects.ToImmutableArray(), + GetOriginDescription(), + GetDestinationDescription()))); + } + } + } + } + var allResults = allResultsBuilder.ToImmutable(); + // This check also returns true if there are no actions. + if (allResults.All(a => a.GetResult() == ActionResult.Result.NoOp)) + { + string detailedMessage = allResults.Length == 0 + ? "actions field is empty" + : "[" + string.Join(", ", allResults.Select(a => a.GetMsg()).Where(m => m != null)) + "]"; + throw new EmptyChangeException( + $"{Capitalize(_mode)} migration '{_name}' was noop. Detailed messages: {detailedMessage}"); + } + } + + private static string Capitalize(string str) => + str.Substring(0, 1).ToUpperInvariant() + str.Substring(1); + + public string GetName() => _name; + + public string? GetDescription() => _description; + + public string GetModeString() => _mode; + + public ConfigFile GetMainConfigFile() => _configFile; + + public ImmutableListMultimap GetOriginDescription() => _trigger.Describe(); + + public ImmutableListMultimap GetDestinationDescription() + { + // We currently require one endpoint to be the designated destination, all others should be + // read only. + object destination = + Preconditions.CheckNotNull(_endpoints.GetValue(DestinationEndpointName)); + return ((IEndpoint)destination).Describe(); + } + + public IReadOnlyDictionary> GetEndpointDescriptions() + { + var result = ImmutableDictionary.CreateBuilder>(); + foreach (var name in _endpoints.GetFieldNames()) + { + if (name != DestinationEndpointName && _endpoints.GetValue(name) is IEndpoint e) + { + result[name] = e.Describe(); + } + } + return result.ToImmutable(); + } + + /// + /// Returns a multimap containing enough data to fingerprint the actions for validation purposes. + /// + public ImmutableListMultimap> GetActionsDescription() + { + var descriptionBuilder = + ImmutableListMultimap>.CreateBuilder(); + foreach (var action in _actions) + { + descriptionBuilder.Put(action.GetName(), action.Describe()); + } + return descriptionBuilder.Build(); + } + + public IReadOnlyList> GetCredentialDescription() + { + var allCreds = ImmutableArray.CreateBuilder>(); + allCreds.AddRange(_trigger.GetEndpoint().DescribeCredentials("trigger")); + foreach (var name in _endpoints.GetFieldNames()) + { + if (_endpoints.GetValue(name) is IEndpoint e) + { + allCreds.AddRange(e.DescribeCredentials(name)); + } + } + return allCreds.ToImmutable(); + } + + internal ITrigger GetTrigger() => _trigger; + + public IStructure GetEndpoints() => _endpoints; + + public override string ToString() => + $"ActionMigration{{name={_name}, trigger={_trigger}, endpoints={_endpoints}," + + $" actions=[{string.Join(", ", _actions)}]}}"; + + private Profiler.Profiler Profiler() => _generalOptions.Profiler(); + + private IEventMonitor.EventMonitors EventMonitors() => _generalOptions.EventMonitors(); + + public ImmutableArray GetDefinitionStack() => _definitionStack; +} diff --git a/src/Copybara.Core/ActionMigrationContext.cs b/src/Copybara.Core/ActionMigrationContext.cs new file mode 100644 index 000000000..787b36b12 --- /dev/null +++ b/src/Copybara.Core/ActionMigrationContext.cs @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Action; +using Copybara.Common; +using Copybara.Transform; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara; + +/// Skylark context for migrations that can do arbitrary endpoint calls and file manipulations. +[StarlarkBuiltin( + // TODO(b/269526710): Rename this. Update docs + "feedback.context", + Doc = + "Gives access to the feedback migration information and utilities. This context is a " + + "concrete implementation for feedback migrations.")] +public class ActionMigrationContext : ActionContext +{ + private readonly ActionMigration _actionMigration; + private readonly ImmutableArray _refs; + private readonly ActionFileSystem? _fs; + + internal ActionMigrationContext( + ActionMigration actionMigration, + IAction currentAction, + IReadOnlyDictionary labels, + IReadOnlyList refs, + SkylarkConsole console) + : this(actionMigration, currentAction, labels, refs, console, Dict.Empty(), fs: null) + { + } + + private ActionMigrationContext( + ActionMigration actionMigration, + IAction currentAction, + IReadOnlyDictionary labels, + IReadOnlyList refs, + SkylarkConsole console, + Dict @params, + ActionFileSystem? fs) + : base(currentAction, console, labels, @params) + { + _actionMigration = Preconditions.CheckNotNull(actionMigration); + _refs = refs.ToImmutableArray(); + _fs = fs; + } + + [StarlarkMethod( + "origin", + Doc = "An object representing the origin. Can be used to query about the ref or modifying" + + " the origin state", + StructField = true)] + public IEndpoint GetOrigin() => + _actionMigration.GetTrigger().GetEndpoint().WithConsole(GetConsole()); + + // TODO(b/269526710): Deprecate this function and use endpoints instead. + [StarlarkMethod( + "destination", + Doc = "An object representing the destination. Can be used to query or modify the" + + " destination state", + StructField = true)] + public IEndpoint GetDestination() + { + if (_actionMigration.GetEndpoints().GetValue("destination") is IEndpoint e) + { + return e; + } + throw new InvalidOperationException("Expected an endpoint called destination"); + } + + [StarlarkMethod( + "endpoints", + Doc = "An object that gives access to the API of the configured endpoints", + StructField = true, + Documented = false)] + public IStructure GetEndpoints() => _actionMigration.GetEndpoints(); + + // TODO(b/269526710): Deprecate this + [StarlarkMethod( + "feedback_name", + Doc = "DEPRECATED: The name of the Feedback migration calling this action." + + " Use migration_name instead.", + StructField = true)] + public string GetFeedbackName() => _actionMigration.GetName(); + + [StarlarkMethod( + "migration_name", + Doc = "The name of the migration calling this action.", + Documented = false, + StructField = true)] + public string GetMigrationName() => _actionMigration.GetName(); + + [StarlarkMethod( + "refs", + Doc = "A list containing string representations of the entities that triggered the event", + StructField = true)] + public StarlarkList GetRefs() => StarlarkList.ImmutableCopyOf(_refs.Cast()); + + [StarlarkMethod( + "fs", + Doc = "If a migration of type `core.action_migration` sets `filesystem = True`, it gives" + + " access to the underlying migration filesystem to manipulate files.", + Documented = false, + StructField = true)] + public ActionFileSystem GetFs() + { + if (_fs == null) + { + throw StarlarkRt.Errorf( + "Migration '{0}' doesn't have access to the filesystem. Use filesystem = True to" + + " enable it", + GetMigrationName()); + } + return _fs; + } + + public override ActionMigrationContext WithParams(Dict @params) => + new(_actionMigration, Action, Labels, _refs, Console, @params, _fs); + + public ActionMigrationContext WithFileSystem(string checkoutDir) => + new(_actionMigration, Action, Labels, _refs, Console, GetParams(), + new ActionFileSystem(checkoutDir)); + + [StarlarkBuiltin( + "action.filesystem", + Doc = "This object gives access to actions to the filesystem for manipulating files.", + Documented = false)] + public sealed class ActionFileSystem : CheckoutFileSystem + { + public ActionFileSystem(string checkoutDir) + : base(checkoutDir) + { + } + } +} diff --git a/src/Copybara.Core/Approval/ChangeWithApprovals.cs b/src/Copybara.Core/Approval/ChangeWithApprovals.cs new file mode 100644 index 000000000..aca88e83a --- /dev/null +++ b/src/Copybara.Core/Approval/ChangeWithApprovals.cs @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Revision; + +namespace Copybara.Approval; + +/// +/// Approvals for a change reference. Port of +/// com.google.copybara.approval.ChangeWithApprovals. +/// TODO: Rename this to Statement +/// (https://github.com/in-toto/attestation/tree/v0.1.0/spec#predicate). +/// +public sealed class ChangeWithApprovals +{ + private readonly Change _change; + private readonly ImmutableArray _predicates; + + public ChangeWithApprovals(Change change) + : this(change, ImmutableArray.Empty) + { + } + + public ChangeWithApprovals( + Change change, ImmutableArray predicates) + { + _change = Preconditions.CheckNotNull(change); + _predicates = predicates; + } + + public Change GetChange() => _change; + + public IReadOnlyList GetPredicates() => _predicates; + + public ChangeWithApprovals AddApprovals(IEnumerable approvals) => + new(_change, _predicates.AddRange(approvals)); + + public override string ToString() => + $"ChangeWithApprovals{{change={_change}, predicates=[{string.Join(", ", _predicates)}]}}"; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is not ChangeWithApprovals that) + { + return false; + } + return Equals(_change, that._change) && _predicates.SequenceEqual(that._predicates); + } + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(_change); + foreach (var p in _predicates) + { + hash.Add(p); + } + return hash.ToHashCode(); + } +} diff --git a/src/Copybara.Core/Approval/IApprovalsProvider.cs b/src/Copybara.Core/Approval/IApprovalsProvider.cs new file mode 100644 index 000000000..1a25e5c10 --- /dev/null +++ b/src/Copybara.Core/Approval/IApprovalsProvider.cs @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using ConsoleT = Copybara.Util.Console.Console; + +namespace Copybara.Approval; + +/// +/// An approvals validator that is provided by the origin. Port of +/// com.google.copybara.approval.ApprovalsProvider. +/// +public interface IApprovalsProvider +{ + /// + /// Given a list of changes, return a list of changes that have approvals. + /// + /// changes to be verified with the existing approvals. + /// + /// describes how to find label inputs in case the labels can't be found among + /// . + /// + /// console, in case some message need to be printed. + /// + /// if access to the origin system fails because of being unavailable, server error, etc. + /// + /// + /// if failure is attributable to the user setup (e.g. permission errors, etc.). + /// + ApprovalsResult ComputeApprovals( + ImmutableArray changes, + Func>? labelFinder, + ConsoleT console); +} + +/// +/// An object containing the approvals found for a set of changes. +/// +/// The purpose of this class is to make it easier to migrate to attestations in the future. For +/// example, storing general information about the source. +/// +public sealed class ApprovalsResult +{ + private readonly ImmutableArray _changes; + + public ApprovalsResult(ImmutableArray changes) + { + _changes = changes; + } + + /// + /// List of changes with their corresponding approvals. Must be the complete list of changes, with + /// or without any approval. + /// + public IReadOnlyList GetChanges() => _changes; +} diff --git a/src/Copybara.Core/Approval/NoneApprovedProvider.cs b/src/Copybara.Core/Approval/NoneApprovedProvider.cs new file mode 100644 index 000000000..0bc1f8ab0 --- /dev/null +++ b/src/Copybara.Core/Approval/NoneApprovedProvider.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using ConsoleT = Copybara.Util.Console.Console; + +namespace Copybara.Approval; + +/// +/// An approval provider that returns all the changes as not approved. Port of +/// com.google.copybara.approval.NoneApprovedProvider. +/// +public sealed class NoneApprovedProvider : IApprovalsProvider +{ + public ApprovalsResult ComputeApprovals( + ImmutableArray changes, + Func>? labelFinder, + ConsoleT console) => + new(changes); +} diff --git a/src/Copybara.Core/Approval/StatementPredicate.cs b/src/Copybara.Core/Approval/StatementPredicate.cs new file mode 100644 index 000000000..adb11435f --- /dev/null +++ b/src/Copybara.Core/Approval/StatementPredicate.cs @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; + +namespace Copybara.Approval; + +/// +/// A predicate represents a statement over a change. This is an approximation to +/// predicates. +/// Port of com.google.copybara.approval.StatementPredicate. +/// +public class StatementPredicate +{ + private readonly string _type; + private readonly string _description; + private readonly string _url; + + public StatementPredicate(string type, string description, string url) + { + _type = type; + _description = description; + _url = url; + } + + /// + /// Utility method that filters out elements of that are an instance of + /// . + /// + public static ImmutableArray FilterByClass( + IEnumerable list) + where T : StatementPredicate => + list.OfType().ToImmutableArray(); + + /// Predicate type: Approval, ownership, etc. + public string Type() => _type; + + /// Text representation of the predicate for human consumption. + public string Description() => _description; + + /// + /// Returns where the predicate happened (E.g. an approval in GitHub would have + /// https://github.com/example/project/pull/123 as the url). + /// + public string Url() => _url; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is not StatementPredicate that) + { + return false; + } + return string.Equals(_type, that._type) + && string.Equals(_description, that._description) + && string.Equals(_url, that._url); + } + + public override int GetHashCode() => HashCode.Combine(_type, _description, _url); + + public sealed override string ToString() => ToStringDescription(); + + protected virtual string ToStringDescription() => + $"StatementPredicate{{type={_type}, description={_description}, url={_url}}}"; +} diff --git a/src/Copybara.Core/Approval/UserPredicate.cs b/src/Copybara.Core/Approval/UserPredicate.cs new file mode 100644 index 000000000..a4a776aaf --- /dev/null +++ b/src/Copybara.Core/Approval/UserPredicate.cs @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Approval; + +/// +/// Defines a predicate over a user action. Port of +/// com.google.copybara.approval.UserPredicate. +/// +public class UserPredicate : StatementPredicate +{ + private readonly string _username; + private readonly UserPredicateType _type; + + public UserPredicate( + string username, UserPredicateType userType, string originUrl, string description) + : base(userType.ToString(), description, originUrl) + { + _username = username; + _type = userType; + } + + /// + /// String representing the username for the user predicate (e.g. the username of the approver, + /// owners, etc.). + /// + public string Username() => _username; + + public UserPredicateType UserType() => _type; + + /// Type of user predicate. + public enum UserPredicateType + { + /// username is the owner of the change. + OWNER, + + /// + /// username has approved the change. Called LGTM for historical reasons (used internally). + /// + LGTM, + } + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is not UserPredicate that) + { + return false; + } + if (!base.Equals(o)) + { + return false; + } + return string.Equals(_username, that._username) && _type == that._type; + } + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), _username, _type); + + protected override string ToStringDescription() => + base.ToStringDescription() + $" username={_username}, type={_type}"; +} diff --git a/src/Copybara.Core/AutoPatchfileConfiguration.cs b/src/Copybara.Core/AutoPatchfileConfiguration.cs new file mode 100644 index 000000000..ee0846d19 --- /dev/null +++ b/src/Copybara.Core/AutoPatchfileConfiguration.cs @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara; + +/// Parameters for customizing auto patch file generation. +[StarlarkBuiltin( + "core.autopatch_config", + Doc = "The configuration that describes automatic patch file generation")] +public sealed class AutoPatchfileConfiguration : IStarlarkValue +{ + private readonly string? _header; + private readonly string _suffix; + private readonly string _directoryPrefix; + private readonly string? _directory; + private readonly bool _stripFilenames; + private readonly bool _stripLineNumbers; + private readonly Glob _glob; + + private AutoPatchfileConfiguration( + string? header, + string suffix, + string directoryPrefix, + string? directory, + bool stripFilenames, + bool stripLineNumbers, + Glob glob) + { + _header = header; + _suffix = suffix; + _directoryPrefix = directoryPrefix; + _directory = directory; + _stripFilenames = stripFilenames; + _stripLineNumbers = stripLineNumbers; + _glob = glob; + } + + public static AutoPatchfileConfiguration Create( + string? header, + string suffix, + string directoryPrefix, + string? directory, + bool stripFilenames, + bool stripLineNumbers, + Glob glob) => + new(header, suffix, directoryPrefix, directory, stripFilenames, stripLineNumbers, glob); + + public string? Header() => _header; + + public string Suffix() => _suffix; + + public string DirectoryPrefix() => _directoryPrefix; + + public string? Directory() => _directory; + + public bool StripFilenames() => _stripFilenames; + + public bool StripLineNumbers() => _stripLineNumbers; + + public Glob GlobValue() => _glob; +} diff --git a/src/Copybara.Core/BaselinesWithoutLabelVisitor.cs b/src/Copybara.Core/BaselinesWithoutLabelVisitor.cs new file mode 100644 index 000000000..f7a093000 --- /dev/null +++ b/src/Copybara.Core/BaselinesWithoutLabelVisitor.cs @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Revision; +using Copybara.Util; + +namespace Copybara; + +/// A visitor that finds all the parents that match the origin glob. +public class BaselinesWithoutLabelVisitor : IChangesVisitor +{ + private readonly List _result = new(); + private readonly int _limit; + private readonly Glob _originFiles; + private bool _skipFirst; + private readonly IRevision? _toSkip; + + public BaselinesWithoutLabelVisitor(Glob originFiles, int limit, IRevision? toSkip, bool skipFirst) + { + _originFiles = Preconditions.CheckNotNull(originFiles); + Preconditions.CheckArgument(limit > 0); + _limit = limit; + _toSkip = toSkip; + _skipFirst = skipFirst; + } + + public IReadOnlyList GetResult() => _result.ToImmutableArray(); + + public VisitResult Visit(Change change) + { + if (_skipFirst || (_toSkip != null && change.GetRevision().Equals(_toSkip))) + { + _skipFirst = false; + return VisitResult.Continue; + } + var files = change.GetChangeFiles(); + if (Glob.AffectsRoots(_originFiles.Roots(), files)) + { + _result.Add((T)(object)change.GetRevision()); + return _result.Count < _limit ? VisitResult.Continue : VisitResult.Terminate; + } + // This change only contains files that are not exported + return VisitResult.Continue; + } +} diff --git a/src/Copybara.Core/ChangeMessage.cs b/src/Copybara.Core/ChangeMessage.cs new file mode 100644 index 000000000..39d7c238b --- /dev/null +++ b/src/Copybara.Core/ChangeMessage.cs @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2016 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using Copybara.Common; +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara; + +/// +/// An object that represents a well formed message: No superfluous new lines, a group of labels, +/// etc. +/// +/// This class is immutable. +/// +[StarlarkBuiltin( + "ChangeMessage", + Doc = "Represents a well formed parsed change message with its associated labels.")] +public sealed class ChangeMessage : IStarlarkValue +{ + private const string DoubleNewline = "\n\n"; + private const string DashDashSeparator = "\n--\n"; + + private readonly string _text; + private readonly string _groupSeparator; + private readonly ImmutableArray _labels; + + private ChangeMessage(string text, string groupSeparator, IReadOnlyList labels) + { + _text = text.Trim('\n'); + _groupSeparator = Preconditions.CheckNotNull(groupSeparator); + _labels = Preconditions.CheckNotNull(labels).ToImmutableArray(); + } + + /// + /// Create a new message object looking for labels in just the last paragraph. + /// + /// Use this for Copybara well-formed messages. + /// + public static ChangeMessage ParseMessage(string message) + { + string trimMsg = message.Trim('\n'); + int doubleNewLine = trimMsg.LastIndexOf(DoubleNewline, StringComparison.Ordinal); + int dashDash = trimMsg.LastIndexOf(DashDashSeparator, StringComparison.Ordinal); + if (doubleNewLine == -1 && dashDash == -1) + { + // Empty message like "\n\nfoo: bar" or "\n\nfoo bar baz" + if (message.StartsWith(DoubleNewline, StringComparison.Ordinal)) + { + return new ChangeMessage("", DoubleNewline, LinesAsLabels(trimMsg)); + } + return new ChangeMessage(trimMsg, DoubleNewline, new List()); + } + else if (doubleNewLine > dashDash) + { + return new ChangeMessage( + trimMsg.Substring(0, doubleNewLine), + DoubleNewline, + LinesAsLabels(trimMsg.Substring(doubleNewLine + 2))); + } + else + { + return new ChangeMessage( + trimMsg.Substring(0, dashDash), + DashDashSeparator, + LinesAsLabels(trimMsg.Substring(dashDash + 4))); + } + } + + /// + /// Create a new message object treating all the lines as possible labels instead of looking just + /// in the last paragraph for labels. + /// + public static ChangeMessage ParseAllAsLabels(string message) + { + Preconditions.CheckNotNull(message); + return new ChangeMessage("", DoubleNewline, LinesAsLabels(message)); + } + + private static List LinesAsLabels(string message) + { + Preconditions.CheckNotNull(message); + return message.TrimEnd('\n').Split('\n').Select(line => new LabelFinder(line)).ToList(); + } + + [StarlarkMethod("first_line", Doc = "First line of this message", StructField = true)] + public string FirstLine() + { + int idx = _text.IndexOf('\n'); + return idx == -1 ? _text : _text.Substring(0, idx); + } + + [StarlarkMethod( + "text", + Doc = "The text description this message, not including the labels.", + StructField = true)] + public string GetText() => _text; + + public IReadOnlyList GetLabels() => _labels; + + /// + /// Returns all the labels in the message. If a label appears multiple times, it respects the + /// order of appearance. + /// + public ImmutableListMultimap LabelsAsMultimap() + { + var result = ImmutableListMultimap.CreateBuilder(); + foreach (var label in _labels) + { + if (label.IsLabel()) + { + result.Put(label.GetName(), label.GetValue()); + } + } + return result.Build(); + } + + [StarlarkMethod( + "label_values", + Doc = "Returns a list of values associated with the label name.")] + public IReadOnlyList GetLabelValues( + [Param(Name = "label_name", Named = true, Doc = "The label name.")] string labelName) + { + var localLabels = LabelsAsMultimap(); + if (localLabels.ContainsKey(labelName)) + { + return localLabels.Get(labelName); + } + return ImmutableArray.Empty; + } + + public ChangeMessage WithLabel(string name, string separator, string value) + { + var newLabels = new List(_labels); + // Add an additional line if none of the previous elements are labels + if (newLabels.Count != 0 && !newLabels.Any(l => l.IsLabel())) + { + newLabels.Add(new LabelFinder("")); + } + newLabels.Add(new LabelFinder( + ValidateLabelName(name) + Preconditions.CheckNotNull(separator) + + Preconditions.CheckNotNull(value))); + return new ChangeMessage(_text, _groupSeparator, newLabels); + } + + public ChangeMessage WithReplacedLabel(string labelName, string separator, string value) + { + ValidateLabelName(labelName); + var newLabels = _labels + .Select(label => label.IsLabel(labelName) + ? new LabelFinder(labelName + separator + value) + : label) + .ToList(); + return new ChangeMessage(_text, _groupSeparator, newLabels); + } + + public ChangeMessage WithNewOrReplacedLabel(string labelName, string separator, string value) + { + ValidateLabelName(labelName); + var newLabels = new List(); + bool wasReplaced = false; + + foreach (var originalLabel in _labels) + { + if (originalLabel.IsLabel(labelName)) + { + newLabels.Add(new LabelFinder(labelName + separator + value)); + wasReplaced = true; + } + else + { + newLabels.Add(originalLabel); + } + } + + var newChangeMessage = new ChangeMessage(_text, _groupSeparator, newLabels); + if (!wasReplaced) + { + return newChangeMessage.WithLabel(labelName, separator, value); + } + return newChangeMessage; + } + + /// Filters out all labels that do not match . + public ChangeMessage WithLabelsFilteredBy(Func predicate) + { + var filteredLabels = _labels.Where(predicate).ToList(); + return new ChangeMessage(_text, _groupSeparator, filteredLabels); + } + + /// Remove a label by name if it exists. + public ChangeMessage WithRemovedLabelByName(string name) + { + ValidateLabelName(name); + var filteredLabels = _labels.Where(label => !label.IsLabel(name)).ToList(); + return new ChangeMessage(_text, _groupSeparator, filteredLabels); + } + + /// Remove a label by name and value if it exists. + public ChangeMessage WithRemovedLabelByNameAndValue(string name, string value) + { + ValidateLabelName(name); + var filteredLabels = _labels + .Where(label => !label.IsLabel(name) || !label.GetValue().Equals(value)) + .ToList(); + return new ChangeMessage(_text, _groupSeparator, filteredLabels); + } + + private static string ValidateLabelName(string label) + { + ValidationException.CheckCondition( + LabelFinder.VALID_LABEL.IsMatch(label), "Label '{0}' is not a valid label", label); + return label; + } + + /// Set the text part of the message, leaving the labels untouched. + public ChangeMessage WithText(string text) => + new(text.Trim('\n'), _groupSeparator, _labels); + + public override string ToString() + { + var sb = new StringBuilder(); + + if (_text.Length != 0) + { + sb.Append(_text).Append(_labels.Length == 0 ? "\n" : _groupSeparator); + } + foreach (var label in _labels) + { + sb.Append(label.GetLine()).Append('\n'); + } + // Let's normalize in case parseAllAsLabels was used and all the labels were removed. + return sb.ToString().Trim('\n') + '\n'; + } +} diff --git a/src/Copybara.Core/ChangeVisitable.cs b/src/Copybara.Core/ChangeVisitable.cs new file mode 100644 index 000000000..0a9f61240 --- /dev/null +++ b/src/Copybara.Core/ChangeVisitable.cs @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Revision; + +namespace Copybara; + +/// The result type for the function passed to visitChanges. +public enum VisitResult +{ + /// + /// Continue. If more changes are available for visiting, the origin will call again the function + /// with the next changes. + /// + Continue, + + /// + /// Stop. Origin will not pass more changes to the visitor function. Usually used because the + /// function found what it was looking for (for example a commit with a label). + /// + Terminate, +} + +/// +/// A visitor of changes. An implementation of this interface is provided to visitChanges +/// methods to visit changes in Origin or Destination history. +/// +public interface IChangesVisitor +{ + /// + /// Invoked for each change found. The implementation can choose to cancel the visitation by + /// returning . + /// + VisitResult Visit(Change input); +} + +/// A visitor of changes that only receives changes that match any of the passed labels. +public interface IChangesLabelVisitor +{ + /// + /// Invoked for each change found that matches the labels. + /// + /// Note that the can be disjoint with the labels in + /// , since labels might be stored with a different string format. + /// + VisitResult Visit(Change input, IReadOnlyDictionary matchedLabels); +} + +/// +/// An interface stating that the implementing class accepts child visitors to explore repository +/// state beyond the changes being migrated. +/// +/// the revision type. +public interface IChangeVisitable + where R : class, IRevision +{ + /// + /// Visit the parents of the revision and call the visitor for each + /// change. The visitor can stop the stream of changes at any moment by returning + /// . + /// + /// It is up to the Origin how and what changes it provides to the function. + /// + void VisitChanges(R? start, IChangesVisitor visitor); + + /// Visit only changes that contain any of the labels in . + void VisitChangesWithAnyLabel( + R? start, IReadOnlyCollection labels, IChangesLabelVisitor visitor) + { + VisitChanges(start, new AnyLabelAdapter(labels, visitor)); + } + + private sealed class AnyLabelAdapter : IChangesVisitor + { + private readonly IReadOnlyCollection _labels; + private readonly IChangesLabelVisitor _visitor; + + public AnyLabelAdapter(IReadOnlyCollection labels, IChangesLabelVisitor visitor) + { + _labels = labels; + _visitor = visitor; + } + + public VisitResult Visit(Change input) + { + // We could return all the label values, but this is really only used for RevId-like ones + // and last is good enough for now. + var labels = input.GetLabels(); + var copy = ImmutableDictionary.CreateBuilder(); + foreach (var key in labels.Keys) + { + if (_labels.Contains(key)) + { + var values = labels.Get(key); + copy[key] = values[values.Length - 1]; + } + } + if (copy.Count == 0) + { + return VisitResult.Continue; + } + return _visitor.Visit(input, copy.ToImmutable()); + } + } +} diff --git a/src/Copybara.Core/CheckoutFileSystem.cs b/src/Copybara.Core/CheckoutFileSystem.cs new file mode 100644 index 000000000..da87e25c2 --- /dev/null +++ b/src/Copybara.Core/CheckoutFileSystem.cs @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using Copybara.Common; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara; + +/// Common Starlark methods that allow users to manipulate paths of the workdir/checkoutPath. +public class CheckoutFileSystem : IStarlarkValue +{ + private readonly string _checkoutDir; + + public CheckoutFileSystem(string checkoutDir) + { + _checkoutDir = Preconditions.CheckNotNull(checkoutDir); + } + + [StarlarkMethod("new_path", Doc = "Create a new path")] + public CheckoutPath NewPath( + [Param( + Name = "path", + Doc = "The string representing the path, relative to the checkout root directory")] + string path) => + CheckoutPath.CreateWithCheckoutDir(path, _checkoutDir); + + [StarlarkMethod("create_symlink", Doc = "Create a symlink")] + public void CreateSymlink( + [Param(Name = "link", Doc = "The link path")] CheckoutPath link, + [Param(Name = "target", Doc = "The target path")] CheckoutPath target) + { + try + { + string linkFullPath = AsCheckoutPath(link); + // Verify target is inside checkout dir + _ = AsCheckoutPath(target); + + if (System.IO.File.Exists(linkFullPath) || System.IO.Directory.Exists(linkFullPath)) + { + string kind = System.IO.Directory.Exists(linkFullPath) + ? " and is a directory" + : (System.IO.File.GetAttributes(linkFullPath) & System.IO.FileAttributes.ReparsePoint) != 0 + ? " and is a symlink" + : System.IO.File.Exists(linkFullPath) + ? " and is a regular file" + : " and we don't know what kind of file is"; + throw StarlarkRt.Errorf("'{0}' already exist{1}", link.GetPath(), kind); + } + + string? linkParent = PathOps.GetParent(link.GetPath()); + string relativized = linkParent == null + ? target.GetPath() + : PathOps.Relativize(linkParent, target.GetPath()); + string? fullParent = PathOps.GetParent(linkFullPath); + if (fullParent != null) + { + System.IO.Directory.CreateDirectory(fullParent); + } + + System.IO.File.CreateSymbolicLink(linkFullPath, relativized); + } + catch (System.IO.IOException e) + { + throw StarlarkRt.Errorf("Cannot create symlink: {0}", e.Message); + } + } + + [StarlarkMethod("write_path", Doc = "Write an arbitrary string to a path (UTF-8 will be used)")] + public void WritePath( + [Param(Name = "path", Doc = "The Path to write to")] CheckoutPath path, + [Param(Name = "content", Doc = "The content of the file")] string content) + { + string fullPath = AsCheckoutPath(path); + string? parent = PathOps.GetParent(fullPath); + if (parent != null) + { + System.IO.Directory.CreateDirectory(parent); + } + System.IO.File.WriteAllBytes(fullPath, Encoding.UTF8.GetBytes(content)); + } + + [StarlarkMethod("read_path", Doc = "Read the content of path as UTF-8")] + public string ReadPath( + [Param(Name = "path", Doc = "The Path to read from")] CheckoutPath path) => + Encoding.UTF8.GetString(System.IO.File.ReadAllBytes(AsCheckoutPath(path))); + + [StarlarkMethod("set_executable", Doc = "Set the executable permission of a file")] + public void SetExecutable( + [Param(Name = "path", Doc = "The Path to set the executable permission of")] CheckoutPath path, + [Param(Name = "value", Doc = "Whether or not the file should be executable")] bool value) + { + string full = AsCheckoutPath(path); + if (!OperatingSystem.IsWindows()) + { + var mode = System.IO.File.GetUnixFileMode(full); + var exec = UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute; + mode = value ? mode | exec : mode & ~exec; + System.IO.File.SetUnixFileMode(full, mode); + } + } + + /// + /// The path containing the repository state to transform. Transformation should be done in-place. + /// + public string GetCheckoutDir() => _checkoutDir; + + [StarlarkMethod("list", Doc = "List files in the checkout/work directory that matches a glob")] + public StarlarkList List( + [Param(Name = "paths", Doc = "A glob representing the paths to list")] Glob glob) + { + var pathMatcher = glob.RelativeTo(_checkoutDir); + var result = new List(); + foreach (var full in System.IO.Directory.EnumerateFiles( + _checkoutDir, "*", System.IO.SearchOption.AllDirectories)) + { + string normalized = full.Replace('\\', '/'); + if (pathMatcher.Matches(normalized)) + { + result.Add(new CheckoutPath(PathOps.Relativize(_checkoutDir, normalized), _checkoutDir)); + } + } + return StarlarkList.ImmutableCopyOf(result.Cast()); + } + + private string AsCheckoutPath(CheckoutPath path) + { + string resolved = PathOps.Resolve(_checkoutDir, path.GetPath()); + string normalized = PathOps.Normalize(resolved); + if (!PathOps.StartsWith(normalized, _checkoutDir)) + { + throw StarlarkRt.Errorf( + "{0} is not inside the checkout directory or links to a file outside the path." + + " Normalized path was {1}, checkout dir was {2}", + path, normalized, _checkoutDir); + } + return normalized; + } +} diff --git a/src/Copybara.Core/CheckoutPath.cs b/src/Copybara.Core/CheckoutPath.cs new file mode 100644 index 000000000..df1ba385e --- /dev/null +++ b/src/Copybara.Core/CheckoutPath.cs @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara; + +/// +/// Represents a file that is exposed to Skylark. +/// +/// Files are always relative to the checkout dir and normalized. Paths are represented as +/// forward-slash-separated strings, mirroring java.nio.file.Path semantics. +/// +[StarlarkBuiltin("Path", Doc = "Represents a path in the checkout directory")] +public class CheckoutPath : IComparable, IStarlarkPrintableValue +{ + private readonly string _path; + private readonly string _checkoutDir; + + internal CheckoutPath(string path, string checkoutDir) + { + _path = Preconditions.CheckNotNull(path); + _checkoutDir = Preconditions.CheckNotNull(checkoutDir); + } + + private CheckoutPath Create(string path) => CreateWithCheckoutDir(path, _checkoutDir); + + public static CheckoutPath CreateWithCheckoutDir(string relative, string checkoutDir) + { + if (PathOps.IsAbsolute(relative)) + { + throw StarlarkRt.Errorf("Absolute paths are not allowed: {0}", relative); + } + string targetPath = PathOps.Normalize(PathOps.Resolve(checkoutDir, relative)); + if (!PathOps.StartsWith(targetPath, checkoutDir)) + { + throw StarlarkRt.Errorf("Escaping the checkout dir is not allowed: {0}", relative); + } + + return new CheckoutPath(PathOps.Normalize(relative), checkoutDir); + } + + [StarlarkMethod( + "path", + Doc = "Full path relative to the checkout directory", + StructField = true)] + public string PathAsString() => _path; + + /// + /// The full path pointing to the real location of the checkout file. Use only for internal + /// implementations and do not make this available to Starlark. + /// + public string FullPath() => PathOps.Resolve(_checkoutDir, _path); + + [StarlarkMethod( + "name", + Doc = "Filename of the path. For foo/bar/baz.txt it would be baz.txt", + StructField = true)] + public string Name() => PathOps.GetFileName(_path); + + [StarlarkMethod("parent", Doc = "Get the parent path", StructField = true)] + public object Parent() + { + string? parent = PathOps.GetParent(_path); + if (parent == null) + { + // nio equivalent of new_path("foo").parent returns null, but we want to be able to do + // foo.parent.resolve("bar"). + return _path.Length == 0 ? StarlarkRt.None : Create(""); + } + return Create(parent); + } + + [StarlarkMethod( + "relativize", + Doc = + "Constructs a relative path between this path and a given path. For example:
" + + " path('a/b').relativize('a/b/c/d')
" + + "returns 'c/d'")] + public CheckoutPath Relativize( + [Param(Name = "other", Doc = "The path to relativize against this path")] CheckoutPath other) => + Create(PathOps.Relativize(_path, other._path)); + + [StarlarkMethod( + "resolve", + Doc = "Resolve the given path against this path.")] + public CheckoutPath Resolve( + [Param( + Name = "child", + AllowedTypes = new[] { typeof(string), typeof(CheckoutPath) }, + Doc = "Resolve the given path against this path. The parameter" + + " can be a string or a Path.")] + object child) + { + if (child is string s) + { + return Create(PathOps.Resolve(_path, s)); + } + if (child is CheckoutPath cp) + { + return Create(PathOps.Resolve(_path, cp._path)); + } + throw StarlarkRt.Errorf("Cannot resolve children for type {0}: {1}", child.GetType().Name, child); + } + + [StarlarkMethod( + "resolve_sibling", + Doc = "Resolve the given path against this path.")] + public CheckoutPath ResolveSibling( + [Param( + Name = "other", + AllowedTypes = new[] { typeof(string), typeof(CheckoutPath) }, + Doc = "Resolve the given path against this path. The parameter can be a string or a Path.")] + object other) + { + if (other is string s) + { + return Create(PathOps.ResolveSibling(_path, s)); + } + if (other is CheckoutPath cp) + { + return Create(PathOps.ResolveSibling(_path, cp._path)); + } + throw StarlarkRt.Errorf("Cannot resolve sibling for type {0}: {1}", other.GetType().Name, other); + } + + [StarlarkMethod("attr", Doc = "Get the file attributes, for example size.", StructField = true)] + public CheckoutPathAttributes Attr() + { + try + { + string full = PathOps.Resolve(_checkoutDir, _path); + bool isSymlink = + (System.IO.File.GetAttributes(full) & System.IO.FileAttributes.ReparsePoint) != 0; + long size = isSymlink ? 0 : new System.IO.FileInfo(full).Length; + return new CheckoutPathAttributes(_path, size, isSymlink); + } + catch (System.IO.IOException e) + { + throw StarlarkRt.Errorf("Error getting attributes for {0}:{1}", _path, e); + } + } + + [StarlarkMethod("read_symlink", Doc = "Read the symlink")] + public CheckoutPath ReadSymbolicLink() + { + try + { + string symlinkPath = PathOps.Resolve(_checkoutDir, _path); + var info = new System.IO.FileInfo(symlinkPath); + if ((System.IO.File.GetAttributes(symlinkPath) & System.IO.FileAttributes.ReparsePoint) == 0) + { + throw StarlarkRt.Errorf("{0} is not a symlink", _path); + } + + var resolvedSymlink = + FileUtil.ResolveSymlink(Glob.AllFiles.RelativeTo(_checkoutDir), symlinkPath); + if (resolvedSymlink.TargetLocationValue != FileUtil.ResolvedSymlink.TargetLocation.Inside) + { + throw StarlarkRt.Errorf( + "Symlink {0} does not point to a file inside the checkout dir: {1}", + symlinkPath, resolvedSymlink.RegularFile); + } + + return Create(PathOps.Relativize(_checkoutDir, resolvedSymlink.RegularFile)); + } + catch (System.IO.IOException e) + { + throw StarlarkRt.Errorf("Cannot resolve symlink {0}: {1}", _path, e); + } + } + + [StarlarkMethod("remove", Doc = "Delete self")] + public void Remove() + { + try + { + System.IO.File.Delete(PathOps.Resolve(_checkoutDir, _path)); + } + catch (System.IO.FileNotFoundException e) + { + throw StarlarkRt.Errorf("Could not find file {0}, received error {1}", _path, e.ToString()); + } + catch (System.IO.IOException e) + { + throw new ValidationException("Could not delete file for unknown reason", e); + } + } + + [StarlarkMethod( + "rmdir", + Doc = + "Delete all files in a directory. If recursive is true, delete descendants of all files" + + " in directory")] + public void RmDir( + [Param( + Name = "recursive", + Named = true, + Doc = "When true, delete descendants of self and of siblings", + DefaultValue = "False")] + bool recursive) + { + try + { + string full = PathOps.Resolve(_checkoutDir, _path); + if (!System.IO.Directory.Exists(full) && !System.IO.File.Exists(full)) + { + return; + } + if (recursive) + { + FileUtil.DeleteRecursively(full); + } + else + { + System.IO.Directory.Delete(full); + } + } + catch (System.IO.IOException e) + { + throw new ValidationException("Could not delete file for unknown reason", e); + } + } + + [StarlarkMethod("exists", Doc = "Check whether a file, directory or symlink exists at this path")] + public bool FileExists() + { + string full = PathOps.Resolve(_checkoutDir, _path); + return System.IO.File.Exists(full) || System.IO.Directory.Exists(full); + } + + public string GetPath() => _path; + + public string GetCheckoutDir() => _checkoutDir; + + public override string ToString() => _path; + + public int CompareTo(CheckoutPath? o) => + string.CompareOrdinal(_path, o?._path); + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(_path); + + public override int GetHashCode() => _path.GetHashCode(); + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is not CheckoutPath other) + { + return false; + } + return string.Equals(_path, other._path); + } +} diff --git a/src/Copybara.Core/CheckoutPathAttributes.cs b/src/Copybara.Core/CheckoutPathAttributes.cs new file mode 100644 index 000000000..3423648f0 --- /dev/null +++ b/src/Copybara.Core/CheckoutPathAttributes.cs @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara; + +/// Represents file attributes exposed to Skylark. +[StarlarkBuiltin("PathAttributes", Doc = "Represents a path attributes like size.")] +public class CheckoutPathAttributes : IStarlarkValue +{ + private readonly string _path; + private readonly long _size; + private readonly bool _isSymlink; + + internal CheckoutPathAttributes(string path, long size, bool isSymlink) + { + _path = Preconditions.CheckNotNull(path); + _size = size; + _isSymlink = isSymlink; + } + + [StarlarkMethod( + "size", + Doc = "The size of the file. Throws an error if file size > 2GB.", + StructField = true)] + public int Size() + { + if (_size is > int.MaxValue or < int.MinValue) + { + throw StarlarkRt.Errorf( + "File {0} is too big to compute the size: {1} bytes", _path, _size); + } + return (int)_size; + } + + [StarlarkMethod("symlink", Doc = "Returns true if it is a symlink", StructField = true)] + public bool IsSymlink() => _isSymlink; +} diff --git a/src/Copybara.Core/Config/CapturingConfigFile.cs b/src/Copybara.Core/Config/CapturingConfigFile.cs new file mode 100644 index 000000000..e36040d22 --- /dev/null +++ b/src/Copybara.Core/Config/CapturingConfigFile.cs @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; + +namespace Copybara.Config; + +/// +/// A config file that records the children created from it. Useful for collecting dependencies in +/// dry runs. +/// +internal sealed class CapturingConfigFile : ConfigFile +{ + private readonly LinkedHashSet _children = new(); + private readonly ConfigFile _wrapped; + + internal CapturingConfigFile(ConfigFile config) => _wrapped = Preconditions.CheckNotNull(config); + + public ConfigFile Resolve(string path) + { + var resolved = new CapturingConfigFile(_wrapped.Resolve(path)); + _children.Add(resolved); + return resolved; + } + + public ImmutableDictionary ResolveAll(IReadOnlySet paths) + { + var result = ImmutableDictionary.CreateBuilder(); + foreach (var e in _wrapped.ResolveAll(paths)) + { + var capturingConfigFile = new CapturingConfigFile(e.Value); + result[e.Key] = capturingConfigFile; + _children.Add(capturingConfigFile); + } + return result.ToImmutable(); + } + + public string Path() => _wrapped.Path(); + + public byte[] ReadContentBytes() => _wrapped.ReadContentBytes(); + + public string GetIdentifier() => _wrapped.GetIdentifier(); + + /// + /// Retrieve collected dependencies. + /// + /// A map from path to the wrapped ConfigFile for each ConfigFile created by this or one + /// of its descendants. Includes this. + internal ImmutableDictionary GetAllLoadedFiles() + { + var map = new Dictionary(); + GetAllLoadedFiles(map); + return map.ToImmutableDictionary(); + } + + private void GetAllLoadedFiles(Dictionary map) + { + map[Path()] = _wrapped; + foreach (var child in _children) + { + child.GetAllLoadedFiles(map); + } + } + + public override bool Equals(object? otherObject) => + otherObject is CapturingConfigFile other + && other._wrapped.Equals(_wrapped) + && _children.SetEquals(other._children); + + public override int GetHashCode() => Path().GetHashCode(); + + public override string ToString() => + $"CapturingConfigFile{{children={_children.Count}, wrapped={_wrapped}}}"; + + /// An insertion-ordered set, mirroring Java's LinkedHashSet semantics. + private sealed class LinkedHashSet : IEnumerable + where T : notnull + { + private readonly Dictionary _index = new(); + private readonly List _items = new(); + + public void Add(T item) + { + if (_index.TryAdd(item, _items.Count)) + { + _items.Add(item); + } + } + + public int Count => _items.Count; + + public bool SetEquals(LinkedHashSet other) => + _index.Count == other._index.Count && _index.Keys.All(other._index.ContainsKey); + + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => + GetEnumerator(); + } +} diff --git a/src/Copybara.Core/Config/Config.cs b/src/Copybara.Core/Config/Config.cs new file mode 100644 index 000000000..0626bd90c --- /dev/null +++ b/src/Copybara.Core/Config/Config.cs @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Exceptions; + +namespace Copybara.Config; + +/// +/// Configuration for a Copybara project. +/// +/// Objects of this class represent a parsed Copybara configuration. +/// +public sealed class Config +{ + private readonly ImmutableDictionary _migrations; + private readonly string _location; + private readonly ImmutableDictionary _globals; + + public Config( + IReadOnlyDictionary migrations, + string location, + IReadOnlyDictionary globals) + { + _migrations = migrations.ToImmutableDictionary(); + _location = Preconditions.CheckNotNull(location); + _globals = globals.ToImmutableDictionary(); + } + + /// Returns the named after . + /// if no migration with the given name exists + public IMigration GetMigration(string migrationName) + { + ValidationException.CheckCondition( + _migrations.ContainsKey(migrationName), + "No migration with name '{0}' exists. Valid migrations: {1}", + migrationName, + string.Join(", ", _migrations.Keys)); + return _migrations[migrationName]; + } + + /// + /// Location of the top-level config file. An arbitrary string meant to be used for + /// logging/debugging. It shouldn't be parsed, as the format might change. + /// + public string GetLocation() => _location; + + /// + /// Reads values from the global frame of the skylark environment, i.e. global variables. + /// + public T? GetGlobalEnvironmentVariable(string name) + where T : class => + _globals.TryGetValue(name, out object? value) ? value as T : null; + + /// Returns all the migrations in this configuration. + public ImmutableDictionary GetMigrations() => _migrations; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is not Config config) + { + return false; + } + return DictionaryEquals(_migrations, config._migrations); + } + + public override int GetHashCode() + { + int hash = 17; + foreach (var e in _migrations) + { + hash ^= e.Key.GetHashCode(); + } + return hash; + } + + public override string ToString() => + $"Config{{migrations=[{string.Join(", ", _migrations.Keys)}], location={_location}}}"; + + private static bool DictionaryEquals( + ImmutableDictionary a, ImmutableDictionary b) + { + if (a.Count != b.Count) + { + return false; + } + foreach (var e in a) + { + if (!b.TryGetValue(e.Key, out IMigration? other) || !Equals(e.Value, other)) + { + return false; + } + } + return true; + } +} diff --git a/src/Copybara.Core/Config/ConfigFile.cs b/src/Copybara.Core/Config/ConfigFile.cs new file mode 100644 index 000000000..84622419e --- /dev/null +++ b/src/Copybara.Core/Config/ConfigFile.cs @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using Copybara.Exceptions; +using Copybara.Util; + +namespace Copybara.Config; + +/// +/// An object representing a configuration file and that it can be used to resolve +/// other config files relative to this one. +/// +public interface ConfigFile +{ + /// + /// Check if the path is absolute and validates that the path is normalized. + /// + /// if the path is not normalized + static bool IsAbsolute(string path) + { + bool isAbsolute = path.StartsWith("//", StringComparison.Ordinal); + // Remove '//' for absolute paths + string withoutPrefix = isAbsolute ? path.Substring(2) : path; + try + { + FileUtil.CheckNormalizedRelative(withoutPrefix); + return isAbsolute; + } + catch (ArgumentException e) + { + throw new CannotResolveLabel( + string.Format("Invalid path '{0}': {1}", withoutPrefix, e.Message)); + } + } + + /// + /// Resolve relative to the current config file. + /// + /// if the path cannot be resolved to a content + ConfigFile Resolve(string path); + + /// + /// Resolve a set of configs paths in a batch. This can be used by implementors to check + /// existence or preload the bytes of the content in batch/parallel. + /// + /// a set of paths + /// a map from paths to + /// if any of the paths cannot be resolved + ImmutableDictionary ResolveAll(IReadOnlySet paths) + { + var result = ImmutableDictionary.CreateBuilder(); + foreach (string path in paths) + { + result[path] = Resolve(path); + } + return result.ToImmutable(); + } + + /// Resolved, non-relative name of the config file. + string Path(); + + /// + /// Get the contents of the file. + /// + /// Implementations of this interface should prefer to not eagerly load the content when + /// this method is called in order to allow the callers to check their own cache if they already + /// have . + /// + byte[] ReadContentBytes(); + + /// Utility function to read the content of the config file as String. + string ReadContent() => Encoding.UTF8.GetString(ReadContentBytes()); + + /// + /// Return a string representing a stable identifier that works between different + /// implementations. Note that this is best effort based on several + /// heuristics. + /// + /// If root is not defined or cannot be computed, it will return the absolute path. + /// + /// Users of this method should not try to parse the string, since it is subject to + /// change. + /// + string GetIdentifier(); +} diff --git a/src/Copybara.Core/Config/ConfigValidator.cs b/src/Copybara.Core/Config/ConfigValidator.cs new file mode 100644 index 000000000..6b4c9b1e7 --- /dev/null +++ b/src/Copybara.Core/Config/ConfigValidator.cs @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Config; + +/// +/// Validates Copybara s and returns a . +/// +/// Implementations of this interface should not throw exceptions for validation errors. +/// +public interface ConfigValidator +{ + ValidationResult Validate(Config config, string migrationName) + { + var resultBuilder = new ValidationResult.Builder(); + CheckAtLeastOneMigration(resultBuilder, config); + return resultBuilder.Build(); + } + + void CheckAtLeastOneMigration(ValidationResult.Builder resultBuilder, Config config) + { + if (config.GetMigrations().Count == 0) + { + resultBuilder.Error("At least one migration is required."); + } + } +} diff --git a/src/Copybara.Core/Config/ConfigWithDependencies.cs b/src/Copybara.Core/Config/ConfigWithDependencies.cs new file mode 100644 index 000000000..3c56602ba --- /dev/null +++ b/src/Copybara.Core/Config/ConfigWithDependencies.cs @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; + +namespace Copybara.Config; + +/// +/// A class that contains a loaded config and all the config files that were accessed during the +/// parsing. +/// +/// Upstream this is a nested class SkylarkParser.ConfigWithDependencies; it is promoted +/// to a top-level type in the .NET port. +/// +public sealed class ConfigWithDependencies +{ + private readonly ImmutableDictionary _files; + private readonly Config _config; + + internal ConfigWithDependencies(ImmutableDictionary files, Config config) + { + _config = config; + _files = files; + } + + public Config GetConfig() => _config; + + public ImmutableDictionary GetFiles() => _files; +} diff --git a/src/Copybara.Core/Config/GlobalMigrations.cs b/src/Copybara.Core/Config/GlobalMigrations.cs new file mode 100644 index 000000000..ac0c81261 --- /dev/null +++ b/src/Copybara.Core/Config/GlobalMigrations.cs @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Config; + +/// Global variable that holds the registered migrations in the config files. +[StarlarkBuiltin(GlobalMigrations.GLOBAL_MIGRATIONS, + Doc = "Global variable that holds the registered migrations in the config files", + Documented = false)] +public sealed class GlobalMigrations : IStarlarkValue +{ + private static readonly Regex MigrationNameFormat = + new(@"^[a-zA-Z0-9_\-\./]+$", RegexOptions.Compiled); + + private const string MigrationNamePattern = "[a-zA-Z0-9_\\-\\./]+"; + + internal const string GLOBAL_MIGRATIONS = "global_migrations"; + + private readonly Dictionary _migrations = new(); + + public static GlobalMigrations GetGlobalMigrations(Module module) => + (GlobalMigrations)Preconditions.CheckNotNull(module.GetPredeclared(GLOBAL_MIGRATIONS)); + + public IReadOnlyDictionary GetMigrations() => _migrations; + + /// if a migration with the name already exists or the name is + /// invalid + public void AddMigration(string name, IMigration migration) + { + CheckMigrationName(name); + SkylarkUtil.Check( + _migrations.TryAdd(name, migration), + "A migration with the name '{0}' is already defined", + name); + } + + /// Checks if a migration name conforms to the expected format. + /// Migration name + /// If the name does not conform to the expected format + public static void CheckMigrationName(string name) + { + SkylarkUtil.Check( + MigrationNameFormat.IsMatch(name), + "Migration name '{0}' doesn't conform to expected pattern: {1}", + name, + MigrationNamePattern); + } +} diff --git a/src/Copybara.Core/Config/ILabelsAwareModule.cs b/src/Copybara.Core/Config/ILabelsAwareModule.cs new file mode 100644 index 000000000..fdcf8f831 --- /dev/null +++ b/src/Copybara.Core/Config/ILabelsAwareModule.cs @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Starlark.Eval; + +namespace Copybara.Config; + +/// +/// A StarlarkBuiltin that implements this interface will be given information about the config files +/// and resources loaded by the configuration. +/// +public interface ILabelsAwareModule +{ + /// + /// Called before invoking any methods on a module in order to give the module access to the + /// current config file. This may be called multiple times, in which case only the most recent + /// should be used. + /// + void SetConfigFile(ConfigFile mainConfigFile, ConfigFile currentConfigFile) + { + } + + /// + /// Called before invoking any methods on a module to give the module access to the current + /// workflow name. This may be called multiple times, in which case only the most recent should + /// be used. + /// + void SetWorkflowName(string workflowName) + { + } + + /// + /// A supplier that returns all the files loaded by the configuration loading. The supplier + /// shouldn't be evaluated before loading finishes. + /// + void SetAllConfigResources(Func> configs) + { + } + + /// + /// Set handler for print statements executed by Starlark code run during a migration (for + /// example dynamic transformations, migration hooks or feedback mechanism). + /// + void SetPrintHandler(StarlarkThread.PrintHandler printHandler) + { + } +} diff --git a/src/Copybara.Core/Config/IMigration.cs b/src/Copybara.Core/Config/IMigration.cs new file mode 100644 index 000000000..f1da67e55 --- /dev/null +++ b/src/Copybara.Core/Config/IMigration.cs @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Revision; +using Starlark.Eval; + +namespace Copybara.Config; + +/// +/// A migration is a process that moves files and/or metadata (comments, labels...) at a particular +/// revision from one/many systems to one/many destinations. +/// +/// For helping with the migration a working directory is provided to do any temporary file +/// operations. +/// +public interface IMigration +{ + /// + /// Run a migration for a list of source references. If empty, the default (if any) will be used. + /// + /// Different implementations of Migration might process the list of source references + /// differently (batching them, or running one by one). + /// + /// a working directory for doing file operations if needed. + /// the source references to be migrated. If not present the default + /// (if any) for the migration will be used. + void Run(string workdir, IReadOnlyList sourceRefs); + + Info GetInfo() => Info.Empty; + + /// The migration's name. + string GetName(); + + /// An optional description that users can set to describe what this workflow achieves. + string? GetDescription(); + + /// The migration's mode. + string GetModeString(); + + /// The migration's main config file. + ConfigFile GetMainConfigFile(); + + /// + /// Returns a multimap containing enough data to fingerprint the origin for validation purposes. + /// + Common.ImmutableListMultimap GetOriginDescription(); + + /// + /// Returns a multimap containing enough data to fingerprint the destination for validation + /// purposes. + /// + Common.ImmutableListMultimap GetDestinationDescription(); + + /// Returns a multimap containing enough data to fingerprint credentials used. + IReadOnlyList> GetCredentialDescription(); + + /// Returns the Starlark call stack captured when the migration was defined. + ImmutableArray GetDefinitionStack() => + ImmutableArray.Empty; +} diff --git a/src/Copybara.Core/Config/IOptionsAwareModule.cs b/src/Copybara.Core/Config/IOptionsAwareModule.cs new file mode 100644 index 000000000..23da2c451 --- /dev/null +++ b/src/Copybara.Core/Config/IOptionsAwareModule.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Config; + +/// +/// A StarlarkBuiltin that implements this interface will be initialized with the options. +/// +/// This method will be invoked just after registering the namespace objects in Skylark. +/// +public interface IOptionsAwareModule +{ + /// Set the options for the current Copybara run. + void SetOptions(Options options); +} diff --git a/src/Copybara.Core/Config/MapConfigFile.cs b/src/Copybara.Core/Config/MapConfigFile.cs new file mode 100644 index 000000000..70ef0f7b2 --- /dev/null +++ b/src/Copybara.Core/Config/MapConfigFile.cs @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Exceptions; + +namespace Copybara.Config; + +/// +/// A Config file implementation that uses a map for storing the internal data structure. +/// +/// Assumes all paths to be absolute. +/// +public class MapConfigFile : ConfigFile +{ + private readonly ImmutableDictionary _configFiles; + private readonly string _current; + + public MapConfigFile(ImmutableDictionary configFiles, string current) + { + _configFiles = configFiles; + _current = current; + } + + public ConfigFile Resolve(string path) + { + string resolved = ConfigFile.IsAbsolute(path) + ? ContainsLabel(path.Substring(2)) + : RelativeToCurrentPath(path); + if (!_configFiles.ContainsKey(resolved)) + { + throw new CannotResolveLabel( + string.Format("Cannot resolve '{0}': '{1}' does not exist.", path, resolved)); + } + return new MapConfigFile(_configFiles, resolved); + } + + public string Path() => _current; + + public string GetIdentifier() => Path(); + + public byte[] ReadContentBytes() => _configFiles[_current]; + + public override string ToString() => + $"MapConfigFile{{current={_current}, configFiles=[{string.Join(", ", _configFiles.Keys)}]}}"; + + private string RelativeToCurrentPath(string label) + { + int i = _current.LastIndexOf('/'); + string resolved = i == -1 ? label : _current.Substring(0, i) + "/" + label; + return ContainsLabel(resolved); + } + + private string ContainsLabel(string resolved) + { + if (!_configFiles.ContainsKey(resolved)) + { + throw new CannotResolveLabel( + string.Format("Cannot resolve '{0}': does not exist.", resolved)); + } + return resolved; + } +} diff --git a/src/Copybara.Core/Config/MigrationValidator.cs b/src/Copybara.Core/Config/MigrationValidator.cs new file mode 100644 index 000000000..bd244d012 --- /dev/null +++ b/src/Copybara.Core/Config/MigrationValidator.cs @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Config; + +/// +/// Validates Copybara s and returns a . +/// +/// Implementations of this interface should not throw exceptions for validation errors. +/// +// Reference-forward: upstream dispatches on Workflow, Mirror and ActionMigration. Only +// ActionMigration is ported so far; Workflow/Mirror dispatch is added during final consolidation +// once those types land (Copybara.Workflow, Copybara.Git.Mirror). +public abstract class MigrationValidator +{ + public ValidationResult Validate(IMigration migration, Config config) + { + if (migration is ActionMigration actionMigration) + { + return ValidateActionMigration(migration.GetName(), actionMigration, config); + } + // TODO(consolidation): add Workflow and Mirror dispatch when those types are ported. + throw new InvalidOperationException($"Validation missing for {migration}"); + } + + /// Performs specific validation of an migration. + protected abstract ValidationResult ValidateActionMigration( + string name, ActionMigration actionMigration, Config config); +} diff --git a/src/Copybara.Core/Config/PathBasedConfigFile.cs b/src/Copybara.Core/Config/PathBasedConfigFile.cs new file mode 100644 index 000000000..620d22524 --- /dev/null +++ b/src/Copybara.Core/Config/PathBasedConfigFile.cs @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using SysPath = System.IO.Path; + +namespace Copybara.Config; + +/// +/// A Skylark dependency resolver that resolves relative paths and absolute paths if +/// rootPath is defined. +/// +public class PathBasedConfigFile : ConfigFile +{ + private readonly string _path; + private readonly string? _rootPath; + private readonly string? _identifierPrefix; + + public PathBasedConfigFile(string path, string? rootPath, string? identifierPrefix) + { + Preconditions.CheckArgument(SysPath.IsPathRooted(path), "path must be absolute"); + _path = path; + _rootPath = rootPath; + _identifierPrefix = identifierPrefix; + if (identifierPrefix != null) + { + // Check we don't generate weird identifiers like identifierPrefix + "/absolute/path" + Preconditions.CheckNotNull(rootPath, "identifierPrefix requires a non null root"); + } + } + + public ConfigFile Resolve(string path) + { + string resolved = ConfigFile.IsAbsolute(path) + ? RelativeToRoot(path) + : RelativeToCurrentPath(path); + + if (!File.Exists(resolved) && !Directory.Exists(resolved)) + { + throw new CannotResolveLabel( + string.Format("Cannot find '{0}'. '{1}' does not exist.", path, resolved)); + } + if (!File.Exists(resolved)) + { + throw new CannotResolveLabel( + string.Format("Cannot find '{0}'. '{1}' is not a file.", path, resolved)); + } + return new PathBasedConfigFile(resolved, _rootPath, _identifierPrefix); + } + + public string Path() => _path; + + public string GetIdentifier() + { + if (_rootPath == null) + { + return Path(); + } + + return (string.IsNullOrEmpty(_identifierPrefix) ? "" : _identifierPrefix + "/") + + Relativize(_rootPath, _path); + } + + private string RelativeToCurrentPath(string label) + { + string? dir = SysPath.GetDirectoryName(_path); + return dir == null ? label : SysPath.Combine(dir, label); + } + + private string RelativeToRoot(string path) + { + if (_rootPath == null) + { + throw new CannotResolveLabel( + "Absolute paths are not allowed because the root config path couldn't be" + + " automatically detected. Use " + GeneralOptions.ConfigRootFlag); + } + return SysPath.Combine(_rootPath, path.Substring(2)); + } + + public byte[] ReadContentBytes() + { + try + { + return File.ReadAllBytes(_path); + } + catch (FileNotFoundException e) + { + throw new CannotResolveLabel("Cannot resolve " + _path, e); + } + catch (DirectoryNotFoundException e) + { + throw new CannotResolveLabel("Cannot resolve " + _path, e); + } + } + + public override string ToString() => + $"PathBasedConfigFile{{path={_path}, rootPath={_rootPath}, identifierPrefix={_identifierPrefix}}}"; + + private static string Relativize(string root, string path) + { + string rel = SysPath.GetRelativePath(root, path); + // Normalize to forward slashes so identifiers are stable across platforms. + return rel.Replace(SysPath.DirectorySeparatorChar, '/'); + } +} diff --git a/src/Copybara.Core/Config/ResolveDelegateConfigFile.cs b/src/Copybara.Core/Config/ResolveDelegateConfigFile.cs new file mode 100644 index 000000000..a3140aec2 --- /dev/null +++ b/src/Copybara.Core/Config/ResolveDelegateConfigFile.cs @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Copybara.Config; + +/// +/// A that delegates to a main config file and falls back to a secondary one +/// for file resolution, if necessary. +/// +/// This is useful for cases where generated in-memory configurations have dependencies on +/// persisted configurations. +/// +public sealed class ResolveDelegateConfigFile : ConfigFile +{ + private static readonly ILogger Logger = NullLogger.Instance; + + private readonly ConfigFile _mainConfigFile; + private readonly ConfigFile _secondConfigFile; + + public ResolveDelegateConfigFile(ConfigFile mainConfigFile, ConfigFile secondConfigFile) + { + _mainConfigFile = Preconditions.CheckNotNull(mainConfigFile); + _secondConfigFile = Preconditions.CheckNotNull(secondConfigFile); + } + + public ConfigFile Resolve(string path) + { + try + { + return _mainConfigFile.Resolve(path); + } + catch (CannotResolveLabel) + { + Logger.LogInformation( + "Could not resolve {Path} from {Main}. Resolving from {Second}.", + path, _mainConfigFile.Path(), _secondConfigFile.Path()); + try + { + return _secondConfigFile.Resolve(path); + } + catch (CannotResolveLabel crl) + { + throw new CannotResolveLabel( + string.Format( + "Could not resolve main config or second config to path '{0}'. Main config" + + " path is '{1}', second config path is '{2}'", + path, _mainConfigFile.Path(), _secondConfigFile.Path()), + crl); + } + } + } + + public string Path() => _mainConfigFile.Path(); + + public byte[] ReadContentBytes() => _mainConfigFile.ReadContentBytes(); + + public string GetIdentifier() => _mainConfigFile.GetIdentifier(); +} diff --git a/src/Copybara.Core/Config/SkylarkParser.cs b/src/Copybara.Core/Config/SkylarkParser.cs new file mode 100644 index 000000000..e10f6e823 --- /dev/null +++ b/src/Copybara.Core/Config/SkylarkParser.cs @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Reflection; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util.Console; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Starlark.Annot; +using Starlark.Eval; +using Starlark.Syntax; +using Console = Copybara.Util.Console.Console; +using StarlarkRt = Starlark.Eval.Starlark; +using Module = Starlark.Eval.Module; +using Tuple = Starlark.Eval.Tuple; +using FileOptions = Starlark.Syntax.FileOptions; + +namespace Copybara.Config; + +/// Loads Copybara configs out of Skylark files. +public class SkylarkParser +{ + private static readonly ILogger Logger = NullLogger.Instance; + private const string DefaultExtension = ".bara.sky"; + + private static readonly ImmutableHashSet AllowedLoadExtensions = + ImmutableHashSet.Create(DefaultExtension, ".scl"); + + private static readonly object VisibilityFunc = new VisibilityFunction(); + + // For now all the modules are namespaces. We don't use variables except for 'core'. + private readonly IReadOnlyList _modules; + private readonly StarlarkMode _validation; + + public SkylarkParser(IReadOnlySet staticModules, StarlarkMode validation) + { + var builder = ImmutableArray.CreateBuilder(); + builder.Add(typeof(GlobalMigrations)); + builder.AddRange(staticModules); + _modules = builder.ToImmutable(); + _validation = validation; + } + + /// + public Config LoadConfig(ConfigFile config, ModuleSet moduleSet, Console console) => + GetConfigWithTransitiveImports(config, moduleSet, console).GetConfig(); + + private Config LoadConfigInternal( + ConfigFile content, + ModuleSet moduleSet, + Func> configFilesSupplier, + Console console) + { + Module module = new Evaluator(this, moduleSet, content, configFilesSupplier, console) + .Eval(content); + GlobalMigrations globalMigrations = GlobalMigrations.GetGlobalMigrations(module); + return new Config( + globalMigrations.GetMigrations(), content.Path(), module.GetPredeclaredBindings()); + } + + public Module ExecuteSkylark(ConfigFile content, ModuleSet moduleSet, Console console) + { + var capturingConfigFile = new CapturingConfigFile(content); + var configFilesSupplier = new ConfigFilesSupplier(); + + Module module = new Evaluator(this, moduleSet, content, configFilesSupplier.Get, console) + .Eval(content); + configFilesSupplier.SetConfigFiles(capturingConfigFile.GetAllLoadedFiles()); + return module; + } + + /// + /// Collect all ConfigFiles retrieved by the parser while loading . + /// + /// Root file of the configuration. + /// the module set providing the Starlark globals. + /// the console to use for printing error/information. + /// A map linking paths to the captured ConfigFiles and the parsed Config. + /// If config is invalid, references an invalid file or + /// contains dependency cycles. + public ConfigWithDependencies GetConfigWithTransitiveImports( + ConfigFile config, ModuleSet moduleSet, Console console) + { + var capturingConfigFile = new CapturingConfigFile(config); + var configFilesSupplier = new ConfigFilesSupplier(); + + Config parsedConfig = + LoadConfigInternal(capturingConfigFile, moduleSet, configFilesSupplier.Get, console); + + ImmutableDictionary allLoadedFiles = + capturingConfigFile.GetAllLoadedFiles(); + + configFilesSupplier.SetConfigFiles(allLoadedFiles); + + return new ConfigWithDependencies(allLoadedFiles, parsedConfig); + } + + private sealed class ConfigFilesSupplier + { + private ImmutableDictionary? _configFiles; + + internal void SetConfigFiles(ImmutableDictionary configFiles) + { + Preconditions.CheckState(_configFiles == null, "Already set"); + _configFiles = Preconditions.CheckNotNull(configFiles); + } + + internal ImmutableDictionary Get() + { + // We need to load all the files before knowing the set of files in the config. + return Preconditions.CheckNotNull( + _configFiles, "Don't call the supplier before loading finishes."); + } + } + + /// A class that traverses and evaluates the config file dependency graph. + private sealed class Evaluator + { + private readonly SkylarkParser _parser; + private readonly HashSet _pending = new(); + private readonly List _pendingOrder = new(); + private readonly Dictionary _loaded = new(); + private readonly Console _console; + private readonly ConfigFile _mainConfigFile; + + // Predeclared environment shared by all files (modules) loaded. + private readonly ImmutableDictionary _environment; + private readonly ModuleSet _moduleSet; + + internal Evaluator( + SkylarkParser parser, + ModuleSet moduleSet, + ConfigFile mainConfigFile, + Func> configFilesSupplier, + Console console) + { + _parser = parser; + _console = Preconditions.CheckNotNull(console); + _mainConfigFile = Preconditions.CheckNotNull(mainConfigFile); + _moduleSet = Preconditions.CheckNotNull(moduleSet); + _environment = _parser.CreateEnvironment(_moduleSet, configFilesSupplier); + } + + internal Module Eval(ConfigFile content) + { + if (_pending.Contains(content.Path())) + { + throw ThrowCycleError(content.Path()); + } + if (_loaded.TryGetValue(content.Path(), out Module? existing)) + { + return existing; + } + _pending.Add(content.Path()); + _pendingOrder.Add(content.Path()); + + // Make the modules available as predeclared bindings. + StarlarkSemantics semantics = StarlarkSemantics.DEFAULT; + Module module = Module.WithPredeclared(semantics, _environment); + + // parse & compile + ParserInput input = ParserInput.FromUTF8(content.ReadContentBytes(), content.Path()); + FileOptions options = + FileOptions.DEFAULT.ToBuilder() + // Ordinarily, load statements should create file-local variables. + // For now, we make them create first-class members of Module.globals. + .LoadBindsGlobally(true) + .AllowToplevelRebinding(true) // allow e.g. x=1; x=2 at top level + .RequireLoadStatementsFirst(_parser._validation == StarlarkMode.Strict) + .Build(); + + Program prog; + try + { + prog = Program.CompileFile( + StarlarkFile.Parse(input, options), StarlarkRt.ModuleAsResolverModule(module)); + } + catch (SyntaxError.Exception ex) + { + foreach (SyntaxError error in ex.Errors) + { + _console.Error(error.ToString()); + } + throw new ValidationException("Error loading config file."); + } + + // process loads + var loadedModules = new Dictionary(); + var fileToLoad = new Dictionary(); + foreach (string l in prog.GetLoads().Distinct()) + { + string key = AllowedLoadExtensions.Any(l.EndsWith) ? l : l + DefaultExtension; + fileToLoad[key] = l; + } + + foreach (var entry in + content + // Resolve all in one call so the implementor can do it in batch/parallel. + .ResolveAll(fileToLoad.Keys.ToImmutableHashSet())) + { + Module loadedModule = Eval(entry.Value); + loadedModules[fileToLoad[entry.Key]] = loadedModule; + } + + // execute + _parser.UpdateEnvironmentForConfigFile( + StarlarkPrint, content, _mainConfigFile, _environment, _moduleSet); + using (Mutability mu = Mutability.Create("CopybaraModules")) + { + StarlarkThread thread = StarlarkThread.CreateTransient(mu, semantics); + thread.SetLoader(m => loadedModules.TryGetValue(m, out Module? md) ? md : null); + thread.SetPrintHandler(StarlarkPrint); + try + { + StarlarkRt.ExecFileProgram(prog, module, thread); + } + catch (EvalException ex) + { + _console.Error(ex.Message); + throw new ValidationException("Error loading config file", ex); + } + } + + _pending.Remove(content.Path()); + _pendingOrder.Remove(content.Path()); + _loaded[content.Path()] = module; + return module; + } + + private void StarlarkPrint(StarlarkThread thread, string msg) => + _console.Verbose(thread.GetCallerLocation() + ": " + msg); + + private ValidationException ThrowCycleError(string cycleElement) + { + var sb = new System.Text.StringBuilder(); + foreach (string element in _pendingOrder) + { + sb.Append(element.Equals(cycleElement) ? "* " : " "); + sb.Append(element).Append('\n'); + } + sb.Append("* ").Append(cycleElement).Append('\n'); + _console.Error("Cycle was detected in the configuration: \n" + sb); + return new ValidationException("Cycle was detected"); + } + } + + /// Updates the module globals with information about the current loaded config file. + private void UpdateEnvironmentForConfigFile( + StarlarkThread.PrintHandler printHandler, + ConfigFile currentConfigFile, + ConfigFile mainConfigFile, + ImmutableDictionary environment, + ModuleSet moduleSet) + { + foreach (object module in moduleSet.GetModules().Values) + { + // We mutate the module per file loaded. Not ideal but it is the best we can do. + if (module is ILabelsAwareModule m) + { + m.SetConfigFile(mainConfigFile, currentConfigFile); + m.SetPrintHandler(printHandler); + } + } + foreach (Type module in _modules) + { + Logger.LogInformation("Creating variable for {Module}", module.FullName); + // We mutate the module per file loaded. Not ideal but it is the best we can do. + if (typeof(ILabelsAwareModule).IsAssignableFrom(module)) + { + var m = (ILabelsAwareModule)environment[GetModuleName(module)]; + m.SetConfigFile(mainConfigFile, currentConfigFile); + m.SetPrintHandler(printHandler); + } + } + } + + /// + /// Create the environment for all evaluations (will be shared between all the dependent files + /// loaded). + /// + private ImmutableDictionary CreateEnvironment( + ModuleSet moduleSet, Func> configFilesSupplier) + { + var env = new Dictionary(); + foreach (var module in moduleSet.GetModules()) + { + Logger.LogInformation("Creating variable for {Module}", module.Key); + if (module.Value is ILabelsAwareModule lam) + { + lam.SetAllConfigResources(configFilesSupplier); + } + // Modules shouldn't use the same name + env[module.Key] = module.Value; + } + + foreach (Type module in _modules) + { + Logger.LogInformation("Creating variable for {Module}", module.FullName); + // Create the module object and associate it with the functions + var envBuilder = new Dictionary(); + var annot = module.GetCustomAttribute(inherit: false); + if (annot != null) + { + envBuilder[annot.Name] = Activator.CreateInstance(module)!; + } + else if (IsLibrary(module)) + { + // Reference-forward: upstream registers @Library modules' methods directly. The + // doc.annotations.Library attribute is not yet ported; handled via IsLibrary probe. + StarlarkRt.AddMethods(envBuilder, Activator.CreateInstance(module)!); + } + + foreach (var e in envBuilder) + { + env[e.Key] = e.Value; + } + + // Add the options to the module that require them + if (typeof(IOptionsAwareModule).IsAssignableFrom(module)) + { + ((IOptionsAwareModule)env[GetModuleName(module)]).SetOptions(moduleSet.GetOptions()); + } + if (typeof(ILabelsAwareModule).IsAssignableFrom(module)) + { + ((ILabelsAwareModule)env[GetModuleName(module)]) + .SetAllConfigResources(configFilesSupplier); + } + } + env["visibility"] = VisibilityFunc; + return env.ToImmutableDictionary(); + } + + private static string GetModuleName(Type cls) => + cls.GetCustomAttribute(inherit: false)!.Name; + + // Probes for a doc.annotations.Library-style attribute by name so the port keeps working once + // that annotation lands, without a hard dependency on it here. + private static bool IsLibrary(Type module) => + module.GetCustomAttributes(inherit: false) + .Any(a => a.GetType().Name is "LibraryAttribute" or "Library"); + + private sealed class VisibilityFunction : IStarlarkCallable + { + public object Call(StarlarkThread thread, Tuple args, Dict kwargs) => StarlarkRt.None; + + public object? Fastcall(StarlarkThread thread, object?[] positional, object?[] named) => + StarlarkRt.None; + + public string Name => "visibility"; + + public void Repr(Printer printer, StarlarkSemantics semantics) => + printer.Append(""); + + public Location Location => Location.BUILTIN; + } +} diff --git a/src/Copybara.Core/Config/SkylarkUtil.cs b/src/Copybara.Core/Config/SkylarkUtil.cs new file mode 100644 index 000000000..f34ec7be1 --- /dev/null +++ b/src/Copybara.Core/Config/SkylarkUtil.cs @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Config; + +/// +/// Utilities for dealing with Skylark parameter objects and converting them to Java ones. +/// +public static class SkylarkUtil +{ + /// + /// Converts an object that can be the NoneType to the actual object if it is not, or returns the + /// default value if none. + /// + public static T? ConvertFromNoneable(object? obj, T? defaultValue) + { + if (StarlarkRt.IsNullOrNone(obj)) + { + return defaultValue; + } + return (T)obj!; // wildly unsound cast, matching upstream + } + + /// + /// Converts a noneable Starlark object into a nullable reference. If the object is null or None, + /// it will be converted to null. + /// + /// Upstream returns Optional<T>; this port uses nullable per project + /// conventions. + /// + public static T? ConvertToOptional(object? obj) + where T : class => + ConvertFromNoneable(obj, null); + + /// Converts a string to the corresponding enum or fails if invalid value. + /// if the value is not a valid enum member + public static T StringToEnum(string fieldName, string value) + where T : struct, Enum + { + if (Enum.TryParse(value, out T result) && Enum.IsDefined(result)) + { + return result; + } + throw StarlarkRt.Errorf( + "Invalid value '{0}' for field '{1}'. Valid values are: {2}", + value, fieldName, string.Join(", ", Enum.GetNames())); + } + + /// Converts a sequence of strings to a list of the corresponding enum values. + /// if any string cannot be cast to the enum + public static IReadOnlyList StringListToEnumList( + IEnumerable sequence, string fieldName, Console console) + where T : struct, Enum + { + var list = sequence.ToList(); + try + { + return list.Select(value => + { + if (Enum.TryParse(value, out T result) && Enum.IsDefined(result)) + { + return result; + } + throw new ArgumentException(value); + }).ToList(); + } + catch (ArgumentException e) + { + console.ErrorFmt( + "Failed to convert list of strings '{0}' to list of enums. Cause: {1}", + string.Join(", ", list), e.Message); + throw StarlarkRt.Errorf( + "Invalid value '{0}' for field '{1}'. Valid values are: {2}", + string.Join(", ", list), fieldName, string.Join(", ", Enum.GetNames())); + } + } + + /// Checks that a mandatory string field is not empty. + /// if the value is null or empty + public static string CheckNotEmpty(string? value, string name) + { + Check(!string.IsNullOrEmpty(value), "Invalid empty field '{0}'.", name); + return value!; + } + + /// Checks a condition or throws . + /// if the condition is false + public static void Check(bool condition, string format, params object?[] args) + { + if (!condition) + { + throw StarlarkRt.Errorf(format, args); + } + } + + /// + /// Converts a Starlark sequence value (such as a list or tuple) to a list of strings. The result + /// is a new, mutable copy. It throws EvalException if x is not a Starlark iterable or if any of + /// its elements are not strings. The message argument is prefixed to any error message. + /// + /// if x is not a sequence or an element is not a string + public static List ConvertStringList(object? x, string message) + { + if (x is not ISequence seq) + { + throw StarlarkRt.Errorf("{0}: got {1}, want sequence", message, StarlarkRt.Type(x)); + } + + var result = new List(); + foreach (object? elem in seq) + { + if (elem is not string s) + { + throw StarlarkRt.Errorf( + "{0}: at index #{1}, got {2}, want string", + message, result.Count, StarlarkRt.Type(elem)); + } + result.Add(s); + } + return result; + } + + /// + /// Converts a Starlark dict value to a map of strings to strings. The result is a new, mutable + /// copy. It throws EvalException if x is not a Starlark dict or if any of its keys or values are + /// not strings. The message argument is prefixed to any error message. + /// + /// if x is not a dict or a key/value is not a string + public static Dictionary ConvertStringMap(object? x, string message) + { + if (x is not Dict dict) + { + throw StarlarkRt.Errorf("{0}: got {1}, want dict", message, StarlarkRt.Type(x)); + } + var result = new Dictionary(); + foreach (var e in dict.Entries) + { + if (e.Key is not string key) + { + throw StarlarkRt.Errorf( + "{0}: in dict key, got {1}, want string", message, StarlarkRt.Type(e.Key)); + } + if (e.Value is not string value) + { + throw StarlarkRt.Errorf( + "{0}: in value for dict key '{1}', got {2}, want string", + message, e.Key, StarlarkRt.Type(e.Value)); + } + result[key] = value; + } + return result; + } + + /// + /// Converts a Starlark optional string value (string or None) to a nullable String reference. + /// + public static string? ConvertOptionalString(object? x) => + StarlarkRt.IsNullOrNone(x) ? null : (string)x!; +} diff --git a/src/Copybara.Core/Config/ValidationResult.cs b/src/Copybara.Core/Config/ValidationResult.cs new file mode 100644 index 000000000..9ffaf536e --- /dev/null +++ b/src/Copybara.Core/Config/ValidationResult.cs @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; + +namespace Copybara.Config; + +/// The immutable result of a validation performed by . +public sealed class ValidationResult +{ + public static readonly ValidationResult EMPTY = + new(ImmutableArray.Empty); + + private readonly ImmutableArray _messages; + + private ValidationResult(ImmutableArray messages) => _messages = messages; + + /// Returns all the s in the order they were registered. + public IReadOnlyList GetAllMessages() => _messages; + + /// Returns true iff there was at least one warning message. + public bool HasWarnings() => GetWarnings().Count > 0; + + /// Returns true iff there was at least one error message. + public bool HasErrors() => GetErrors().Count > 0; + + /// Returns the text of the warning messages, in the order that were registered. + public IReadOnlyList GetWarnings() => + _messages.Where(v => v.GetLevel() == Level.WARNING).Select(v => v.GetMessage()) + .ToImmutableArray(); + + /// Returns the text of the error messages, in the order that were registered. + public IReadOnlyList GetErrors() => + _messages.Where(v => v.GetLevel() == Level.ERROR).Select(v => v.GetMessage()) + .ToImmutableArray(); + + public override string ToString() => + $"ValidationResult{{messages=[{string.Join(", ", _messages)}]}}"; + + /// + /// Levels of validation messages. Can only be warning or error, because it doesn't make sense + /// to have info here. + /// + public enum Level + { + WARNING, + ERROR, + } + + /// Encapsulates a validation message and a . + public sealed class ValidationMessage + { + private readonly Level _level; + private readonly string _message; + + internal ValidationMessage(Level level, string message) + { + _level = level; + _message = Preconditions.CheckNotNull(message); + } + + public Level GetLevel() => _level; + + public string GetMessage() => _message; + + /// + /// Generates a string from this validation message with padded level and message text. + /// + public override string ToString() => string.Format("{0,-8} {1}", _level, _message); + } + + /// A builder of . + public sealed class Builder + { + private readonly List _messages = new(); + + public Builder Warning(string message) + { + _messages.Add(new ValidationMessage(Level.WARNING, message)); + return this; + } + + public Builder WarningFmt(string message, params object?[] args) + { + _messages.Add(new ValidationMessage(Level.WARNING, string.Format(message, args))); + return this; + } + + public Builder Error(string message) + { + _messages.Add(new ValidationMessage(Level.ERROR, message)); + return this; + } + + public Builder ErrorFmt(string message, params object?[] args) + { + _messages.Add(new ValidationMessage(Level.ERROR, string.Format(message, args))); + return this; + } + + public Builder Append(ValidationResult result) + { + _messages.AddRange(result._messages); + return this; + } + + public ValidationResult Build() => new(_messages.ToImmutableArray()); + } +} diff --git a/src/Copybara.Core/ConfigFileArgs.cs b/src/Copybara.Core/ConfigFileArgs.cs new file mode 100644 index 000000000..9925928a6 --- /dev/null +++ b/src/Copybara.Core/ConfigFileArgs.cs @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; + +namespace Copybara; + +/// +/// Arguments for a command that expects the CLI arguments be like: +/// config_file [workflow [source_ref]]. +/// +public sealed class ConfigFileArgs +{ + private readonly string _configPath; + private readonly string? _workflowName; + private readonly ImmutableArray _sourceRefs; + + public ConfigFileArgs(string configPath, string? workflowName) + : this(configPath, workflowName, ImmutableArray.Empty) + { + } + + public ConfigFileArgs(string configPath, string? workflowName, IEnumerable sourceRefs) + { + _configPath = Preconditions.CheckNotNull(configPath); + _workflowName = workflowName; + _sourceRefs = sourceRefs.ToImmutableArray(); + } + + public string GetConfigPath() => _configPath; + + public string GetWorkflowName() => _workflowName ?? "default"; + + public bool HasWorkflowName() => _workflowName != null; + + /// + /// Returns the first sourceRef from the command arguments, or null if no source ref was + /// provided. + /// + /// This method is provided for convenience, for subcommands that only care about the first + /// source_ref. + /// + public string? GetSourceRef() => _sourceRefs.Length == 0 ? null : _sourceRefs[0]; + + public IReadOnlyList GetSourceRefs() => _sourceRefs; +} diff --git a/src/Copybara.Core/ConfigItemDescription.cs b/src/Copybara.Core/ConfigItemDescription.cs new file mode 100644 index 000000000..9dc90b098 --- /dev/null +++ b/src/Copybara.Core/ConfigItemDescription.cs @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Util; + +namespace Copybara; + +/// +/// Interface for self-description. The information returned should be sufficient to create a new +/// instance with identical migration behavior (but potentially different side effects). This is +/// intended for discovering changes in a config. +/// +public interface IConfigItemDescription +{ + string GetTypeName() => GetType().FullName ?? GetType().Name; + + /// Returns a key-value list of the options the endpoint was instantiated with. + ImmutableListMultimap Describe(Glob? originFiles) + { + var builder = ImmutableListMultimap.CreateBuilder(); + builder.Put("type", GetTypeName()); + return builder.Build(); + } + + /// Returns a key-value list describing the credentials the endpoint was instantiated with. + IReadOnlyList> DescribeCredentials() => + ImmutableArray>.Empty; + + /// Returns a key-value list describing the credentials the endpoint was instantiated with. + IReadOnlyList> DescribeCredentials(string endpoint) + { + var creds = DescribeCredentials(); + if (creds.Count == 0) + { + return creds; + } + var builder = ImmutableArray.CreateBuilder>(); + foreach (var cred in creds) + { + var credBuilder = ImmutableListMultimap.CreateBuilder(); + credBuilder.PutAll(cred); + credBuilder.Put("endpoint", endpoint); + builder.Add(credBuilder.Build()); + } + return builder.ToImmutable(); + } +} diff --git a/src/Copybara.Core/ConfigLoader.cs b/src/Copybara.Core/ConfigLoader.cs new file mode 100644 index 000000000..2131f9df6 --- /dev/null +++ b/src/Copybara.Core/ConfigLoader.cs @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Config; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Util.Console; +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// Loads the configuration from a given config file. +public class ConfigLoader +{ + private readonly SkylarkParser _skylarkParser; + protected readonly ConfigFile ConfigFile; + private readonly ModuleSet _moduleSet; + + public ConfigLoader(ModuleSet moduleSet, ConfigFile configFile, StarlarkMode validateStarlark) + { + _moduleSet = moduleSet; + _skylarkParser = new SkylarkParser(_moduleSet.GetStaticModules(), validateStarlark); + ConfigFile = Preconditions.CheckNotNull(configFile); + } + + /// Returns a string representation of the location of this configuration. + public string Location() => ConfigFile.Path(); + + /// Loads the configuration using this loader. + /// the console to use for reporting progress/errors. + public Config.Config Load(Console console) => LoadForConfigFile(console, ConfigFile); + + /// Loads the configuration and its dependencies using this loader. + /// the console to use for reporting progress/errors. + public ConfigWithDependencies LoadWithDependencies(Console console) + { + console.ProgressFmt("Loading config and dependencies {0}", ConfigFile.GetIdentifier()); + + using (_moduleSet.GetOptions().Get().Profiler() + .Start("loading_config_with_deps")) + { + return _skylarkParser.GetConfigWithTransitiveImports(ConfigFile, _moduleSet, console); + } + } + + protected Config.Config LoadForConfigFile(Console console, ConfigFile configFile) + { + console.ProgressFmt("Loading config {0}", configFile.GetIdentifier()); + + using (_moduleSet.GetOptions().Get().Profiler().Start("loading_config")) + { + return _skylarkParser.LoadConfig(configFile, _moduleSet, console); + } + } + + protected virtual Config.Config DoLoadForRevision(Console console, IRevision revision) => + throw new NotSupportedException( + "This origin/configuration doesn't allow loading configs from specific revisions"); + + public Config.Config LoadForRevision(Console console, IRevision revision) + { + using (_moduleSet.GetOptions().Get().Profiler() + .Start("loading_config_for_revision")) + { + return DoLoadForRevision(console, revision); + } + } + + public virtual bool SupportsLoadForRevision() => false; +} diff --git a/src/Copybara.Core/ConsistencyFileConfiguration.cs b/src/Copybara.Core/ConsistencyFileConfiguration.cs new file mode 100644 index 000000000..362510116 --- /dev/null +++ b/src/Copybara.Core/ConsistencyFileConfiguration.cs @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2026 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara; + +/// An object used to configure Consistency File options. +[StarlarkBuiltin("core.consistency_file_config", Documented = true)] +public sealed class ConsistencyFileConfiguration : IStarlarkValue +{ + private readonly string _path; + private readonly bool _excludeBuildFiles; + + private ConsistencyFileConfiguration(string path, bool excludeBuildFiles) + { + _path = path; + _excludeBuildFiles = excludeBuildFiles; + } + + public static ConsistencyFileConfiguration Create(string path, bool excludeBuildFiles) => + new(path, excludeBuildFiles); + + public string Path() => _path; + + public bool ExcludeBuildFiles() => _excludeBuildFiles; +} diff --git a/src/Copybara.Core/ContextProvider.cs b/src/Copybara.Core/ContextProvider.cs new file mode 100644 index 000000000..dac6824f7 --- /dev/null +++ b/src/Copybara.Core/ContextProvider.cs @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Config; +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// A class providing additional context for CMD. +public interface IContextProvider +{ + /// Get context for CMD. + IReadOnlyDictionary GetContext( + Config.Config config, + ConfigFileArgs configFileArgs, + IConfigLoaderProvider configLoaderProvider, + Console console); + + /// Get context for CMD. + IReadOnlyDictionary GetContext( + ConfigWithDependencies config, + ConfigFileArgs configFileArgs, + IConfigLoaderProvider configLoaderProvider, + Options options, + Console console) => + GetContext(config.GetConfig(), configFileArgs, configLoaderProvider, console); +} diff --git a/src/Copybara.Core/ConvertEncoding.cs b/src/Copybara.Core/ConvertEncoding.cs new file mode 100644 index 000000000..526a4cf63 --- /dev/null +++ b/src/Copybara.Core/ConvertEncoding.cs @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using Copybara.TreeState; +using Copybara.Util; + +namespace Copybara; + +/// Convert encoding for a set of files. +public class ConvertEncoding : ITransformation +{ + private readonly Encoding _before; + private readonly Encoding _after; + private readonly Glob _paths; + + public ConvertEncoding(Encoding before, Encoding after, Glob paths) + { + _before = before; + _after = after; + _paths = paths; + } + + public TransformationStatus Transform(TransformWork work) + { + string checkoutDir = work.GetCheckoutDir(); + var files = new HashSet(); + foreach (var f in work.GetTreeState().Find(_paths.RelativeTo(checkoutDir))) + { + byte[] raw = File.ReadAllBytes(f.GetPath()); + string content = _before.GetString(raw); + File.WriteAllBytes(f.GetPath(), _after.GetBytes(content)); + files.Add(f); + } + work.GetTreeState().NotifyModify(files); + return files.Count == 0 + ? TransformationStatus.Noop("Glob didn't match any file") + : TransformationStatus.Success(); + } + + public ITransformation Reverse() => new ConvertEncoding(_after, _before, _paths); + + public string Describe() => "convert_encoding"; +} diff --git a/src/Copybara.Core/CoreGlobal.cs b/src/Copybara.Core/CoreGlobal.cs new file mode 100644 index 000000000..2090d476e --- /dev/null +++ b/src/Copybara.Core/CoreGlobal.cs @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Authoring; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; + +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara; + +/// +/// A module to expose Starlark glob(), parse_message(), etc. functions. +/// +/// Don't add functions here and prefer the "core" namespace unless it is something really +/// general. +/// +public sealed class CoreGlobal : IStarlarkValue +{ + [StarlarkMethod("glob", + Doc = + "Returns an object which matches every file in the workdir that matches at least one" + + " pattern in include and does not match any of the patterns in exclude.")] + public Glob Glob( + [Param(Name = "include", Named = true, + Doc = "The list of glob patterns to include", + AllowedTypes = new[] { typeof(StarlarkList) })] + StarlarkList include, + [Param(Name = "exclude", Named = true, Positional = false, DefaultValue = "[]", + Doc = "The list of glob patterns to exclude", + AllowedTypes = new[] { typeof(StarlarkList) })] + StarlarkList exclude) + { + var includeStrings = ConvertStringList(include, "include"); + var excludeStrings = ConvertStringList(exclude, "exclude"); + try + { + return Copybara.Util.Glob.CreateGlob(includeStrings, excludeStrings); + } + catch (ArgumentException e) + { + throw StarlarkRt.Errorf( + "Cannot create a glob from: include='{0}' and exclude='{1}': {2}", + string.Join(", ", includeStrings), string.Join(", ", excludeStrings), e.Message); + } + } + + [StarlarkMethod("parse_message", + Doc = "Returns a ChangeMessage parsed from a well formed string.")] + public ChangeMessage ParseMessage( + [Param(Name = "message", Named = true, Doc = "The contents of the change message")] + string changeMessage) + { + try + { + return ChangeMessage.ParseMessage(changeMessage); + } + catch (Exception e) + { + throw StarlarkRt.Errorf( + "Cannot parse change message '{0}': {1}", changeMessage, e.Message); + } + } + + [StarlarkMethod("new_author", + Doc = "Create a new author from a string with the form 'name '")] + public Author NewAuthor( + [Param(Name = "author_string", Named = true, + Doc = "A string representation of the author with the form 'name '")] + string authorString) => + Author.Parse(authorString); + + // Port of SkylarkUtil.convertStringList (that helper is being ported concurrently in + // Copybara.Config). Inlined here to keep CoreGlobal self-contained. + internal static List ConvertStringList(IEnumerable list, string name) + { + var result = new List(); + foreach (var o in list) + { + if (o is not string s) + { + throw StarlarkRt.Errorf( + "Expected a string for element of '{0}', but got {1}", name, StarlarkRt.Type(o)); + } + + result.Add(s); + } + + return result; + } +} diff --git a/src/Copybara.Core/CoreModule.cs b/src/Copybara.Core/CoreModule.cs new file mode 100644 index 000000000..c81c15bf1 --- /dev/null +++ b/src/Copybara.Core/CoreModule.cs @@ -0,0 +1,1493 @@ +/* + * Copyright (C) 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Config; +using Copybara.Exceptions; +using Copybara.TemplateToken; +using Copybara.Transform; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; +using Starlark.Syntax; + +// Domain 'Console' collides with System.Console (not used here directly) and static 'Starlark' +// collides with the root namespace segment. +using StarlarkRt = Starlark.Eval.Starlark; + +// Java's net.starlark.java.eval.Sequence (the interface for Starlark lists/tuples) maps to the +// concrete StarlarkList in this port; the static helper class Starlark.Eval.Sequence is unrelated. +using StarlarkSequence = Starlark.Eval.StarlarkList; +using StructImpl = Copybara.StructModule.StructImpl; + +// 'Authoring' is both a namespace (Copybara.Authoring) and a type; alias the type to disambiguate. +using AuthoringType = Copybara.Authoring.Authoring; + +namespace Copybara; + +/// +/// Main configuration class for creating migrations. +/// +/// This class is exposed in Starlark configuration as an instance variable called "core". So +/// users can use it as: +/// +/// core.workflow( +/// name = "foo", +/// ... +/// ) +/// +/// +[StarlarkBuiltin( + "core", + Doc = "Core functionality for creating migrations, and basic transformations.")] +public class CoreModule : ILabelsAwareModule, IStarlarkValue +{ + // Restrict for label ids like 'BAZEL_REV_ID' or 'Bazel-RevId'. + private static readonly Regex CustomRevidFormat = + new("^([A-Z][A-Z_0-9]{1,30}_REV_ID|[A-Z][a-zA-Z0-9-]{1,30}-RevId)$"); + + private const string CheckLastRevStateName = "check_last_rev_state"; + + private readonly GeneralOptions _generalOptions; + private readonly WorkflowOptions _workflowOptions; + + // TODO(port): reconcile — DebugOptions and FolderModule are being ported concurrently. + private readonly Copybara.Transform.Debug.DebugOptions _debugOptions; + private readonly Copybara.Folder.FolderModule _folderModule; + + private ConfigFile _mainConfigFile = null!; + private Func>? _allConfigFiles; + private StarlarkThread.PrintHandler? _printHandler; + private readonly Dictionary> _transformNames = new(); + private SkylarkConsole? _console; + private readonly object _consoleLock = new(); + + public CoreModule( + GeneralOptions generalOptions, + WorkflowOptions workflowOptions, + Copybara.Transform.Debug.DebugOptions debugOptions, + Copybara.Folder.FolderModule folderModule) + { + _generalOptions = Preconditions.CheckNotNull(generalOptions); + _workflowOptions = Preconditions.CheckNotNull(workflowOptions); + _debugOptions = Preconditions.CheckNotNull(debugOptions); + _folderModule = Preconditions.CheckNotNull(folderModule); + } + + [StarlarkMethod("reverse", + Doc = + "Given a list of transformations, returns the list of transformations equivalent to" + + " undoing all the transformations")] + public StarlarkSequence Reverse( + [Param(Name = "transformations", Named = true, + Doc = "The transformations to reverse", + AllowedTypes = new[] { typeof(StarlarkSequence) })] + StarlarkSequence transforms) + { + var builder = ImmutableArray.CreateBuilder(); + foreach (var t in transforms) + { + try + { + builder.Add(Transformations.ToTransformation(t, "transformations", _printHandler).Reverse()); + } + catch (NonReversibleValidationException e) + { + throw StarlarkRt.Errorf("{0} at {1}", e.Message, GetLocation(t)); + } + } + + var reversed = builder.ToImmutable(); + return StarlarkList.ImmutableCopyOf(reversed.Reverse().Cast()); + } + + private static Location GetLocation(object? transformationOrCallable) => + transformationOrCallable switch + { + IStarlarkCallable callable => callable.Location, + ITransformation transformation => transformation.Location(), + _ => Location.BUILTIN, + }; + + [StarlarkMethod("workflow", + Doc = "Defines a migration pipeline which can be invoked via the Copybara command.", + UseStarlarkThread = true)] + public void Workflow( + [Param(Name = "name", Named = true, Positional = false, Doc = "The name of the workflow.")] + string workflowName, + [Param(Name = "origin", Named = true, Positional = false, + Doc = "Where to read from the code to be migrated, before applying the transformations.")] + object origin, + [Param(Name = "destination", Named = true, Positional = false, + Doc = "Where to write to the code being migrated, after applying the transformations.")] + object destination, + [Param(Name = "authoring", Named = true, Positional = false, + Doc = "The author mapping configuration from origin to destination.")] + AuthoringType authoring, + [Param(Name = "transformations", Named = true, Positional = false, DefaultValue = "[]", + Doc = "The transformations to be run for this workflow. They will run in sequence.")] + StarlarkSequence transformations, + [Param(Name = "origin_files", Named = true, Positional = false, DefaultValue = "None", + Doc = "A glob or list of files relative to the workdir that will be read from the origin.", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object originFiles, + [Param(Name = "destination_files", Named = true, Positional = false, DefaultValue = "None", + Doc = "A glob relative to the root of the destination repository.", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object destinationFiles, + [Param(Name = "mode", Named = true, Positional = false, DefaultValue = "\"SQUASH\"", + Doc = "Workflow mode: SQUASH, ITERATIVE, CHANGE_REQUEST, CHANGE_REQUEST_FROM_SOT.")] + string modeStr, + [Param(Name = "reversible_check", Named = true, Positional = false, DefaultValue = "None", + Doc = "Indicates if the tool should try to reverse all the transformations at the end.", + AllowedTypes = new[] { typeof(bool), typeof(NoneType) })] + object reversibleCheckObj, + [Param(Name = CheckLastRevStateName, Named = true, Positional = false, DefaultValue = "False", + Doc = "If set to true, Copybara will validate that the destination didn't change.")] + bool checkLastRevState, + [Param(Name = "ask_for_confirmation", Named = true, Positional = false, DefaultValue = "False", + Doc = "Show the diff and require the user's confirmation before making a change.")] + bool askForConfirmation, + [Param(Name = "dry_run", Named = true, Positional = false, DefaultValue = "False", + Doc = "Run the migration in dry-run mode.")] + bool dryRunMode, + [Param(Name = "after_migration", Named = true, Positional = false, DefaultValue = "[]", + Doc = "Run a feedback workflow after one migration happens.")] + StarlarkSequence afterMigrations, + [Param(Name = "after_workflow", Named = true, Positional = false, DefaultValue = "[]", + Doc = "Run a feedback workflow after all the changes for this workflow run are migrated.")] + StarlarkSequence afterAllMigrations, + [Param(Name = "change_identity", Named = true, Positional = false, DefaultValue = "None", + Doc = "Customize the identity hash generation.", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object changeIdentityObj, + [Param(Name = "set_rev_id", Named = true, Positional = false, DefaultValue = "True", + Doc = "Whether Copybara adds labels like 'GitOrigin-RevId' in the destination.")] + bool setRevId, + [Param(Name = "smart_prune", Named = true, Positional = false, DefaultValue = "False", + Doc = "Best-effort approach at restoring the non-affected snippets.")] + bool smartPrune, + [Param(Name = "merge_import", Named = true, Positional = false, DefaultValue = "None", + Doc = "A migration mode that shells out to a diffing tool to merge all files.", + AllowedTypes = new[] { typeof(bool), typeof(MergeImportConfiguration), typeof(NoneType) })] + object mergeImportObj, + [Param(Name = "autopatch_config", Named = true, Positional = false, DefaultValue = "None", + Doc = "Configuration that describes the setting for automatic patch file generation.", + AllowedTypes = new[] { typeof(AutoPatchfileConfiguration), typeof(NoneType) })] + object autoPatchFileConfigurationObj, + [Param(Name = "after_merge_transformations", Named = true, Positional = false, DefaultValue = "[]", + Doc = "Perform these transformations after merge_import.")] + StarlarkSequence afterMergeTransformations, + [Param(Name = "migrate_noop_changes", Named = true, Positional = false, DefaultValue = "False", + Doc = "Include all the changes, not only those affecting origin_files or config files.")] + bool migrateNoopChanges, + [Param(Name = "experimental_custom_rev_id", Named = true, Positional = false, DefaultValue = "None", + Doc = "DEPRECATED. Use custom_rev_id.", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object experimentalCustomRevIdField, + [Param(Name = "custom_rev_id", Named = true, Positional = false, DefaultValue = "None", + Doc = "Use this label name instead of the one provided by the origin.", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object customRevIdField, + [Param(Name = "description", Named = true, Positional = false, DefaultValue = "None", + Doc = "A description of what this workflow achieves", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object description, + [Param(Name = "checkout", Named = true, Positional = false, DefaultValue = "True", + Doc = "Allows disabling the checkout.")] + bool checkout, + [Param(Name = "reversible_check_ignore_files", Named = true, Positional = false, DefaultValue = "None", + Doc = "Ignore the files matching the glob in the reversible check", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object reversibleCheckIgnoreFiles, + [Param(Name = "consistency_file_path", Named = true, Positional = false, DefaultValue = "None", + Doc = "Under development. Must end with .bara.consistency", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object consistencyFilePathObj, + [Param(Name = "consistency_file", Named = true, Positional = false, DefaultValue = "None", + Doc = "Consistency file configuration. Can be a boolean or a consistency_file_config object.", + AllowedTypes = new[] { typeof(bool), typeof(ConsistencyFileConfiguration), typeof(NoneType) })] + object consistencyFileObj, + StarlarkThread thread) + { + var mode = SkylarkUtil.StringToEnum("mode", modeStr); + + // Overwrite destination for testing workflow locally. + if (_workflowOptions.ToFolder) + { + destination = _folderModule.Destination(); + } + + var sequenceTransform = + Copybara.Transform.Sequence.FromConfig( + _generalOptions.Profiler(), + null, + _workflowOptions, + transformations, + "transformations", + _printHandler, + _debugOptions.TransformWrapper, + Copybara.Transform.Sequence.NoopBehavior.NOOP_IF_ANY_NOOP); + + ITransformation? reverseTransform = null; + if (!_generalOptions.IsDisableReversibleCheck() + && SkylarkUtil.ConvertFromNoneable(reversibleCheckObj, mode == WorkflowMode.ChangeRequest)) + { + try + { + reverseTransform = sequenceTransform.Reverse(); + } + catch (NonReversibleValidationException e) + { + throw StarlarkRt.Errorf("{0}", e.Message); + } + } + + var changeIdentity = GetChangeIdentity(changeIdentityObj); + + if (!StarlarkRt.IsNullOrNone(experimentalCustomRevIdField)) + { + _generalOptions.GetConsole() + .Warn("experimental_custom_rev_id is deprecated. Use custom_rev_id instead."); + } + + string? customRevId = SkylarkUtil.ConvertFromNoneable( + customRevIdField, SkylarkUtil.ConvertFromNoneable(experimentalCustomRevIdField, null)); + + SkylarkUtil.Check( + customRevId == null || CustomRevidFormat.IsMatch(customRevId), + "Invalid custom_rev_id format. Format: {0}", + CustomRevidFormat.ToString()); + + if (setRevId) + { + SkylarkUtil.Check( + mode != WorkflowMode.ChangeRequest || customRevId == null, + "custom_rev_id is not allowed to be used in CHANGE_REQUEST mode if set_rev_id is set" + + " to true. custom_rev_id is used for looking for the baseline in the origin. No" + + " revId is stored in the destination."); + } + else + { + SkylarkUtil.Check( + mode == WorkflowMode.ChangeRequest || mode == WorkflowMode.ChangeRequestFromSot, + "'set_rev_id = False' is only supported for CHANGE_REQUEST and" + + " CHANGE_REQUEST_FROM_SOT mode."); + } + + if (smartPrune) + { + SkylarkUtil.Check( + mode == WorkflowMode.ChangeRequest, + "'smart_prune = True' is only supported for CHANGE_REQUEST mode."); + } + + if (checkLastRevState) + { + SkylarkUtil.Check( + mode != WorkflowMode.ChangeRequest, + "{0} is not compatible with {1}", + CheckLastRevStateName, + WorkflowMode.ChangeRequest); + } + + var resolvedAuthoring = authoring; + var defaultAuthorFlag = _workflowOptions.GetDefaultAuthorFlag(); + if (defaultAuthorFlag != null) + { + resolvedAuthoring = + new AuthoringType(defaultAuthorFlag, authoring.GetMode(), authoring.GetAllowPredicate()); + } + + string? consistencyFilePath = SkylarkUtil.ConvertFromNoneable(consistencyFilePathObj, null); + MergeImportConfiguration? mergeImport; + if (mergeImportObj is bool objectValue) + { + mergeImport = + objectValue + ? MergeImportConfiguration.Create( + "", + Glob.AllFiles, + !string.IsNullOrEmpty(consistencyFilePath), + MergeImportConfiguration.MergeStrategy.DIFF3) + : null; + } + else + { + mergeImport = SkylarkUtil.ConvertFromNoneable(mergeImportObj, null); + } + + var consistencyConfig = ResolveConsistencyFileConfig(consistencyFileObj, consistencyFilePath); + + if (mergeImport != null && mergeImport.UseConsistencyFile()) + { + SkylarkUtil.Check( + consistencyConfig != null, + "error: use_consistency_file set but consistency_file.path is null"); + } + + if (consistencyConfig != null && mergeImport != null) + { + SkylarkUtil.Check( + mergeImport.UseConsistencyFile(), + "error: consistency_file.path set and merge import is enabled, but" + + " use_consistency_file in merge_import is false"); + } + + var autoPatchfileConfiguration = + SkylarkUtil.ConvertFromNoneable(autoPatchFileConfigurationObj, null); + + var effectiveMode = + _generalOptions.Squash || _workflowOptions.ImportSameVersion ? WorkflowMode.Squash : mode; + + // TODO(port): reconcile — Debug.getCallStack / captureDefinitionStackLocals depend on the + // Starlark Debug helper. Left as empty until reconciled with the Workflow port. + var locals = ImmutableArray>.Empty; + + // Mirrors Java's `new Workflow<>(...)` where origin/destination arrive as untyped Starlark + // objects. The non-generic factory reflects the concrete revision types out of origin and + // destination and instantiates the correctly-typed Workflow. + var workflow = + Copybara.Workflow.Create( + workflowName, + SkylarkUtil.ConvertFromNoneable(description, null), + origin, + destination, + resolvedAuthoring, + sequenceTransform, + _workflowOptions.GetLastRevision(), + _workflowOptions.IsInitHistory(), + _generalOptions, + Glob.WrapGlob(originFiles, Glob.AllFiles), + Glob.WrapGlob(destinationFiles, Glob.AllFiles), + effectiveMode, + _workflowOptions, + reverseTransform, + Glob.WrapGlob(reversibleCheckIgnoreFiles, null), + askForConfirmation, + _mainConfigFile, + _allConfigFiles, + dryRunMode, + checkLastRevState || _workflowOptions.CheckLastRevState, + ConvertListOfActions(afterMigrations, _printHandler), + ConvertListOfActions(afterAllMigrations, _printHandler), + changeIdentity, + setRevId, + smartPrune, + mergeImport, + autoPatchfileConfiguration, + AsSingleTransform(afterMergeTransformations), + _workflowOptions.MigrateNoopChanges || migrateNoopChanges, + customRevId, + checkout, + consistencyConfig, + _workflowOptions.ExpectedFixedRef, + _workflowOptions.PinnedFixedRef, + thread.GetCallStack(), + locals); + + var module = Module.OfInnermostEnclosingStarlarkFunction(thread)!; + RegisterGlobalMigration(workflowName, workflow, module); + } + + private Copybara.Transform.Sequence AsSingleTransform(StarlarkSequence transformations) => + Copybara.Transform.Sequence.FromConfig( + _generalOptions.Profiler(), + null, + _workflowOptions, + transformations, + "transformations", + _printHandler, + _debugOptions.TransformWrapper, + Copybara.Transform.Sequence.NoopBehavior.NOOP_IF_ANY_NOOP); + + private static ImmutableArray GetChangeIdentity(object changeIdentityObj) + { + string? changeIdentity = SkylarkUtil.ConvertFromNoneable(changeIdentityObj, null); + + if (changeIdentity == null) + { + return ImmutableArray.Empty; + } + + var result = new Parser().Parse(changeIdentity); + bool configVarFound = false; + foreach (var token in result) + { + if (token.GetTokenType() != TokenType.Interpolation) + { + continue; + } + + if (token.GetValue() == Copybara.Workflow.COPYBARA_CONFIG_PATH_IDENTITY_VAR) + { + configVarFound = true; + continue; + } + + if (token.GetValue() == Copybara.Workflow.COPYBARA_WORKFLOW_NAME_IDENTITY_VAR + || token.GetValue() == Copybara.Workflow.COPYBARA_REFERENCE_IDENTITY_VAR + || token.GetValue().StartsWith(Copybara.Workflow.COPYBARA_REFERENCE_LABEL_VAR)) + { + continue; + } + + throw StarlarkRt.Errorf("Unrecognized variable: {0}", token.GetValue()); + } + + SkylarkUtil.Check( + configVarFound, + "${{{0}}} variable is required", + Copybara.Workflow.COPYBARA_CONFIG_PATH_IDENTITY_VAR); + return result.ToImmutableArray(); + } + + [StarlarkMethod("move", + Doc = "Moves files between directories and renames files", + UseStarlarkThread = true)] + public ITransformation Move( + [Param(Name = "before", Named = true, + Doc = "The name of the file or directory before moving.")] + string before, + [Param(Name = "after", Named = true, + Doc = "The name of the file or directory after moving.")] + string after, + [Param(Name = "paths", Named = true, DefaultValue = "None", + Doc = "A glob expression relative to 'before' if it represents a directory.", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object paths, + [Param(Name = "overwrite", Named = true, DefaultValue = "False", + Doc = "Overwrite destination files if they already exist.")] + bool overwrite, + [Param(Name = "regex_groups", Named = true, Positional = false, DefaultValue = "{}", + Doc = "A set of named regexes that can be used to match part of the file name.")] + Dict regexes, + StarlarkThread thread) + { + SkylarkUtil.Check( + before != after, + "Moving from the same folder to the same folder is a noop. Remove the transformation."); + + return CopyOrMove.CreateMove( + before, + after, + SkylarkUtil.ConvertStringMap(regexes, "regex_groups"), + Glob.WrapGlob(paths, Glob.AllFiles), + overwrite, + thread.GetCallerLocation()); + } + + [StarlarkMethod("rename", + Doc = + "A transformation for renaming several filenames in the working directory. This is a" + + " simplified version of core.move() for just renaming filenames.", + UseStarlarkThread = true)] + public ITransformation Rename( + [Param(Name = "before", Named = true, Doc = "The filepath or suffix to change")] + string before, + [Param(Name = "after", Named = true, Doc = "A filepath or suffix to use as replacement")] + string after, + [Param(Name = "paths", Named = true, DefaultValue = "None", + Doc = "A glob expression relative to 'before' if it represents a directory.", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object paths, + [Param(Name = "overwrite", Named = true, DefaultValue = "False", + Doc = "Overwrite destination files if they already exist.")] + bool overwrite, + [Param(Name = "suffix", Named = true, DefaultValue = "False", + Doc = "When set to true, it will match partial parts of the path string.")] + bool suffix, + StarlarkThread thread) + { + SkylarkUtil.Check( + before != after, + "Renaming from the same filename to the same filename is a noop. Remove the" + + " transformation."); + + // TODO(port): reconcile — Rename transform is being ported concurrently. + return new Copybara.Transform.Rename( + before, + after, + Glob.WrapGlob(paths, Glob.AllFiles), + overwrite, + suffix, + thread.GetCallerLocation()); + } + + [StarlarkMethod("copy", + Doc = "Copy files between directories and renames files", + UseStarlarkThread = true)] + public ITransformation Copy( + [Param(Name = "before", Named = true, Doc = "The name of the file or directory to copy.")] + string before, + [Param(Name = "after", Named = true, Doc = "The name of the file or directory destination.")] + string after, + [Param(Name = "paths", Named = true, DefaultValue = "None", + Doc = "A glob expression relative to 'before' if it represents a directory.", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object paths, + [Param(Name = "overwrite", Named = true, DefaultValue = "False", + Doc = "Overwrite destination files if they already exist.")] + bool overwrite, + [Param(Name = "regex_groups", Named = true, Positional = false, DefaultValue = "{}", + Doc = "A set of named regexes that can be used to match part of the file name.")] + Dict regexes, + StarlarkThread thread) + { + SkylarkUtil.Check( + before != after, + "Copying from the same folder to the same folder is a noop. Remove the transformation."); + return CopyOrMove.CreateCopy( + before, + after, + SkylarkUtil.ConvertStringMap(regexes, "regex_groups"), + Glob.WrapGlob(paths, Glob.AllFiles), + overwrite, + thread.GetCallerLocation()); + } + + [StarlarkMethod("remove", + Doc = + "Remove files from the workdir. **This transformation is only meant to be used inside" + + " core.transform for reversing core.copy like transforms**.", + UseStarlarkThread = true)] + public Copybara.Transform.Remove Remove( + [Param(Name = "paths", Named = true, Doc = "The files to be deleted")] + Glob paths, + StarlarkThread thread) => + // TODO(port): reconcile — Remove transform is being ported concurrently. + new(paths, thread.GetCallerLocation()); + + [StarlarkMethod("convert_encoding", + Doc = "Change the encoding for a set of files", + UseStarlarkThread = true)] + public ITransformation ConvertEncoding( + [Param(Name = "before", Named = true, + Doc = "The expected encoding of the files before transformation.")] + string before, + [Param(Name = "after", Named = true, Doc = "The encoding to convert to.")] + string after, + [Param(Name = "paths", Named = true, Doc = "The files to be converted")] + Glob paths, + StarlarkThread thread) + { + System.Text.Encoding cBefore; + try + { + cBefore = System.Text.Encoding.GetEncoding(before); + } + catch (ArgumentException e) + { + throw new EvalException("Incorrect charset " + before + " for 'before': " + e.Message); + } + + System.Text.Encoding cAfter; + try + { + cAfter = System.Text.Encoding.GetEncoding(after); + } + catch (ArgumentException e) + { + throw new EvalException("Incorrect charset " + after + " for 'after': " + e.Message); + } + + // TODO(port): reconcile — ConvertEncoding transform is being ported concurrently. + return new Copybara.ConvertEncoding(cBefore, cAfter, paths); + } + + [StarlarkMethod("replace", + Doc = + "Replace a text with another text using optional regex groups. This transformation can" + + " be automatically reversed.", + UseStarlarkThread = true)] + public Replace Replace( + [Param(Name = "before", Named = true, + Doc = "The text before the transformation. Can contain references to regex groups.")] + string before, + [Param(Name = "after", Named = true, + Doc = "The text after the transformation.")] + string after, + [Param(Name = "regex_groups", Named = true, DefaultValue = "{}", + Doc = "A set of named regexes that can be used to match part of the replaced text.")] + Dict regexes, + [Param(Name = "paths", Named = true, DefaultValue = "None", + Doc = "A glob expression relative to the workdir representing the files to apply.", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object paths, + [Param(Name = "first_only", Named = true, DefaultValue = "False", + Doc = "If true, only replaces the first instance rather than all.")] + bool firstOnly, + [Param(Name = "multiline", Named = true, DefaultValue = "False", + Doc = "Whether to replace text that spans more than one line.")] + bool multiline, + [Param(Name = "repeated_groups", Named = true, DefaultValue = "False", + Doc = "Allow to use a group multiple times.")] + bool repeatedGroups, + [Param(Name = "ignore", Named = true, DefaultValue = "[]", + Doc = "A set of regexes to ignore lines/files.")] + StarlarkSequence ignore, + StarlarkThread thread) => + Copybara.Transform.Replace.Create( + thread.GetCallerLocation(), + before, + after, + SkylarkUtil.ConvertStringMap(regexes, "regex_groups"), + Glob.WrapGlob(paths, Glob.AllFiles), + firstOnly, + multiline, + repeatedGroups, + SkylarkUtil.ConvertStringList(ignore, "patterns_to_ignore"), + _workflowOptions); + + [StarlarkMethod("todo_replace", + Doc = "Replace Google style TODOs. For example `TODO(username, othername)`.", + UseStarlarkThread = true)] + public TodoReplace TodoReplace( + [Param(Name = "tags", Named = true, DefaultValue = "['TODO', 'NOTE']", + Doc = "Prefix tag to look for", + AllowedTypes = new[] { typeof(StarlarkSequence) })] + StarlarkSequence skyTags, + [Param(Name = "mapping", Named = true, DefaultValue = "{}", + Doc = "Mapping of users/strings")] + Dict skyMapping, + [Param(Name = "mode", Named = true, DefaultValue = "'MAP_OR_IGNORE'", + Doc = "Mode for the replace.")] + string modeStr, + [Param(Name = "paths", Named = true, DefaultValue = "None", + Doc = "A glob expression relative to the workdir representing the files to apply.", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object paths, + [Param(Name = "default", Named = true, DefaultValue = "None", + Doc = "Default value if mapping not found.", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object skyDefault, + [Param(Name = "ignore", Named = true, DefaultValue = "None", + Doc = "If set, elements within TODO that match the regex will be ignored.", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object regexToIgnore, + StarlarkThread thread) + { + var mode = SkylarkUtil.StringToEnum("mode", modeStr); + var mapping = SkylarkUtil.ConvertStringMap(skyMapping, "mapping"); + string? defaultString = SkylarkUtil.ConvertFromNoneable(skyDefault, null); + var tags = SkylarkUtil.ConvertStringList(skyTags, "tags").ToImmutableArray(); + string? ignorePattern = SkylarkUtil.ConvertFromNoneable(regexToIgnore, null); + Regex? regexIgnorelist = ignorePattern != null ? new Regex(ignorePattern) : null; + + SkylarkUtil.Check(tags.Length != 0, "'tags' cannot be empty"); + if (mode is Copybara.Transform.TodoReplace.Mode.MAP_OR_DEFAULT + or Copybara.Transform.TodoReplace.Mode.USE_DEFAULT) + { + SkylarkUtil.Check(defaultString != null, "'default' needs to be set for mode '{0}'", mode); + } + else + { + SkylarkUtil.Check(defaultString == null, "'default' cannot be used for mode '{0}'", mode); + } + + if (mode is Copybara.Transform.TodoReplace.Mode.USE_DEFAULT + or Copybara.Transform.TodoReplace.Mode.SCRUB_NAMES) + { + SkylarkUtil.Check(mapping.Count == 0, "'mapping' cannot be used with mode {0}", mode); + } + + return new Copybara.Transform.TodoReplace( + thread.GetCallerLocation(), + Glob.WrapGlob(paths, Glob.AllFiles), + tags, + mode, + mapping, + defaultString, + _workflowOptions.Parallelizer(), + regexIgnorelist); + } + + public const string TodoFilterReplaceExample = + "core.filter_replace(\n" + + " regex = 'TODO\\\\((.*?)\\\\)',\n" + + " group = 1,\n" + + " mapping = core.replace_mapper([\n" + + " core.replace(\n" + + " before = '${p}foo${s}',\n" + + " after = '${p}fooz${s}',\n" + + " regex_groups = { 'p': '.*', 's': '.*'}\n" + + " ),\n" + + " ],\n" + + " all = True\n" + + " )\n" + + ")"; + + public const string SimpleFilterReplaceExample = + "core.filter_replace(\n" + + " regex = 'a.*',\n" + + " mapping = {\n" + + " 'afoo': 'abar',\n" + + " 'abaz': 'abam'\n" + + " }\n" + + ")"; + + [StarlarkMethod("filter_replace", + Doc = + "Applies an initial filtering to find a substring to be replaced and then applies a" + + " `mapping` of replaces for the matched text.", + UseStarlarkThread = true)] + public FilterReplace FilterReplace( + [Param(Name = "regex", Named = true, Doc = "A re2 regex to match a substring of the file", + AllowedTypes = new[] { typeof(string) })] + string regex, + [Param(Name = "mapping", Named = true, DefaultValue = "{}", + Doc = "A mapping function like core.replace_mapper or a dict with mapping values.")] + object mapping, + [Param(Name = "group", Named = true, DefaultValue = "None", + Doc = "Extract a regex group from the matching text.", + AllowedTypes = new[] { typeof(StarlarkInt), typeof(NoneType) })] + object group, + [Param(Name = "paths", Named = true, DefaultValue = "None", + Doc = "A glob expression relative to the workdir representing the files to apply.", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object paths, + [Param(Name = "reverse", Named = true, DefaultValue = "None", + Doc = "A re2 regex used as reverse transformation", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object reverse, + StarlarkThread thread) + { + var func = GetMappingFunction(mapping); + + string afterPattern = SkylarkUtil.ConvertFromNoneable(reverse, regex); + int numGroup = SkylarkUtil.ConvertFromNoneable(group, StarlarkInt.Of(0)).ToInt("group"); + var beforeRegex = new Regex(regex); + int beforeGroups = beforeRegex.GetGroupNumbers().Length - 1; + SkylarkUtil.Check( + numGroup <= beforeGroups, + "group idx is greater than the number of groups defined in '{0}'. Regex has {1} groups", + beforeRegex.ToString(), + beforeGroups); + var afterRegex = new Regex(afterPattern); + int afterGroups = afterRegex.GetGroupNumbers().Length - 1; + SkylarkUtil.Check( + numGroup <= afterGroups, + "reverse_group idx is greater than the number of groups defined in '{0}'. Regex has {1}" + + " groups", + afterRegex.ToString(), + afterGroups); + return new FilterReplace( + _workflowOptions, + beforeRegex, + afterRegex, + numGroup, + numGroup, + func, + Glob.WrapGlob(paths, Glob.AllFiles), + thread.GetCallerLocation()); + } + + public static IReversibleFunction GetMappingFunction(object mapping) + { + if (mapping is Dict dict) + { + var map = SkylarkUtil.ConvertStringMap(dict, "mapping"); + SkylarkUtil.Check( + map.Count != 0, "Empty mapping is not allowed. Remove the transformation instead"); + return new MapMapper(map.ToImmutableDictionary()); + } + + SkylarkUtil.Check( + mapping is IReversibleFunction, + "mapping has to be instance of map or a reversible function"); + return (IReversibleFunction)mapping; + } + + [StarlarkMethod("replace_mapper", + Doc = + "A mapping function that applies a list of replaces until one replaces the text" + + " (Unless `all = True` is used).")] + public ReplaceMapper MapImports( + [Param(Name = "mapping", Named = true, + Doc = "The list of core.replace transformations", + AllowedTypes = new[] { typeof(StarlarkSequence) })] + StarlarkSequence mapping, + [Param(Name = "all", Named = true, Positional = false, DefaultValue = "False", + Doc = "Run all the mappings despite a replace happens.")] + bool all) + { + SkylarkUtil.Check(mapping.Count != 0, "Empty mapping is not allowed"); + var replaces = ImmutableArray.CreateBuilder(); + foreach (var obj in mapping) + { + if (obj is not ITransformation t) + { + throw StarlarkRt.Errorf("Expected a transformation in 'mapping'"); + } + + SkylarkUtil.Check( + t is Replace, "Only core.replace can be used as mapping, but got: {0}", t.Describe()); + var replace = (Replace)t; + SkylarkUtil.Check( + Equals(replace.GetPaths(), Glob.AllFiles), + "core.replace cannot use 'paths' inside core.replace_mapper"); + replaces.Add(replace); + } + + return new ReplaceMapper(replaces.ToImmutable(), all); + } + + [StarlarkMethod("verify_match", + Doc = + "Verifies that a RegEx matches (or not matches) the specified files. Does not transform" + + " anything, but will stop the workflow if it fails.", + UseStarlarkThread = true)] + public VerifyMatch VerifyMatch( + [Param(Name = "regex", Named = true, Doc = "The regex pattern to verify.")] + string regex, + [Param(Name = "paths", Named = true, DefaultValue = "None", + Doc = "A glob expression relative to the workdir representing the files to apply.", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object paths, + [Param(Name = "verify_no_match", Named = true, DefaultValue = "False", + Doc = "If true, the transformation will verify that the RegEx does not match.")] + bool verifyNoMatch, + [Param(Name = "also_on_reversal", Named = true, DefaultValue = "False", + Doc = "If true, the check will also apply on the reversal.")] + bool alsoOnReversal, + [Param(Name = "failure_message", Named = true, DefaultValue = "None", + Doc = "Optional string that will be included in the failure message.")] + object failureMessage, + StarlarkThread thread) => + Copybara.Transform.VerifyMatch.Create( + thread.GetCallerLocation(), + regex, + Glob.WrapGlob(paths, Glob.AllFiles), + verifyNoMatch, + alsoOnReversal, + SkylarkUtil.ConvertOptionalString(failureMessage), + _workflowOptions.Parallelizer()); + + [StarlarkMethod("transform", + Doc = + "Groups some transformations in a transformation that can contain a particular," + + " manually-specified, reversal.")] + public ITransformation Transform( + [Param(Name = "transformations", Named = true, + Doc = "The list of transformations to run as a result of running this transformation.", + AllowedTypes = new[] { typeof(StarlarkSequence) })] + StarlarkSequence transformations, + [Param(Name = "reversal", Named = true, Positional = false, DefaultValue = "None", + Doc = "The list of transformations to run in reverse.", + AllowedTypes = new[] { typeof(StarlarkSequence), typeof(NoneType) })] + object reversal, + [Param(Name = "name", Named = true, Positional = false, DefaultValue = "None", + Doc = "Optional string identifier to name this transform.")] + object name, + [Param(Name = "ignore_noop", Named = true, Positional = false, DefaultValue = "None", + Doc = "WARNING: Deprecated. Use `noop_behavior` instead.", + AllowedTypes = new[] { typeof(bool), typeof(NoneType) })] + object ignoreNoop, + [Param(Name = "noop_behavior", Named = true, Positional = false, DefaultValue = "None", + Doc = "How to handle no-op transformations.", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object noopBehaviorString) + { + ValidationException.CheckCondition( + StarlarkRt.IsNullOrNone(ignoreNoop) || StarlarkRt.IsNullOrNone(noopBehaviorString), + "The deprecated param 'ignore_noop' cannot be set simultaneously with 'noop_behavior'." + + " Prefer using 'noop_behavior'."); + var noopBehavior = + SkylarkUtil.StringToEnum( + "noop_behavior", + SkylarkUtil.ConvertFromNoneable(noopBehaviorString, "NOOP_IF_ANY_NOOP")); + if (ignoreNoop is true) + { + noopBehavior = Copybara.Transform.Sequence.NoopBehavior.IGNORE_NOOP; + } + else if (ignoreNoop is false) + { + noopBehavior = Copybara.Transform.Sequence.NoopBehavior.FAIL_IF_ANY_NOOP; + } + + string? convertedName = SkylarkUtil.ConvertFromNoneable(name, null); + if (!_transformNames.TryGetValue(_mainConfigFile, out var names)) + { + names = new HashSet(); + _transformNames[_mainConfigFile] = names; + } + + if (convertedName != null && !names.Add(convertedName)) + { + throw new ValidationException($"Name `{convertedName}` already used."); + } + + var forward = + Copybara.Transform.Sequence.FromConfig( + _generalOptions.Profiler(), + convertedName, + _workflowOptions, + transformations, + "transformations", + _printHandler, + _debugOptions.TransformWrapper, + noopBehavior); + StarlarkSequence? reverseList = SkylarkUtil.ConvertFromNoneable(reversal, null); + if (reverseList == null) + { + try + { + reverseList = StarlarkList.ImmutableCopyOf(new object?[] { forward.Reverse() }); + } + catch (NonReversibleValidationException) + { + throw StarlarkRt.Errorf( + "transformations are not automatically reversible. Use 'reversal' field to" + + " explicitly configure the reversal of the transform"); + } + } + + var reverse = + Copybara.Transform.Sequence.FromConfig( + _generalOptions.Profiler(), + convertedName, + _workflowOptions, + reverseList, + "reversal", + _printHandler, + _debugOptions.TransformWrapper, + noopBehavior); + return new ExplicitReversal(forward, reverse); + } + + [StarlarkMethod("dynamic_transform", + Doc = "Create a dynamic Starlark transformation. This should only be used by library developers", + UseStarlarkThread = true)] + public ITransformation DynamicTransform( + [Param(Name = "impl", Named = true, Doc = "The Starlark function to call")] + IStarlarkCallable impl, + [Param(Name = "params", Named = true, DefaultValue = "{}", + Doc = "The parameters to the function. Will be available under ctx.params")] + Dict @params, + StarlarkThread thread) => + // TODO(port): reconcile — SkylarkTransformation is being ported concurrently. + new SkylarkTransformation(impl, Dict.CopyOf(thread.Mutability, @params.Entries), _printHandler); + + [StarlarkMethod("dynamic_feedback", + Doc = "Create a dynamic Starlark feedback migration. This should only be used by library developers", + UseStarlarkThread = true)] + public Copybara.Action.IAction DynamicFeedback( + [Param(Name = "impl", Named = true, Doc = "The Starlark function to call")] + IStarlarkCallable impl, + [Param(Name = "params", Named = true, DefaultValue = "{}", + Doc = "The parameters to the function. Will be available under ctx.params")] + Dict @params, + StarlarkThread thread) => + // TODO(port): reconcile — StarlarkAction is being ported concurrently. + new Copybara.Action.StarlarkAction( + FindCallableName(impl, thread), impl, Dict.CopyOf(thread.Mutability, @params.Entries), + _printHandler); + + [StarlarkMethod("action", + Doc = "Create a dynamic Starlark action. This should only be used by library developers.", + UseStarlarkThread = true)] + public Copybara.Action.IAction Action( + [Param(Name = "impl", Named = true, Doc = "The Starlark function to call")] + IStarlarkCallable impl, + [Param(Name = "params", Named = true, DefaultValue = "{}", + Doc = "The parameters to the function. Will be available under ctx.params")] + Dict @params, + StarlarkThread thread) => + new Copybara.Action.StarlarkAction( + FindCallableName(impl, thread), impl, Dict.CopyOf(thread.Mutability, @params.Entries), + _printHandler); + + private static string FindCallableName(IStarlarkCallable impl, StarlarkThread thread) + { + string name = impl.Name; + var stack = thread.GetCallStack(); + if (name == "lambda" && stack.Length > 1 + && stack[stack.Length - 2].Name != "") + { + name = stack[stack.Length - 2].Name; + } + + return name; + } + + [StarlarkMethod("fail_with_noop", + Doc = "If invoked, it will fail the current migration as a noop")] + public Copybara.Action.IAction FailWithNoop( + [Param(Name = "msg", Named = true, Doc = "The noop message")] + string msg) => + throw new EmptyChangeException(msg); + + [StarlarkMethod("main_config_path", + Doc = "Location of the config file. This is subject to change", + StructField = true)] + public string GetMainConfigFile() => _mainConfigFile.GetIdentifier(); + + [StarlarkMethod("feedback", + Doc = + "Defines a migration of changes' metadata, that can be invoked via the Copybara command" + + " in the same way as a regular workflow migrates the change itself.", + UseStarlarkThread = true)] + public NoneType Feedback( + [Param(Name = "name", Named = true, Positional = false, Doc = "The name of the feedback workflow.")] + string workflowName, + [Param(Name = "origin", Named = true, Positional = false, Doc = "The trigger of a feedback migration.")] + ITrigger trigger, + [Param(Name = "destination", Named = true, Positional = false, Doc = "Where to write change metadata to.")] + object destination, + [Param(Name = "actions", Named = true, Positional = false, DefaultValue = "[]", + Doc = "DEPRECATED: **DO NOT USE**")] + StarlarkSequence actionList, + [Param(Name = "action", Named = true, Positional = false, DefaultValue = "None", + Doc = "An action to execute when the migration is triggered")] + object action, + [Param(Name = "description", Named = true, Positional = false, DefaultValue = "None", + Doc = "A description of what this workflow achieves", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object description, + StarlarkThread thread) + { + var destinationProvider = (Copybara.IEndpointProvider)destination; + var migration = + new ActionMigration( + workflowName, + SkylarkUtil.ConvertFromNoneable(description, null), + _mainConfigFile, + trigger, + new StructImpl( + ImmutableDictionary.Empty.Add( + "destination", destinationProvider.GetEndpoint())), + HandleActionActionsMigration(actionList, action), + _generalOptions, + "feedback", + fileSystem: false, + thread.GetCallStack()); + var module = Module.OfInnermostEnclosingStarlarkFunction(thread)!; + RegisterGlobalMigration(workflowName, migration, module); + return StarlarkRt.None; + } + + private ImmutableArray HandleActionActionsMigration( + StarlarkSequence actionList, object action) + { + if (actionList.Count == 0 && ReferenceEquals(action, StarlarkRt.None)) + { + throw new EvalException("'action' is a required field"); + } + + if (actionList.Count != 0 && !ReferenceEquals(action, StarlarkRt.None)) + { + throw new EvalException( + "Cannot use both 'action' and 'actions' field. 'actions' is deprecated, so use" + + " 'action'"); + } + + if (!ReferenceEquals(action, StarlarkRt.None)) + { + return ImmutableArray.Create(MaybeWrapAction(_printHandler, action)); + } + + return ConvertListOfActions(actionList, _printHandler); + } + + [StarlarkMethod("action_migration", + Doc = + "Defines a migration that is more flexible/less-opinionated migration than" + + " `core.workflow`.", + UseStarlarkThread = true)] + public NoneType ActionMigrationMethod( + [Param(Name = "name", Named = true, Positional = false, Doc = "The name of the migration.")] + string workflowName, + [Param(Name = "origin", Named = true, Positional = false, + Doc = "The trigger endpoint of the migration. Accessible as `ctx.origin`")] + ITrigger trigger, + [Param(Name = "endpoints", Named = true, Positional = false, + Doc = "One or more endpoints that the migration will have access to.")] + IStructure endpoints, + [Param(Name = "action", Named = true, Positional = false, + Doc = "The action to execute when the migration is triggered.")] + object action, + [Param(Name = "description", Named = true, Positional = false, DefaultValue = "None", + Doc = "A description of what this workflow achieves", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object description, + [Param(Name = "filesystem", Named = true, Positional = false, DefaultValue = "False", + Doc = "If true, the migration provides access to the filesystem to the endpoints", + AllowedTypes = new[] { typeof(bool) })] + bool filesystem, + StarlarkThread thread) + { + var actions = ImmutableArray.Create(MaybeWrapAction(_printHandler, action)); + var endpointsMap = GetEndpoints(endpoints); + if (!endpointsMap.ContainsKey(ActionMigration.DestinationEndpointName)) + { + throw new EvalException("Action migration must have an endpoint named 'destination'."); + } + + var migration = + new ActionMigration( + workflowName, + SkylarkUtil.ConvertFromNoneable(description, null), + _mainConfigFile, + trigger, + new StructImpl(endpointsMap), + actions, + _generalOptions, + "action_migration", + filesystem, + thread.GetCallStack()); + var module = Module.OfInnermostEnclosingStarlarkFunction(thread)!; + RegisterGlobalMigration(workflowName, migration, module); + return StarlarkRt.None; + } + + private static ImmutableDictionary GetEndpoints(IStructure endpoints) + { + var result = ImmutableDictionary.CreateBuilder(); + foreach (var fieldName in endpoints.GetFieldNames()) + { + var epProvider = endpoints.GetValue(fieldName); + SkylarkUtil.Check( + epProvider is Copybara.IEndpointProvider, + "Only endpoints can be used as values in 'endpoints' but got type '{0}' for {1}", + StarlarkRt.Type(epProvider), + fieldName); + result[fieldName] = ((Copybara.IEndpointProvider)epProvider!).GetEndpoint(); + } + + return result.ToImmutable(); + } + + [StarlarkMethod("console", + StructField = true, + Doc = "Returns a handle to the console object.")] + public SkylarkConsole Console() + { + lock (_consoleLock) + { + _console ??= new SkylarkConsole(_generalOptions.GetConsole()); + } + + return _console; + } + + /// Registers a in the global registry. + protected void RegisterGlobalMigration(string name, IMigration migration, Module module) => + GlobalMigrations.GetGlobalMigrations(module).AddMigration(name, migration); + + [StarlarkMethod("format", + Doc = "Formats a String using Java's String.format style.")] + public string Format( + [Param(Name = "format", Named = true, Doc = "The format string")] + string format, + [Param(Name = "args", Named = true, Doc = "The arguments to format")] + StarlarkSequence args) + { + // Convert StarlarkInt to types known to the formatter. + var array = args.ToArray(); + for (int i = 0; i < array.Length; i++) + { + if (array[i] is StarlarkInt si) + { + array[i] = si.ToNumber(); + } + } + + try + { + // NOTE(port): upstream uses Java's String.format ('%s'); this port uses .NET composite + // formatting ('{0}'). Accepted deviation — see CLAUDE.md. + return string.Format(format, array); + } + catch (FormatException e) + { + throw StarlarkRt.Errorf("Invalid format: {0}: {1}", format, e.Message); + } + } + + // TODO(port): reconcile — the Copybara.Version namespace (IVersionSelector, + // CustomVersionSelector, LatestVersionSelector, OrderedVersionSelector, + // RequestedVersionSelector) is being ported by another agent; these two methods forward-reference + // it and will compile once that scope lands. + [StarlarkMethod("custom_version_selector", + Doc = + "This is experimental: Custom version selector, users are able to define their own" + + " sorting comparator and filter candidates by regex.", + UseStarlarkThread = true)] + public Copybara.Version.IVersionSelector CustomVersionSelector( + [Param(Name = "comparator", Named = true, + Doc = "A callable comparator of two strings.", + AllowedTypes = new[] { typeof(IStarlarkCallable) })] + IStarlarkCallable comparator, + [Param(Name = "regex_filter", Named = true, DefaultValue = "None", + Doc = "Only versions that match this regex will be considered.", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object rawRegexFilter, + StarlarkThread thread) + { + string? filterByRegex = SkylarkUtil.ConvertFromNoneable(rawRegexFilter, null); + // TODO(port): reconcile — CustomVersionSelector is being ported concurrently. + return new Copybara.Version.CustomVersionSelector(comparator, filterByRegex); + } + + [StarlarkMethod("latest_version", + Doc = + "Selects the latest version that matches the format. Using --force in the CLI will force" + + " to use the reference passed as argument instead.", + UseStarlarkThread = true)] + public Copybara.Version.IVersionSelector VersionSelector( + [Param(Name = "format", Named = true, Doc = "The format of the version.")] + string regex, + [Param(Name = "regex_groups", Named = true, DefaultValue = "{}", + Doc = "A set of named regexes that can be used to match part of the versions.")] + Dict groups, + StarlarkThread thread) + { + var groupsMap = SkylarkUtil.ConvertStringMap(groups, "regex_groups"); + + var elements = new SortedDictionary(); + var regexKey = new Regex("^([sn])([0-9])$"); + foreach (var s in groupsMap.Keys) + { + var matcher = regexKey.Match(s); + SkylarkUtil.Check( + matcher.Success, + "Incorrect key for regex_group. Should be in the format of n0, n1, etc. or s0, s1," + + " etc. Value: {0}", + s); + var type = matcher.Groups[1].Value == "s" + ? Copybara.Version.LatestVersionSelector.VersionElementType.ALPHABETIC + : Copybara.Version.LatestVersionSelector.VersionElementType.NUMERIC; + int num = int.Parse(matcher.Groups[2].Value); + SkylarkUtil.Check( + !elements.ContainsKey(num) || elements[num] == type, + "Cannot use same n in both s{0} and n{1}: {2}", + num, + num, + s); + elements[num] = type; + } + + foreach (var num in elements.Keys) + { + if (num > 0) + { + SkylarkUtil.Check( + elements.ContainsKey(num - 1), + "Cannot have s{0} or n{1} if s{2} or n{3} doesn't exist", + num, + num, + num - 1, + num - 1); + } + } + + // TODO(port): reconcile — version selector types are being ported concurrently. + var versionPicker = new Copybara.Version.LatestVersionSelector( + regex, Copybara.Transform.Replace.ParsePatterns(groupsMap), elements, thread.GetCallerLocation()); + var extraGroups = versionPicker.GetUnmatchedGroups(); + SkylarkUtil.Check( + extraGroups.Count == 0, "Extra regex_groups not used in pattern: {0}", extraGroups); + if (_generalOptions.IsForced() || _generalOptions.IsVersionSelectorUseCliRef()) + { + return new Copybara.Version.OrderedVersionSelector( + ImmutableArray.Create( + new Copybara.Version.RequestedVersionSelector(), versionPicker)); + } + + return versionPicker; + } + + private static ImmutableArray ConvertListOfActions( + StarlarkSequence feedbackActions, StarlarkThread.PrintHandler? printHandler) + { + var actions = ImmutableArray.CreateBuilder(); + foreach (var action in feedbackActions) + { + actions.Add(MaybeWrapAction(printHandler, action!)); + } + + return actions.ToImmutable(); + } + + private static Copybara.Action.IAction MaybeWrapAction( + StarlarkThread.PrintHandler? printHandler, object action) + { + if (action is IStarlarkCallable callable) + { + return new Copybara.Action.StarlarkAction( + callable.Name, callable, Dict.Empty(), printHandler); + } + + if (action is Copybara.Action.IAction a) + { + return a; + } + + throw StarlarkRt.Errorf("Invalid action '{0}' of type: {1}", action, action.GetType()); + } + + public void SetConfigFile(ConfigFile mainConfigFile, ConfigFile currentConfigFile) => + _mainConfigFile = mainConfigFile; + + public void SetAllConfigResources(Func> allConfigFiles) => + _allConfigFiles = allConfigFiles; + + public void SetPrintHandler(StarlarkThread.PrintHandler printHandler) => + _printHandler = printHandler; + + [StarlarkMethod("merge_import_config", + Doc = "Describes which paths merge_import mode should be applied")] + public MergeImportConfiguration MergeImportConfigurationMethod( + [Param(Name = "package_path", Named = true, Positional = false, + Doc = "Package location (ex. 'google3/third_party/java/foo').")] + string packagePath, + [Param(Name = "paths", Named = true, Positional = false, DefaultValue = "None", + Doc = "Glob of paths to apply merge_import mode, relative to package_path", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object pathsObj, + [Param(Name = "use_consistency_file", Named = true, Positional = false, DefaultValue = "False", + Doc = "Deprecated. Use consistency_file in core.workflow instead.")] + bool useConsistencyFile, + [Param(Name = "merge_strategy", Named = true, Positional = false, DefaultValue = "'DIFF3'", + Doc = "The strategy to use for merging files.")] + string mergeStrategy) + { + var paths = Glob.WrapGlob(pathsObj, Glob.AllFiles); + return MergeImportConfiguration.Create( + packagePath, + paths!, + useConsistencyFile, + Enum.Parse(mergeStrategy)); + } + + [StarlarkMethod("consistency_file_config", + Doc = "Describes the configuration for consistency file options")] + public ConsistencyFileConfiguration ConsistencyFileConfig( + [Param(Name = "path", Named = true, Positional = false, + DefaultValue = "\"do-not-edit.bara.consistency\"", + Doc = "The path to the consistency file. Must end with .bara.consistency.", + AllowedTypes = new[] { typeof(string) })] + string path, + [Param(Name = "exclude_build_files", Named = true, Positional = false, DefaultValue = "False", + Doc = "Exclude BUILD files from being hashed in consistency files.")] + bool excludeBuildFiles) + { + try + { + ValidationException.CheckCondition( + path.EndsWith(".bara.consistency"), + "Consistency file path must end with .bara.consistency"); + } + catch (ValidationException e) + { + throw new EvalException(e.Message); + } + + return ConsistencyFileConfiguration.Create(path, excludeBuildFiles); + } + + [StarlarkMethod("autopatch_config", + Doc = "Describes in the configuration for automatic patch file generation")] + public AutoPatchfileConfiguration AutoPatchfileConfigurationMethod( + [Param(Name = "header", Named = true, Positional = false, DefaultValue = "None", + Doc = "A string to include at the beginning of each patch file", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object fileContentsPrefix, + [Param(Name = "suffix", Named = true, Positional = false, DefaultValue = "'.patch'", + Doc = "Suffix to use when saving patch files")] + string suffix, + [Param(Name = "directory_prefix", Named = true, Positional = false, DefaultValue = "''", + Doc = "Directory prefix used to relativize filenames when writing patch files.", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object directoryPrefix, + [Param(Name = "directory", Named = true, Positional = false, DefaultValue = "'AUTOPATCHES'", + Doc = "Directory in which to save the patch files.", + AllowedTypes = new[] { typeof(string), typeof(NoneType) })] + object directory, + [Param(Name = "strip_file_names_and_line_numbers", Named = true, Positional = false, + DefaultValue = "False", + Doc = "When true, strip filenames and line numbers from patch files")] + bool stripFileNamesAndLineNumbers, + [Param(Name = "strip_file_names", Named = true, Positional = false, DefaultValue = "False", + Doc = "When true, strip filenames from patch files")] + bool stripFileNames, + [Param(Name = "strip_line_numbers", Named = true, Positional = false, DefaultValue = "False", + Doc = "When true, strip line numbers from patch files")] + bool stripLineNumbers, + [Param(Name = "paths", Named = true, Positional = false, DefaultValue = "None", + Doc = "Only create patch files that match glob. Default is to match all files", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) })] + object globObj) + { + var glob = Glob.WrapGlob(globObj, Glob.AllFiles); + + if (stripFileNamesAndLineNumbers && (stripFileNames || stripLineNumbers)) + { + throw StarlarkRt.Errorf( + "Cannot set both strip_file_names_and_line_numbers and strip_file_names /" + + " strip_line_numbers"); + } + + if (stripFileNamesAndLineNumbers) + { + stripFileNames = true; + stripLineNumbers = true; + } + + return AutoPatchfileConfiguration.Create( + SkylarkUtil.ConvertFromNoneable(fileContentsPrefix, null)!, + suffix, + SkylarkUtil.ConvertFromNoneable(directoryPrefix, null)!, + SkylarkUtil.ConvertFromNoneable(directory, null), + stripFileNames, + stripLineNumbers, + glob!); + } + + private ConsistencyFileConfiguration? ResolveConsistencyFileConfig( + object consistencyFileObj, string? consistencyFilePath) + { + ConsistencyFileConfiguration? consistencyConfig = null; + object? consistencyFileVal = SkylarkUtil.ConvertFromNoneable(consistencyFileObj, null); + if (consistencyFileVal is bool b) + { + consistencyConfig = + b ? ConsistencyFileConfiguration.Create("do-not-edit.bara.consistency", false) : null; + } + else if (consistencyFileVal is ConsistencyFileConfiguration consistencyFileConfiguration) + { + consistencyConfig = consistencyFileConfiguration; + } + + // Validation for mutual exclusivity. + if (consistencyFilePath != null && consistencyFileVal != null) + { + throw StarlarkRt.Errorf( + "Cannot use both 'consistency_file_path' and 'consistency_file' parameters in" + + " workflow."); + } + + if (consistencyFilePath != null) + { + consistencyConfig = ConsistencyFileConfiguration.Create(consistencyFilePath, false); + } + + return consistencyConfig; + } +} diff --git a/src/Copybara.Core/Destination.cs b/src/Copybara.Core/Destination.cs new file mode 100644 index 000000000..aa5e0c8b6 --- /dev/null +++ b/src/Copybara.Core/Destination.cs @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Security.Cryptography; +using Copybara.Common; +using Copybara.Effect; +using Copybara.Revision; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// A repository which a source of truth can be copied to. +/// the revision type this destination handles. +[StarlarkBuiltin("destination", Doc = "A repository which a source of truth can be copied to")] +public interface IDestination : IConfigItemDescription, IStarlarkValue + where R : class, IRevision +{ + /// + /// Creates a writer which is capable of writing to this destination. This writer may maintain + /// state between writing of revisions. + /// + /// + /// if the writer could not be created because of a user error. + /// + IWriter NewWriter(WriterContext writerContext); + + /// + /// Given a reverse workflow with an Origin that is of the same type as this destination, + /// the label that IOrigin.GetLabelName() would return. + /// + string GetLabelNameWhenOrigin(); + + /// + /// A hash function that is preferred by the Destination for use cases where hashing is involved. + /// + HashAlgorithmName GetHashFunction() => HashAlgorithmName.SHA256; + + /// + /// An object which is capable of writing multiple revisions to the destination. This object is + /// allowed to maintain state between the writing of revisions if applicable. + /// + /// the revision type. + interface IWriter : IChangeVisitable + where TR : class, IRevision + { + /// Returns the status of the import at the destination. + DestinationStatus? GetDestinationStatus(Glob destinationFiles, string labelName); + + /// + /// Returns true if this destination stores revisions in the repository so that + /// can be used for discovering the state of the + /// destination and we can use the methods in . + /// + bool SupportsHistory(); + + /// Writes the fully-transformed repository stored at workdir to this destination. + IReadOnlyList Write( + TransformResult transformResult, Glob destinationFiles, Console console); + + /// Utility endpoint for accessing and adding feedback data. + IEndpoint GetFeedbackEndPoint(Console console) => IEndpoint.NoopEndpoint; + + DestinationReader GetDestinationReader( + Console console, Origin.Baseline? baseline, string workdir) => + DestinationReader.NotImplemented; + + DestinationReader GetDestinationReader(Console console, string? baseline, string workdir) => + DestinationReader.NotImplemented; + + IPatchRegenerator? GetPatchRegenerator(Console console) => null; + + /// Returns the object for this destination. + IDestinationInfo? GetDestinationInfo() => null; + } + + /// Writers that implement PatchRegenerator can be used with RegenerateCmd. + interface IPatchRegenerator + { + /// + /// Write the files in the workdir to an already-existing change created by Copybara. This is + /// used to update a pending change with new patch files. + /// + void UpdateChange( + string workflowName, string workdir, Glob destinationFiles, string changeToUpdate) => + throw new Exceptions.ValidationException( + "update change not implemented for this destination"); + + /// Detect regen baseline when not supplied by CLI. + string? InferRegenBaseline() => null; + + /// Detect regen target when not supplied by CLI. + string? InferRegenTarget() => null; + + /// Detect import baseline when not supplied by CLI. + string? InferImportBaseline(string regenTarget, string workdir) => null; + } +} + +/// +/// This class represents the status of the destination. It includes the baseline revision and, if +/// it is a code review destination, the list of pending changes that have been already migrated. +/// In order: first change is the oldest one. +/// +/// Port of the nested Destination.DestinationStatus Java type (kept at top level here +/// as it does not depend on the destination's revision type). +/// +public sealed class DestinationStatus +{ + private readonly string _baseline; + private readonly ImmutableArray _pendingChanges; + + public DestinationStatus(string baseline, IReadOnlyList pendingChanges) + { + _baseline = Preconditions.CheckNotNull(baseline); + _pendingChanges = Preconditions.CheckNotNull(pendingChanges).ToImmutableArray(); + } + + /// String representation of the latest migrated revision in the baseline. + public string GetBaseline() => + Preconditions.CheckNotNull(_baseline, "Trying to get baseline for NO_STATUS"); + + /// + /// String representation of the migrated revisions that are in pending state in the destination. + /// First element is the oldest one. Last element the newest one. + /// + public IReadOnlyList GetPendingChanges() => _pendingChanges; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var that = (DestinationStatus)o; + return string.Equals(_baseline, that._baseline) + && _pendingChanges.SequenceEqual(that._pendingChanges); + } + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(_baseline); + foreach (var c in _pendingChanges) + { + hash.Add(c); + } + return hash.ToHashCode(); + } + + public override string ToString() => + $"DestinationStatus{{baseline={_baseline}, pendingChanges=[{string.Join(", ", _pendingChanges)}]}}"; +} diff --git a/src/Copybara.Core/DestinationInfo.cs b/src/Copybara.Core/DestinationInfo.cs new file mode 100644 index 000000000..f3b903981 --- /dev/null +++ b/src/Copybara.Core/DestinationInfo.cs @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara; + +/// +/// An object that is used by destinations to store data that consists of complex values. +/// +/// Normally, values could be passed to destinations using labels, but the values can only be +/// strings. +/// +public interface IDestinationInfo +{ +} diff --git a/src/Copybara.Core/DestinationReader.cs b/src/Copybara.Core/DestinationReader.cs new file mode 100644 index 000000000..1f52f57fe --- /dev/null +++ b/src/Copybara.Core/DestinationReader.cs @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara; + +/// An api handle to read files from the destination, rather than just the origin. +[StarlarkBuiltin("destination_reader", Doc = "Handle to read from the destination")] +public abstract class DestinationReader : IStarlarkValue +{ + public static readonly DestinationReader NotImplemented = new NotImplementedReader(); + + public static readonly DestinationReader NoopDestinationReader = new NoopReader(); + + [StarlarkMethod("read_file", Doc = "Read a file from the destination.")] + public abstract string ReadFile( + [Param(Name = "path", Named = true, Doc = "Path to the file.")] string path); + + [StarlarkMethod( + "copy_destination_files", + Doc = "Copy files from the destination into the workdir.")] + // TODO(joshgoldman): refactor this out in favor of directory-specific version + public abstract void CopyDestinationFiles( + [Param( + Name = "glob", + Named = true, + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) }, + Doc = "Files to copy to the workdir, potentially overwriting files checked out from the" + + " origin.")] + object glob, + [Param( + Name = "path", + Named = true, + Doc = "Optional path to copy the files to", + AllowedTypes = new[] { typeof(CheckoutPath), typeof(NoneType) }, + DefaultValue = "None")] + object path); + + /// + /// Similar to but specifies a destination directory (instead + /// of using the default working directory workdir). + /// + public abstract void CopyDestinationFilesToDirectory(Glob glob, string directory); + + [StarlarkMethod( + "file_exists", + Doc = "Checks whether a given file exists in the destination.")] + public abstract bool Exists( + [Param(Name = "path", Named = true, Doc = "Path to the file.")] string path); + + /// Fetch the destination version at which this file was last modified. + public virtual string? LastModified(string path) => + throw new NotSupportedException( + "Last modified is not implemented in this destination reader."); + + /// + /// Returns true if this implementation supports . + /// + /// If this returns false, hashes will be computed by reading the files from the local + /// filesystem instead. An implementation should only provide getHash() if it can compute the + /// hashes in a more efficient way. + /// + public virtual bool SupportsGetHash() => false; + + /// Obtain the hash of the destination file at this path. + public virtual string GetHash(string path) => + throw new NotSupportedException("Get hash is not implemented in this destination reader."); + + private sealed class NotImplementedReader : DestinationReader + { + public override string ReadFile(string path) => + throw new RepoException("Reading files is not implemented by this destination"); + + public override void CopyDestinationFiles(object glob, object path) => + throw new RepoException("Reading files is not implemented by this destination"); + + public override void CopyDestinationFilesToDirectory(Glob glob, string directory) => + throw new RepoException("Reading files is not implemented by this destination"); + + public override bool Exists(string path) => false; + } + + private sealed class NoopReader : DestinationReader + { + public override string ReadFile(string path) => ""; + + public override void CopyDestinationFiles(object glob, object path) { } + + public override void CopyDestinationFilesToDirectory(Glob glob, string directory) { } + + public override bool Exists(string path) => false; + } +} diff --git a/src/Copybara.Core/DestinationStatusVisitor.cs b/src/Copybara.Core/DestinationStatusVisitor.cs new file mode 100644 index 000000000..323b63057 --- /dev/null +++ b/src/Copybara.Core/DestinationStatusVisitor.cs @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Revision; +using Copybara.Util; + +namespace Copybara; + +/// +/// A visitor that computes the matching the actual files affected by +/// the changes with the destination files glob. +/// +public class DestinationStatusVisitor : IChangesVisitor +{ + private readonly IPathMatcher _pathMatcher; + private readonly string _labelName; + + private DestinationStatus? _destinationStatus; + + public DestinationStatusVisitor(IPathMatcher pathMatcher, string labelName) + { + _pathMatcher = pathMatcher; + _labelName = labelName; + } + + public VisitResult Visit(Change change) + { + var changeFiles = change.GetChangeFiles(); + if (changeFiles != null) + { + if (change.GetLabels().ContainsKey(_labelName)) + { + foreach (var file in changeFiles) + { + if (_pathMatcher.Matches("/" + file)) + { + var values = change.GetLabels().Get(_labelName); + string lastRev = values[values.Length - 1]; + _destinationStatus = + new DestinationStatus(lastRev, ImmutableArray.Empty); + return VisitResult.Terminate; + } + } + } + } + return VisitResult.Continue; + } + + public DestinationStatus? GetDestinationStatus() => _destinationStatus; +} diff --git a/src/Copybara.Core/Effect/DestinationEffect.cs b/src/Copybara.Core/Effect/DestinationEffect.cs new file mode 100644 index 000000000..1deb8717c --- /dev/null +++ b/src/Copybara.Core/Effect/DestinationEffect.cs @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Revision; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Effect; + +/// An effect happening in the destination as a consequence of the migration. +[StarlarkBuiltin( + "destination_effect", + Doc = "Represents an effect that happened in the destination due to a single migration")] +public class DestinationEffect : IStarlarkPrintableValue +{ + private readonly EffectType _type; + private readonly string _summary; + private readonly ImmutableArray _originRefs; + private readonly DestinationRef? _destinationRef; + private readonly ImmutableArray _errors; + + public DestinationEffect( + EffectType type, + string summary, + IEnumerable originRefs, + DestinationRef? destinationRef) + : this(type, summary, originRefs, destinationRef, ImmutableArray.Empty) + { + } + + public DestinationEffect( + EffectType type, + string summary, + IEnumerable originRefs, + DestinationRef? destinationRef, + IEnumerable errors) + { + _type = type; + _summary = Preconditions.CheckNotNull(summary); + _originRefs = Preconditions.CheckNotNull(originRefs).ToImmutableArray(); + _destinationRef = destinationRef; + _errors = Preconditions.CheckNotNull(errors).ToImmutableArray(); + } + + /// Returns the origin references included in this effect. + public IReadOnlyList OriginRefs => _originRefs; + + [StarlarkMethod( + "origin_refs", + Doc = "List of origin changes that were included in this migration", + StructField = true)] + public StarlarkList GetOriginRefsSkylark() => StarlarkList.ImmutableCopyOf(_originRefs); + + /// Return the type of effect that happened: Create, updated, noop or error. + public EffectType Type => _type; + + [StarlarkMethod( + "type", + Doc = + "Return the type of effect that happened: CREATED, UPDATED, NOOP," + + " INSUFFICIENT_APPROVALS or ERROR", + StructField = true)] + public string GetTypeSkylark() => _type.ToString(); + + /// + /// Textual summary of what happened. Users of this class should not try to parse this field. + /// + [StarlarkMethod( + "summary", + Doc = + "Textual summary of what happened. Users of this class should not try to parse this" + + " field.", + StructField = true)] + public string Summary => _summary; + + /// + /// Destination reference updated/created. Might be null if there was no effect. Might be set even + /// if the type is error (for example a synchronous presubmit test failed but a review was + /// created). + /// + [StarlarkMethod( + "destination_ref", + Doc = + "Destination reference updated/created. Might be null if there was no effect. Might be" + + " set even if the type is error (For example a synchronous presubmit test failed" + + " but a review was created).", + StructField = true, + AllowReturnNones = true)] + public DestinationRef? GetDestinationRef() => _destinationRef; + + /// + /// List of errors that happened during the write to the destination. This can be used for + /// example for synchronous presubmit failures. + /// + public IReadOnlyList Errors => _errors; + + [StarlarkMethod( + "errors", + Doc = "List of errors that happened during the migration", + StructField = true)] + public StarlarkList GetErrorsSkylark() => StarlarkList.ImmutableCopyOf(_errors.Cast()); + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var that = (DestinationEffect)o; + return _type == that._type + && string.Equals(_summary, that._summary) + && _originRefs.SequenceEqual(that._originRefs) + && Equals(_destinationRef, that._destinationRef) + && _errors.SequenceEqual(that._errors); + } + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"DestinationEffect{{type={_type}, summary={_summary}, originRefs=[{string.Join(", ", _originRefs)}]" + + $", destinationRef={_destinationRef?.ToString() ?? "null"}, errors=[{string.Join(", ", _errors)}]}}"; + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(_type); + hash.Add(_summary); + foreach (var r in _originRefs) + { + hash.Add(r); + } + hash.Add(_destinationRef); + foreach (var e in _errors) + { + hash.Add(e); + } + return hash.ToHashCode(); + } + + /// Type of effect on the destination. + public enum EffectType + { + /// A new review or change was created. + CREATED, + + /// An existing review or change was updated. + UPDATED, + + /// The change was a noop, relative to the destination's baseline. + NOOP, + + /// The change was a noop, relative to an existing pending change in the destination. + NOOP_AGAINST_PENDING_CHANGE, + + /// The effect couldn't happen because the change doesn't have enough approvals. + INSUFFICIENT_APPROVALS, + + /// + /// A user attributable error happened that prevented the destination from creating/updating + /// the change. + /// + ERROR, + + /// + /// An error not attributable to the user that could be retried (RepoException, IOException...). + /// + TEMPORARY_ERROR, + + /// + /// A starting effect of a migration that is eventually expected to trigger another migration + /// asynchronously. This allows to have 'dependant' migrations defined by users. An example of + /// this: a workflow migrates code from a Gerrit review to a GitHub PR, and a feedback + /// migration migrates the test results from a CI in GitHub back to the Gerrit change. This + /// effect would be created on the former one. + /// + STARTED, + } + + /// Reference to the change/review created/updated on the destination. + [StarlarkBuiltin( + "destination_ref", + Doc = "Reference to the change/review created/updated on the destination.")] + public class DestinationRef : IStarlarkPrintableValue + { + private readonly string? _url; + private readonly string _id; + private readonly string _type; + + public DestinationRef(string id, string type, string? url) + { + _id = Preconditions.CheckNotNull(id); + _type = Preconditions.CheckNotNull(type); + _url = url; + } + + /// Destination reference id. + [StarlarkMethod("id", Doc = "Destination reference id", StructField = true)] + public string Id => _id; + + /// + /// Type of reference created. Each destination defines its own and guarantees to be more + /// stable than urls/ids. + /// + [StarlarkMethod( + "type", + Doc = + "Type of reference created. Each destination defines its own and guarantees to be" + + " more stable than urls/ids", + StructField = true)] + public string Type => _type; + + /// Url, if any, of the destination change. + [StarlarkMethod( + "url", + Doc = "Url, if any, of the destination change", + StructField = true, + AllowReturnNones = true)] + public string? Url => _url; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var that = (DestinationRef)o; + return string.Equals(_url, that._url) + && string.Equals(_id, that._id) + && string.Equals(_type, that._type); + } + + public override int GetHashCode() => HashCode.Combine(_url, _id, _type); + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"DestinationRef{{url={_url ?? "null"}, id={_id}, type={_type}}}"; + } +} diff --git a/src/Copybara.Core/Endpoint.cs b/src/Copybara.Core/Endpoint.cs new file mode 100644 index 000000000..664575e7e --- /dev/null +++ b/src/Copybara.Core/Endpoint.cs @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Effect; +using Copybara.Revision; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// +/// An origin or destination API in a feedback migration. +/// +/// Endpoints are symmetric, that is, they need to be able to act both as an origin and +/// destination of a feedback migration, which means that they need to support both read and write +/// operations on the API. +/// +[StarlarkBuiltin("endpoint", Doc = "An origin or destination API in a feedback migration.")] +public interface IEndpoint : IStarlarkPrintableValue, IConfigItemDescription +{ + /// + /// To be used for core.workflow origin/destinations that don't want to provide an api for giving + /// feedback. + /// + public static readonly IEndpoint NoopEndpoint = new NoopEndpointImpl(); + + void IStarlarkPrintableValue.Repr(Printer printer, StarlarkSemantics semantics) => + printer.Append(ToString() ?? ""); + + /// Returns a key-value list of the options the endpoint was instantiated with. + ImmutableListMultimap Describe(); + + [StarlarkMethod( + "new_origin_ref", + Doc = "Creates a new origin reference out of this endpoint.")] + OriginRef NewOriginRef([Param(Name = "ref", Named = true, Doc = "The reference.")] string @ref) => + new(@ref); + + [StarlarkMethod( + "new_destination_ref", + Doc = "Creates a new destination reference out of this endpoint.")] + DestinationEffect.DestinationRef NewDestinationRef( + [Param(Name = "ref", Named = true, Doc = "The reference.")] string @ref, + [Param(Name = "type", Named = true, Doc = "The type of this reference.")] string type, + [Param( + Name = "url", + Named = true, + Doc = "The url associated with this reference, if any.", + DefaultValue = "None")] + object? urlObj) + { + string? url = StarlarkRt.IsNullOrNone(urlObj) ? null : (string?)urlObj; + return new DestinationEffect.DestinationRef(@ref, type, url); + } + + [StarlarkMethod( + "url", + Doc = "Return the URL of this endpoint.", + StructField = true, + AllowReturnNones = true)] + string? GetUrl() => null; + + /// Returns an instance of this endpoint with the given console. + IEndpoint WithConsole(Console console) => this; + + private sealed class NoopEndpointImpl : IEndpoint + { + public ImmutableListMultimap Describe() => + throw new InvalidOperationException("Instance shouldn't be used for core.feedback"); + + void IStarlarkPrintableValue.Repr(Printer printer, StarlarkSemantics semantics) => + printer.Append("noop_endpoint"); + } +} diff --git a/src/Copybara.Core/EndpointProvider.cs b/src/Copybara.Core/EndpointProvider.cs new file mode 100644 index 000000000..72c44d266 --- /dev/null +++ b/src/Copybara.Core/EndpointProvider.cs @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara; + +/// +/// Non-generic view over . Since callers frequently hold an +/// endpoint provider without knowing its concrete endpoint type (Java uses the raw type), this +/// interface exposes the endpoint access that doesn't depend on T. +/// +public interface IEndpointProvider : IStarlarkValue +{ + /// Returns the wrapped endpoint. + IEndpoint GetEndpoint(); + + // TODO(b/269526710): Remove method + ImmutableListMultimap Describe(); +} + +/// Wrapper class to prevent arbitrary instantiation of endpoints in starlark. +[StarlarkBuiltin( + "endpoint_provider", + Doc = "An handle for an origin or destination API in a feedback migration.", + Documented = false)] +public class EndpointProvider : IEndpointProvider + where T : IEndpoint +{ + internal readonly T Endpoint; + + internal EndpointProvider(T endpoint) => Endpoint = endpoint; + + public T GetEndpoint() => Endpoint; + + IEndpoint IEndpointProvider.GetEndpoint() => Endpoint; + + // TODO(b/269526710): Remove method + public ImmutableListMultimap Describe() => Endpoint.Describe(); + + /// Wrap an Endpoint. + public static EndpointProvider Wrap(T e) => new(e); +} + +/// Non-generic factory helpers for . +public static class EndpointProvider +{ + public static EndpointProvider Wrap(T e) + where T : IEndpoint => new(e); +} diff --git a/src/Copybara.Core/FlagAttribute.cs b/src/Copybara.Core/FlagAttribute.cs new file mode 100644 index 000000000..f9833ba42 --- /dev/null +++ b/src/Copybara.Core/FlagAttribute.cs @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara; + +/// +/// Lightweight replacement for JCommander's @Parameter annotation. Marks a property (or +/// field) as a command-line flag, recording the flag names and a description that form the CLI +/// surface. A fully featured parser will consume this metadata later; for now it primarily +/// preserves the flag names/defaults exactly as declared upstream. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public sealed class FlagAttribute : Attribute +{ + /// The flag names, e.g. --verbose, -v. + public string[] Names { get; } + + /// Human-readable description shown in help output. + public string Description { get; } + + /// Whether the flag is hidden from help output. + public bool Hidden { get; init; } + + /// Number of values the flag consumes (JCommander's arity). Default -1 (unset). + public int Arity { get; init; } = -1; + + public FlagAttribute(string name, string description) + : this(new[] { name }, description) + { + } + + public FlagAttribute(string[] names, string description) + { + Names = names; + Description = description; + } +} diff --git a/src/Copybara.Core/Folder/FolderDestination.cs b/src/Copybara.Core/Folder/FolderDestination.cs new file mode 100644 index 000000000..e2d1819fe --- /dev/null +++ b/src/Copybara.Core/Folder/FolderDestination.cs @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Effect; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Util; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Folder; + +/// +/// Writes the output tree to a local destination. Any file that is not excluded in the configuration +/// gets deleted before writing the new files. +/// +public class FolderDestination : IDestination +{ + private static readonly ILogger Logger = NullLogger.Instance; + + private const string FolderDestinationName = "folder.destination"; + + private static readonly string HistoryNotSupported = + $"History not supported in {FolderDestinationName}. Consider passing a ref as an argument, " + + "or using --last-rev."; + + private readonly GeneralOptions _generalOptions; + private readonly FolderDestinationOptions _folderDestinationOptions; + + internal FolderDestination( + GeneralOptions generalOptions, FolderDestinationOptions folderDestinationOptions) + { + _generalOptions = Preconditions.CheckNotNull(generalOptions); + _folderDestinationOptions = Preconditions.CheckNotNull(folderDestinationOptions); + } + + public IDestination.IWriter NewWriter(WriterContext writerContext) + { + if (writerContext.IsDryRun()) + { + _generalOptions.GetConsole().Warn( + "--dry-run does not have any effect for folder.destination"); + } + return new WriterImpl(this); + } + + private sealed class WriterImpl : IDestination.IWriter + { + private readonly FolderDestination _destination; + + public WriterImpl(FolderDestination destination) => _destination = destination; + + public DestinationStatus? GetDestinationStatus(Glob destinationFiles, string labelName) => + throw new ValidationException(HistoryNotSupported); + + public void VisitChanges(IRevision? start, IChangesVisitor visitor) => + throw new ValidationException(HistoryNotSupported); + + public DestinationReader GetDestinationReader( + Console console, Origin.Baseline? baseline, string workdir) => + GetDestinationReader(console, (string?)"", workdir); + + public DestinationReader GetDestinationReader( + Console console, string? baseline, string workdir) + { + try + { + return new FolderDestinationReader(_destination.GetFolderPath(console), workdir); + } + catch (IOException e) + { + throw new RepoException("Failed to initialize destination reader.", e); + } + } + + public bool SupportsHistory() => false; + + public IReadOnlyList Write( + TransformResult transformResult, Glob destinationFiles, Console console) + { + string localFolder = _destination.GetFolderPath(console); + return WriteToFolder(transformResult, destinationFiles, console, localFolder); + } + } + + public static IReadOnlyList WriteToFolder( + TransformResult transformResult, Glob destinationFiles, Console console, string localFolder) + { + console.Progress("FolderDestination: creating " + localFolder); + bool exists = Directory.Exists(localFolder) || File.Exists(localFolder); + try + { + if (File.Exists(localFolder) && !Directory.Exists(localFolder)) + { + // Mirrors Java Files.createDirectories throwing FileAlreadyExistsException when a + // non-directory already exists at the path. + throw new RepoException( + $"Cannot create '{localFolder}' because '{localFolder}' already exists and is " + + "not a directory"); + } + Directory.CreateDirectory(localFolder); + } + catch (UnauthorizedAccessException e) + { + throw new ValidationException("Path is not accessible: " + localFolder, e); + } + catch (IOException e) + when (e.Message.Contains("Read-only file system") + || e.Message.Contains("Operation not permitted")) + { + throw new ValidationException("Path is not accessible: " + localFolder, e); + } + + console.Progress("FolderDestination: Deleting destination files in " + localFolder); + int numDeletedFiles = FileUtil.DeleteFilesRecursively(localFolder, destinationFiles); + console.Info( + $"FolderDestination: Deleted {numDeletedFiles} existing destination files in {localFolder}"); + + console.Progress("FolderDestination: Copying contents of the workdir to " + localFolder); + FileUtil.CopyFilesRecursively( + transformResult.GetPath(), + localFolder, + FileUtil.CopySymlinkStrategy.FailOutsideSymlinks); + return ImmutableArray.Create( + new DestinationEffect( + exists ? DestinationEffect.EffectType.UPDATED : DestinationEffect.EffectType.CREATED, + $"Folder '{localFolder}' contains the output files of the migration", + transformResult.GetChanges().GetCurrent().Cast().ToList(), + new DestinationEffect.DestinationRef(localFolder, "local_folder", localFolder))); + } + + private string GetFolderPath(Console console) + { + string? localFolderOption = _folderDestinationOptions.LocalFolder; + string localFolder; + if (string.IsNullOrEmpty(localFolderOption)) + { + localFolder = _generalOptions.GetDirFactory().NewTempDir("folder-destination"); + string msg = + "Using folder in default root (--folder-dir to override): " + + Path.GetFullPath(localFolder); + Logger.LogInformation("{Message}", msg); + console.Info(msg); + } + else + { + // Lets assume we are in the same filesystem for now... + localFolder = localFolderOption; + if (!Path.IsPathRooted(localFolder)) + { + localFolder = Path.Combine(_generalOptions.GetCwd(), localFolder); + } + } + + // Normalize for console and other stuff that might require normalized paths + return Path.GetFullPath(localFolder); + } + + public string GetLabelNameWhenOrigin() => + throw new ValidationException(FolderDestinationName + " does not support labels"); + + public string GetTypeName() => "folder.destination"; + + public ImmutableListMultimap Describe(Glob? originFiles) + { + var builder = ImmutableListMultimap.CreateBuilder(); + builder.Put("type", GetTypeName()); + return builder.Build(); + } +} diff --git a/src/Copybara.Core/Folder/FolderDestinationOptions.cs b/src/Copybara.Core/Folder/FolderDestinationOptions.cs new file mode 100644 index 000000000..1114cd406 --- /dev/null +++ b/src/Copybara.Core/Folder/FolderDestinationOptions.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Folder; + +/// Arguments for FolderDestination. +public sealed class FolderDestinationOptions : IOption +{ + [Flag( + "--folder-dir", + "Local directory to write the output of the migration to. If the directory " + + "exists, all files will be deleted. By default Copybara will generate a temporary " + + "directory, so you shouldn't need this.")] + public string? LocalFolder { get; set; } +} diff --git a/src/Copybara.Core/Folder/FolderDestinationReader.cs b/src/Copybara.Core/Folder/FolderDestinationReader.cs new file mode 100644 index 000000000..dac6db02f --- /dev/null +++ b/src/Copybara.Core/Folder/FolderDestinationReader.cs @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Copybara.Util; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Folder; + +/// A for reading files from a . +public class FolderDestinationReader : DestinationReader +{ + private readonly string _folderPath; + private readonly string _workDir; + + public FolderDestinationReader(string folderPath, string workDir) + { + _folderPath = folderPath; + _workDir = workDir; + } + + public override string ReadFile(string path) + { + try + { + return File.ReadAllText(Path.Combine(_folderPath, path)); + } + catch (IOException e) + { + throw new RepoException($"Unable to read file {path}.", e); + } + } + + public override void CopyDestinationFiles(object glob, object path) + { + CheckoutPath? checkoutPath = + ReferenceEquals(path, StarlarkRt.None) || path is null ? null : (CheckoutPath)path; + Glob resolvedGlob = Glob.WrapGlob(glob, null)!; + if (checkoutPath == null) + { + CopyDestinationFilesToDirectory(resolvedGlob, _workDir); + } + else + { + CopyDestinationFilesToDirectory( + resolvedGlob, + PathOps.Resolve(checkoutPath.GetCheckoutDir(), checkoutPath.GetPath())); + } + } + + public override void CopyDestinationFilesToDirectory(Glob glob, string directory) + { + IPathMatcher pathMatcher = glob.RelativeTo(_folderPath); + + foreach (var root in glob.Roots()) + { + string rootPath = string.IsNullOrEmpty(root) ? _folderPath : Path.Combine(_folderPath, root); + if (!Directory.Exists(rootPath)) + { + continue; + } + try + { + foreach (var sourcePath in Directory.EnumerateFiles( + rootPath, "*", SearchOption.AllDirectories)) + { + string fullSource = Path.GetFullPath(sourcePath); + if (!pathMatcher.Matches(fullSource)) + { + continue; + } + string relative = Path.GetRelativePath(_folderPath, fullSource); + string targetPath = Path.Combine(directory, relative); + string? parent = Path.GetDirectoryName(targetPath); + if (parent != null) + { + Directory.CreateDirectory(parent); + } + File.Copy(fullSource, targetPath); + } + } + catch (IOException e) + { + throw new RepoException($"Failed to copy files from {_folderPath}.", e); + } + } + } + + public override bool Exists(string path) => + File.Exists(Path.Combine(_folderPath, path)) || Directory.Exists(Path.Combine(_folderPath, path)); +} diff --git a/src/Copybara.Core/Folder/FolderModule.cs b/src/Copybara.Core/Folder/FolderModule.cs new file mode 100644 index 000000000..f57e9f75a --- /dev/null +++ b/src/Copybara.Core/Folder/FolderModule.cs @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Folder; + +/// Main module that groups all the functions related to folders. +[StarlarkBuiltin("folder", Doc = "Module for dealing with local filesystem folders")] +public class FolderModule : IStarlarkValue +{ + private const string DestinationVar = "destination"; + + private readonly FolderOriginOptions _originOptions; + private readonly FolderDestinationOptions _destinationOptions; + private readonly GeneralOptions _generalOptions; + + public FolderModule( + FolderOriginOptions originOptions, + FolderDestinationOptions destinationOptions, + GeneralOptions generalOptions) + { + _originOptions = Preconditions.CheckNotNull(originOptions); + _destinationOptions = Preconditions.CheckNotNull(destinationOptions); + _generalOptions = Preconditions.CheckNotNull(generalOptions); + } + + [StarlarkMethod( + DestinationVar, + Doc = + "A folder destination is a destination that puts the output in a folder. It can be used" + + " both for testing or real production migrations." + + "Given that folder destination does not support a lot of the features of real VCS, " + + "there are some limitations on how to use it:" + + "
    " + + "
  • It requires passing a ref as an argument, as there is no way of calculating " + + "previous migrated changes. Alternatively, --last-rev can be used, which could migrate " + + "N changes." + + "
  • Most likely, the workflow should use 'SQUASH' mode, as history is not supported." + + "
  • If 'ITERATIVE' mode is used, a new temp directory will be created for each change " + + "migrated." + + "
")] + public FolderDestination Destination() => + new(_generalOptions, _destinationOptions); + + [StarlarkMethod( + "origin", + Doc = + "A folder origin is a origin that uses a folder as input. The folder is specified via " + + "the source_ref argument.")] + public FolderOrigin Origin( + [Param( + Name = "materialize_outside_symlinks", + Doc = "DEPRECATED - equivalent to outside_symlinks_mode='MATERIALIZE'", + DefaultValue = "None", + AllowedTypes = new[] { typeof(bool), typeof(NoneType) }, + Named = true)] + object materializeOutsideSymlinks, + [Param( + Name = "inside_symlinks_mode", + Doc = + "How to handle symlinks pointing inside the origin folder. Possible values:" + + " 'COPY_AS_IS' (copy the symlink as-is), 'MATERIALIZE' (copy the content of" + + " the target instead of the symlink), 'IGNORE' (ignore the symlink), 'FAIL'" + + " (fail the operation). Defaults to 'COPY_AS_IS'.", + DefaultValue = "None", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Named = true)] + object insideSymlinksMode, + [Param( + Name = "outside_symlinks_mode", + Doc = + "How to handle symlinks pointing outside the origin folder. See" + + " inside_symlinks_mode for possible values. Defaults to 'FAIL'.", + DefaultValue = "None", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Named = true)] + object outsideSymlinksMode, + [Param( + Name = "broken_symlinks_mode", + Doc = + "How to handle broken symlinks. See inside_symlinks_mode for possible values" + + " (except 'MATERIALIZE', which is invalid for broken symlinks). Defaults to" + + " 'FAIL'.", + DefaultValue = "None", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Named = true)] + object brokenSymlinksMode) + { + bool materializeOutsideSymlinksSet = !ReferenceEquals(materializeOutsideSymlinks, StarlarkRt.None); + bool ignoreInvalidSymlinksSet = _originOptions.IgnoreInvalidSymlinks != null; + bool modernSymlinkOptionsSet = + !ReferenceEquals(insideSymlinksMode, StarlarkRt.None) + || !ReferenceEquals(outsideSymlinksMode, StarlarkRt.None) + || !ReferenceEquals(brokenSymlinksMode, StarlarkRt.None); + + if (materializeOutsideSymlinksSet) + { + _generalOptions + .GetConsole() + .Warn( + "folder.origin(materialize_outside_symlinks = ...) is deprecated. Use" + + " outside_symlinks_mode instead."); + } + if (ignoreInvalidSymlinksSet) + { + _generalOptions + .GetConsole() + .Warn( + "--folder-origin-ignore-invalid-symlinks is deprecated. Use" + + " folder.origin(outside_symlinks_mode = ..., broken_symlinks_mode = ...)" + + " instead."); + } + if (modernSymlinkOptionsSet && (materializeOutsideSymlinksSet || ignoreInvalidSymlinksSet)) + { + throw StarlarkRt.Errorf( + "Cannot mix deprecated symlink configuration ('materialize_outside_symlinks' Starlark" + + " parameter or '--folder-origin-ignore-invalid-symlinks' CLI flag) with new" + + " symlink mode parameters ('inside_symlinks_mode', 'outside_symlinks_mode'," + + " 'broken_symlinks_mode')"); + } + + FileUtil.CopySymlinkStrategy symlinkStrategy; + if (modernSymlinkOptionsSet) + { + try + { + FileUtil.SymlinkMode inside = + ReferenceEquals(insideSymlinksMode, StarlarkRt.None) + ? FileUtil.SymlinkMode.CopyAsIs + : ParseSymlinkMode((string)insideSymlinksMode); + FileUtil.SymlinkMode outside = + ReferenceEquals(outsideSymlinksMode, StarlarkRt.None) + ? FileUtil.SymlinkMode.Fail + : ParseSymlinkMode((string)outsideSymlinksMode); + FileUtil.SymlinkMode broken = + ReferenceEquals(brokenSymlinksMode, StarlarkRt.None) + ? FileUtil.SymlinkMode.Fail + : ParseSymlinkMode((string)brokenSymlinksMode); + symlinkStrategy = new FileUtil.CopySymlinkStrategy(inside, outside, broken); + } + catch (ArgumentException e) + { + throw StarlarkRt.Errorf("Invalid symlink configuration: {0}", e.Message); + } + } + else + { + bool materializeOutside = + materializeOutsideSymlinksSet && (bool)materializeOutsideSymlinks; + bool ignoreInvalid = + ignoreInvalidSymlinksSet && _originOptions.IgnoreInvalidSymlinks!.Value; + symlinkStrategy = + new FileUtil.CopySymlinkStrategy( + inside: FileUtil.SymlinkMode.CopyAsIs, + outside: ignoreInvalid + ? FileUtil.SymlinkMode.Ignore + : (materializeOutside + ? FileUtil.SymlinkMode.Materialize + : FileUtil.SymlinkMode.Fail), + broken: ignoreInvalid ? FileUtil.SymlinkMode.Ignore : FileUtil.SymlinkMode.Fail); + } + + string fs = _generalOptions.GetFileSystem(); + return new FolderOrigin( + fs, + Author.Parse(_originOptions.Author), + _originOptions.Message, + _generalOptions.GetCwd(), + symlinkStrategy, + _originOptions.Version); + } + + /// + /// Parses the Starlark symlink-mode strings ('COPY_AS_IS', 'MATERIALIZE', 'IGNORE', 'FAIL') into + /// the C# enum (whose members are PascalCase). + /// + private static FileUtil.SymlinkMode ParseSymlinkMode(string value) => + value switch + { + "COPY_AS_IS" => FileUtil.SymlinkMode.CopyAsIs, + "MATERIALIZE" => FileUtil.SymlinkMode.Materialize, + "IGNORE" => FileUtil.SymlinkMode.Ignore, + "FAIL" => FileUtil.SymlinkMode.Fail, + _ => throw new ArgumentException($"No enum constant for symlink mode: {value}"), + }; +} diff --git a/src/Copybara.Core/Folder/FolderOrigin.cs b/src/Copybara.Core/Folder/FolderOrigin.cs new file mode 100644 index 000000000..92d75d1bd --- /dev/null +++ b/src/Copybara.Core/Folder/FolderOrigin.cs @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Util; + +namespace Copybara.Folder; + +/// Use a folder as the input for the migration. +public class FolderOrigin : IOrigin +{ + private const string LabelName = "FolderOrigin-RevId"; + + private const UnixFileMode FilePermissions = UnixFileMode.UserRead | UnixFileMode.UserWrite; + + private readonly string _fileSystemRoot; + private readonly Author _author; + private readonly string _message; + private readonly string _cwd; + private readonly FileUtil.CopySymlinkStrategy _copySymlinkStrategy; + private readonly string? _version; + + internal FolderOrigin( + string fileSystemRoot, + Author author, + string message, + string cwd, + FileUtil.CopySymlinkStrategy copySymlinkStrategy, + string? version) + { + _fileSystemRoot = Preconditions.CheckNotNull(fileSystemRoot); + _author = author; + _message = message; + _cwd = Preconditions.CheckNotNull(cwd); + _copySymlinkStrategy = Preconditions.CheckNotNull(copySymlinkStrategy); + _version = version; + } + + public FolderRevision Resolve(string? reference) + { + ValidationException.CheckCondition( + reference != null, + "A path is expected as reference in the command line. Invoke copybara as:\n" + + " copybara copy.bara.sky workflow_name ORIGIN_FOLDER"); + string path = reference!; + if (!Path.IsPathRooted(path)) + { + path = Path.GetFullPath(Path.Combine(_cwd, path)); + } + ValidationException.CheckCondition( + Directory.Exists(path) || File.Exists(path), "%s folder doesn't exist", path); + ValidationException.CheckCondition(Directory.Exists(path), "%s is not a folder", path); + + return new FolderRevision(path, DateTimeOffset.Now, _version); + } + + public IOrigin.IReader NewReader( + Glob originFiles, Authoring.Authoring authoring) => + new ReaderImpl(this, originFiles); + + public string GetLabelName() => LabelName; + + public string GetTypeName() => "folder.origin"; + + public ImmutableListMultimap Describe(Glob? originFiles) + { + var builder = ImmutableListMultimap.CreateBuilder(); + builder.Put("type", GetTypeName()); + return builder.Build(); + } + + private sealed class ReaderImpl : IOrigin.IReader + { + private readonly FolderOrigin _origin; + private readonly Glob _originFiles; + + public ReaderImpl(FolderOrigin origin, Glob originFiles) + { + _origin = origin; + _originFiles = originFiles; + } + + public void Checkout(FolderRevision reference, string checkoutDir) + { + try + { + FileUtil.CopyFilesRecursively( + reference.Path, checkoutDir, _origin._copySymlinkStrategy, _originFiles); + FileUtil.AddPermissionsAllRecursively(checkoutDir, FilePermissions); + } + catch (SymlinkException e) + { + throw new ValidationException( + "Cannot copy files into the workdir: " + e.Message, e); + } + catch (IOException e) + { + throw new RepoException( + "Cannot copy files into the workdir:\n" + + $" origin folder: {reference.Path}\n" + + $" workdir: {checkoutDir}", + e); + } + } + + public Origin.ChangesResponse Changes( + FolderRevision? fromRef, FolderRevision toRef) + { + // Ignore fromRef since a folder doesn't have history of changes + return Origin.ChangesResponse.ForChanges( + ImmutableArray.Create(Change(toRef))); + } + + public bool SupportsHistory() => false; + + public Change Change(FolderRevision reference) => + new( + reference, + _origin._author, + _origin._message, + reference.ReadTimestamp() ?? DateTimeOffset.Now, + ImmutableListMultimap.Empty); + + public void VisitChanges(FolderRevision? start, IChangesVisitor visitor) + { + FolderRevision reference = start!; + var change = new Change( + reference, + _origin._author, + _origin._message, + reference.ReadTimestamp() ?? DateTimeOffset.Now, + ImmutableListMultimap.Empty); + visitor.Visit(change); + } + } +} diff --git a/src/Copybara.Core/Folder/FolderOriginOptions.cs b/src/Copybara.Core/Folder/FolderOriginOptions.cs new file mode 100644 index 000000000..9a30ac44e --- /dev/null +++ b/src/Copybara.Core/Folder/FolderOriginOptions.cs @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Folder; + +/// Arguments for FolderOrigin. +public sealed class FolderOriginOptions : IOption +{ + [Flag( + "--folder-origin-author", + "Deprecated. Please use '--force-author'." + + " Author of the change being migrated from folder.origin()")] + public string Author { get; set; } = "Copybara "; + + [Flag( + "--folder-origin-message", + "Deprecated. Please use '--force-message'. Message of the change being migrated" + + " from folder.origin()")] + public string Message { get; set; } = "Copybara code migration"; + + [Flag( + "--folder-origin-version", + "The version string associated with the change migrated from folder.origin(). If not" + + " specified, the default will be the folder path.")] + public string? Version { get; set; } + + [Flag( + "--folder-origin-ignore-invalid-symlinks", + "DEPRECATED - equivalent to folder.origin(outside_symlinks_mode='IGNORE'," + + " broken_symlinks_mode='IGNORE')", + Arity = 1)] + public bool? IgnoreInvalidSymlinks { get; set; } +} diff --git a/src/Copybara.Core/Folder/FolderRevision.cs b/src/Copybara.Core/Folder/FolderRevision.cs new file mode 100644 index 000000000..59b5e8efb --- /dev/null +++ b/src/Copybara.Core/Folder/FolderRevision.cs @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Revision; + +namespace Copybara.Folder; + +/// A reference for folder origins. +public class FolderRevision : IRevision +{ + internal readonly string Path; + private readonly DateTimeOffset _timestamp; + internal readonly string? Version; + + public FolderRevision(string path, DateTimeOffset timestamp) + { + Preconditions.CheckState(System.IO.Path.IsPathRooted(path), "Path must be absolute"); + Path = path; + _timestamp = timestamp; + Version = null; + } + + internal FolderRevision(string path, DateTimeOffset timestamp, string? version) + { + Preconditions.CheckState(System.IO.Path.IsPathRooted(path), "Path must be absolute"); + Path = path; + _timestamp = timestamp; + Version = version; + } + + public string AsString() => Version ?? Path; + + public DateTimeOffset? ReadTimestamp() => _timestamp; +} diff --git a/src/Copybara.Core/GeneralOptions.cs b/src/Copybara.Core/GeneralOptions.cs new file mode 100644 index 000000000..56babf509 --- /dev/null +++ b/src/Copybara.Core/GeneralOptions.cs @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; +using Copybara.Util.Console; + +// Domain 'Console' collides with System.Console; qualify both. +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// General options available for all the program classes. +public sealed class GeneralOptions : IOption +{ + public const string CliFlagPrefix = "FLAG_"; + + public const string Noansi = "--noansi"; + public const string Noprompt = "--noprompt"; + public const string Force = "--force"; + public const string ConfigRootFlag = "--config-root"; + public const string OutputRootFlag = "--output-root"; + public const string OutputLimitFlag = "--output-limit"; + public const string DryRunFlag = "--dry-run"; + public const string SquashFlag = "--squash"; + public const string PatchBinFlag = "--patch-bin"; + internal const string ConsoleFilePath = "--console-file-path"; + internal const string ConsoleFileFlushInterval = "--console-file-flush-interval"; + + internal static readonly TimeSpan DefaultConsoleFileFlushInterval = TimeSpan.FromSeconds(30); + private const string DefaultMonitor = "default"; + + private IReadOnlyDictionary _environment; + private string _fileSystemRoot; + private Console _console; + + // Registry of available event monitors, keyed by name. Populated lazily on first access because + // the default monitor needs the console. Mirrors Java's HashMap. + private Dictionary? _eventMonitors; + + private string? _configRootPath; + private string? _outputRootPath; + + private Copybara.Profiler.Profiler _profiler = new(Copybara.Profiler.Ticker.SystemTicker); + + public GeneralOptions( + IReadOnlyDictionary environment, + string fileSystemRoot, + Console console) + { + _environment = environment; + _fileSystemRoot = Preconditions.CheckNotNull(fileSystemRoot); + _console = Preconditions.CheckNotNull(console); + } + + public GeneralOptions( + IReadOnlyDictionary environment, + string fileSystemRoot, + bool verbose, + Console console, + string? configRoot, + string? outputRoot, + bool noCleanup, + bool disableReversibleCheck, + bool force, + int outputLimit) + : this(environment, fileSystemRoot, console) + { + Verbose = verbose; + _configRootPath = configRoot; + _outputRootPath = outputRoot; + NoCleanup = noCleanup; + DisableReversibleCheck = disableReversibleCheck; + ForceFlag = force; + OutputLimitValue = outputLimit; + } + + public GeneralOptions WithForce(bool force) + { + return new GeneralOptions( + _environment, _fileSystemRoot, Verbose, _console, GetConfigRoot(), + GetOutputRoot(), NoCleanup, DisableReversibleCheck, force, OutputLimitValue); + } + + public GeneralOptions WithConsole(Console console) + { + return new GeneralOptions( + _environment, _fileSystemRoot, Verbose, console, GetConfigRoot(), + GetOutputRoot(), NoCleanup, DisableReversibleCheck, ForceFlag, OutputLimitValue); + } + + public IReadOnlyDictionary GetEnvironment() => _environment; + + public bool IsVerbose() => Verbose; + + public Console GetConsole() => _console; + + /// The root of the filesystem used to resolve paths (analog of Java's FileSystem). + public string GetFileSystem() => _fileSystemRoot; + + public bool IsNoCleanup() => NoCleanup; + + public bool IsDisableReversibleCheck() => DisableReversibleCheck; + + public bool IsForced() => ForceFlag; + + public bool IsVersionSelectorUseCliRef() => VersionSelectorUseCliRef; + + /// Returns current working directory. + public string GetCwd() + { + return _environment.TryGetValue("PWD", out var pwd) + ? pwd + : Directory.GetCurrentDirectory(); + } + + /// Returns the root absolute path to use for config, or null if not set. + public string? GetConfigRoot() + { + if (_configRootPath == null && ConfigRoot != null) + { + _configRootPath = Path.GetFullPath(ConfigRoot); + ValidationException.CheckCondition( + Directory.Exists(_configRootPath) || File.Exists(_configRootPath), + "%s doesn't exist", ConfigRoot); + ValidationException.CheckCondition( + Directory.Exists(_configRootPath), "%s isn't a directory", ConfigRoot); + } + + return _configRootPath; + } + + /// + /// Returns the output root directory, or null if not set. + /// This method is exposed mainly for tests; prefer . + /// + public string? GetOutputRoot() + { + if (_outputRootPath == null && OutputRoot != null) + { + _outputRootPath = OutputRoot; + } + + return _outputRootPath; + } + + /// + /// Returns the output limit. + /// Each subcommand can use this value differently. + /// + public int GetOutputLimit() => OutputLimitValue > 0 ? OutputLimitValue : int.MaxValue; + + public Copybara.Profiler.Profiler Profiler() => _profiler; + + private Dictionary EventMonitorRegistry() + { + // Default registry contains the "default" monitor: a ConsoleEventMonitor wrapping the empty + // monitor. Mirrors GeneralOptions' constructor in Java. + return _eventMonitors ??= new Dictionary + { + [DefaultMonitor] = + new Copybara.Monitor.ConsoleEventMonitor( + _console, Copybara.Monitor.IEventMonitor.EmptyMonitor), + }; + } + + /// + /// Returns the configured (enabled) event monitors. Mirrors Java's eventMonitors(): it + /// filters the available-monitor registry down to those named in . + /// + public Copybara.Monitor.IEventMonitor.EventMonitors EventMonitors() + { + var registry = EventMonitorRegistry(); + var result = new List(); + foreach (var name in EnabledEventMonitors) + { + if (registry.TryGetValue(name, out var monitor)) + { + result.Add(monitor); + } + } + + return new Copybara.Monitor.IEventMonitor.EventMonitors(result); + } + + /// + /// Adds an EventMonitor to the list of available monitors without activating it. Use this to make + /// a monitor available for later enabling. + /// + public GeneralOptions AddEventMonitor(string name, Copybara.Monitor.IEventMonitor eventMonitor) + { + EventMonitorRegistry()[name] = eventMonitor; + return this; + } + + /// Enables an already-registered EventMonitor by name. + public GeneralOptions EnableEventMonitor(string name) + { + ValidationException.CheckCondition( + EventMonitorRegistry().ContainsKey(name), "%s is not a known EventMonitor.", name); + EnabledEventMonitors.Add(name); + return this; + } + + /// Adds an EventMonitor to the list of available monitors and enables it. + public GeneralOptions EnableEventMonitor(string name, Copybara.Monitor.IEventMonitor eventMonitor) + { + AddEventMonitor(name, eventMonitor); + EnableEventMonitor(name); + return this; + } + + /// Clears the enabled event monitors. + public GeneralOptions ClearEventMonitor() + { + EnabledEventMonitors.Clear(); + return this; + } + + public IReadOnlyDictionary CliLabels() => Labels; + + /// Run a repository task with profiling. + public T RepoTask(string description, Func callable) + { + using (_profiler.Start(description)) + { + return callable(); + } + } + + /// Run a repository task that can throw IOException with profiling. + public T IoRepoTask(string description, Func callable) + { + using (_profiler.Start(description)) + { + return callable(); + } + } + + /// + /// Returns a capable of creating directories in a self contained + /// location in the filesystem. + /// By default, the directories are created under $HOME/copybara, but it can be + /// overridden with the flag --output-root. + /// + public DirFactory GetDirFactory() + { + var outputRoot = GetOutputRoot(); + if (outputRoot != null) + { + return new DirFactory(outputRoot); + } + + var home = _environment.TryGetValue("HOME", out var h) ? h : null; + Preconditions.CheckNotNull(home, "$HOME environment var is not set"); + return new DirFactory(Path.Combine(home!, "copybara")); + } + + public void SetEnvironmentForTest(IReadOnlyDictionary environment) + => _environment = environment; + + public void SetTemporaryFeaturesForTest(IReadOnlyDictionary temporaryFeatures) + => TemporaryFeatures = temporaryFeatures.ToImmutableDictionary(); + + public void SetOutputRootPathForTest(string outputRootPath) => _outputRootPath = outputRootPath; + + public void SetConsoleForTest(Console console) => _console = console; + + public void SetForceForTest(bool force) => ForceFlag = force; + + public void SetVersionSelectorUseCliRefForTest(bool versionSelectorUseCliRef) + => VersionSelectorUseCliRef = versionSelectorUseCliRef; + + public void SetCliLabelsForTest(ImmutableDictionary labels) => Labels = labels; + + public void SetFileSystemForTest(string fileSystemRoot) => _fileSystemRoot = fileSystemRoot; + + public void SetSquashForTest(bool squash) => Squash = squash; + + public GeneralOptions WithProfiler(Copybara.Profiler.Profiler profiler) + { + _profiler = Preconditions.CheckNotNull(profiler); + return this; + } + + // ---- Flags ---- + + [Flag(new[] { "-v", "--verbose" }, "Verbose output.")] + internal bool Verbose { get; set; } + + [Flag("--repo-timeout", "Repository operation timeout duration.")] + public TimeSpan RepoTimeout { get; set; } = CommandRunner.DefaultTimeout; + + [Flag("--commands-timeout", "Commands timeout")] + public TimeSpan CommandsTimeout { get; set; } = CommandRunner.DefaultTimeout; + + public CommandRunner NewCommandRunner(Command cmd) => new(cmd, CommandsTimeout); + + // We don't use JCommander for parsing these flags but we do it manually since + // the parsing could fail and we need to report errors using one console. + [Flag(Noansi, "Don't use ANSI output for messages")] + internal bool Noansi_ { get; set; } + + [Flag(Noprompt, "Don't prompt, this will answer all prompts with 'yes'", Arity = 1)] + internal bool NoPrompt { get; set; } + + [Flag( + new[] { Force, "--force-update" }, + "Force the migration even if Copybara cannot find in the destination a change that is an" + + " ancestor of the one(s) being migrated. This should be used with care, as it" + + " could lose changes when migrating a previous/conflicting change.")] + internal bool ForceFlag { get; set; } + + [Flag( + "--version-selector-use-cli-ref", + "If command line ref is to used with a version selector, pass this flag to tell copybara" + + " to use it.", + Arity = 1)] + internal bool VersionSelectorUseCliRef { get; set; } = true; + + [Flag( + ConfigRootFlag, + "Configuration root path to be used for resolving absolute config labels" + + " like '//foo/bar'")] + internal string? ConfigRoot { get; set; } + + [Flag( + "--disable-reversible-check", + "If set, all workflows will be executed without reversible_check, overriding" + + " the workflow config and the normal behavior for CHANGE_REQUEST mode.")] + internal bool DisableReversibleCheck { get; set; } + + [Flag( + OutputRootFlag, + "The root directory where to generate output files. If not set, ~/copybara/out is used " + + "by default. Use with care, Copybara might remove files inside this root if " + + "necessary.")] + internal string? OutputRoot { get; set; } + + [Flag( + OutputLimitFlag, + "Limit the output in the console to a number of records. Each subcommand might use this " + + "flag differently. Defaults to 0, which shows all the output.")] + internal int OutputLimitValue { get; set; } + + [Flag( + "--nocleanup", + "Cleanup the output directories. This includes the workdir, scratch clones of Git" + + " repos, etc. By default is set to false and directories will be cleaned prior to" + + " the execution. If set to true, the previous run output will not be cleaned up." + + " Keep in mind that running in this mode will lead to an ever increasing disk" + + " usage.")] + internal bool NoCleanup { get; set; } + + [Flag( + "--nologging", + "Disable logging of this binary. Note that commands executed by Copybara " + + "might still log to their own file.", + Hidden = true)] + internal bool NoLogging { get; set; } + + [Flag( + "--labels", + "Additional flags. Can be accessed in feedback and mirror context objects via the" + + " `cli_labels` field. In `core.workflow`, they are accessible as labels, but with" + + " names uppercased and prefixed with " + + CliFlagPrefix + + " to avoid name clashes with existing labels. I.e. `--labels=label1:value1` will" + + " define a label FLAG_LABEL1Format: --labels=flag1:value1,flag2:value2 Or: --labels" + + " flag1:value1,flag2:value2 ")] + internal ImmutableDictionary Labels { get; set; } = + ImmutableDictionary.Empty; + + [Flag( + "--temporary-features", + "Change guarded features. If set it means that it will return true.", + Hidden = true)] + private ImmutableDictionary TemporaryFeatures { get; set; } = + ImmutableDictionary.Empty; + + [Flag( + "--diff-bin", + "Command line diff tool bin used in merge import. Defaults to diff3, but users can pass" + + " in their own diffing tools (along with requisite arg reordering)")] + private string DiffBin { get; set; } = "diff3"; + + public string GetDiffBin() => DiffBin; + + [Flag(PatchBinFlag, "Path for GNU Patch command")] + public string PatchBin { get; set; } = "patch"; + + /// + /// Temporary features is meant to be used by Copybara team for guarding new codepaths. Should + /// never be used for user facing flags or longer term experiments. Any caller of this function + /// should have a todo saying when to remove the call. + /// If the flag doesn't have a value it will use defaultVal. If the flag is incorrect + /// (different from true/false) it will use defaultVal (and log at severe). + /// + public bool IsTemporaryFeature(string name, bool defaultVal) + { + Preconditions.CheckNotNull(name); + if (!TemporaryFeatures.TryGetValue(name, out var v)) + { + return defaultVal; + } + + if (string.Equals(v, "true", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (string.Equals(v, "false", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // logger.atSevere(): Invalid boolean value. Using default. + return defaultVal; + } + + // This flag is read before we parse the arguments, because of the console lifecycle. + [Flag( + ConsoleFilePath, + "If set, write the console output also to the given file path.")] + internal string? ConsoleFilePathValue { get; set; } + + // This flag is read before we parse the arguments, because of the console lifecycle. + [Obsolete] + [Flag( + "--console-file-flush-rate", + "How often in number of lines to flush the console to the output file. " + + "If set to 0, console will be flushed only at the end.", + Hidden = true)] + internal int ConsoleFileFlushRateDeprecatedDontUse { get; set; } = -1; + + // This flag is read before we parse the arguments, because of the console lifecycle. + [Flag( + ConsoleFileFlushInterval, + "How often Copybara should flush the console to the output file. (10s, 1m, etc.)" + + "If set to 0s, console will be flushed only at the end.")] + internal TimeSpan ConsoleFileFlushIntervalValue { get; set; } = DefaultConsoleFileFlushInterval; + + [Flag( + DryRunFlag, + "Run the migration in dry-run mode. Some destination implementations might" + + " have some side effects (like creating a code review), but never submit to a main" + + " branch.")] + public bool DryRunMode { get; set; } + + [Flag( + SquashFlag, + "Override workflow's mode with 'SQUASH'. This is " + + "useful mainly for workflows that use 'ITERATIVE' mode, when we want to run a single " + + "export with 'SQUASH', maybe to fix an issue. Always use " + DryRunFlag + " before, to " + + "test your changes locally.")] + public bool Squash { get; set; } + + [Flag( + "--validate-starlark", + "Starlark should be validated prior to execution, but this might break legacy configs." + + " Options are LOOSE, STRICT")] + public string StarlarkModeFlag { get; set; } = StarlarkMode.Loose.ToString().ToUpperInvariant(); + + public StarlarkMode GetStarlarkMode() => + Enum.Parse(StarlarkModeFlag, ignoreCase: true); + + [Flag( + "--info-list-only", + "When set, the INFO command will print a list of workflows defined in the file.")] + public bool InfoListOnly { get; set; } + + [Flag( + "--info-include-definition", + "When set, the INFO command will include the migrations' definition stack info in the" + + " table or list output. In table, leaves out origin, destination and mode.")] + public bool InfoIncludeDefinition { get; set; } + + [Flag( + "--event-monitor", + "Eventmonitors to enable. These must be in the list of available monitors.")] + public List EnabledEventMonitors { get; set; } = new() { DefaultMonitor }; + + [Flag( + "--allow-empty-diff", + "If set to false, Copybara will not write to the destination if the exact same change is" + + " already pending in the destination. Currently only supported for" + + " `git.github_pr_destination` and `git.gerrit_destination`.", + Arity = 1)] + public bool? AllowEmptyDiff { get; set; } + + public bool AllowEmptyDiffValue(bool configAllowEmptyDiff) + => AllowEmptyDiff ?? configAllowEmptyDiff; +} diff --git a/src/Copybara.Core/IConfigLoaderProvider.cs b/src/Copybara.Core/IConfigLoaderProvider.cs new file mode 100644 index 000000000..e44a1ff30 --- /dev/null +++ b/src/Copybara.Core/IConfigLoaderProvider.cs @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara; + +/// A class that given a main config path (copy.bara.sky file) returns a ConfigLoader. +public interface IConfigLoaderProvider +{ + /// Create a new loader for . + /// + ConfigLoader NewLoader(string configPath, string? sourceRef); +} diff --git a/src/Copybara.Core/Info.cs b/src/Copybara.Core/Info.cs new file mode 100644 index 000000000..b69dc1660 --- /dev/null +++ b/src/Copybara.Core/Info.cs @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Revision; + +namespace Copybara; + +/// +/// Represents the information about a Migration. +/// +/// A migration can have one or more s. +/// +/// the origin revision type. +public sealed class Info + where O : class, IRevision +{ + private Info( + ImmutableListMultimap originDescription, + ImmutableListMultimap destinationDescription, + ImmutableArray> migrationReferences, + ImmutableArray> versions) + { + OriginDescription = originDescription; + DestinationDescription = destinationDescription; + MigrationReferences = migrationReferences; + Versions = versions; + } + + public static Info Create( + ImmutableListMultimap originDescription, + ImmutableListMultimap destinationDescription, + IEnumerable> migrationReferences) => + new( + originDescription, + destinationDescription, + migrationReferences.ToImmutableArray(), + ImmutableArray>.Empty); + + public static Info Create( + ImmutableListMultimap originDescription, + ImmutableListMultimap destinationDescription, + IEnumerable> migrationReferences, + IReadOnlyList> versions) => + new( + originDescription, + destinationDescription, + migrationReferences.ToImmutableArray(), + versions.ToImmutableArray()); + + /// Returns origin description of the migration. + public ImmutableListMultimap OriginDescription { get; } + + /// Returns destination description of the migration. + public ImmutableListMultimap DestinationDescription { get; } + + /// Returns information about a migration for one reference (like 'master'). + public IReadOnlyList> MigrationReferences { get; } + + /// Returns a list of the upstream versions for an origin. + public IReadOnlyList> Versions { get; } +} + +/// Non-generic holder for 's empty instance factory. +public static class Info +{ + public static Info Empty { get; } = + Info.Create( + ImmutableListMultimap.Empty, + ImmutableListMultimap.Empty, + Array.Empty>(), + Array.Empty>()); +} + +/// Information about a migration for one reference (like 'master'). +/// the origin revision type. +public sealed class MigrationReference + where O : class, IRevision +{ + private readonly string _label; + private readonly ImmutableArray> _availableToMigrate; + + private MigrationReference( + string label, + O? lastMigrated, + Change? lastMigratedChange, + ImmutableArray> availableToMigrate, + Change? lastResolvedChange) + { + _label = label; + LastMigrated = lastMigrated; + LastMigratedChange = lastMigratedChange; + _availableToMigrate = availableToMigrate; + LastResolvedChange = lastResolvedChange; + } + + public static MigrationReference Create( + string label, + O? lastMigrated, + Change? lastMigratedChange, + IEnumerable> availableToMigrate) => + new(label, lastMigrated, lastMigratedChange, availableToMigrate.ToImmutableArray(), null); + + public static MigrationReference Create( + string label, + Change? lastMigratedChange, + IEnumerable> availableToMigrate) => + new( + label, + lastMigratedChange?.GetRevision(), + lastMigratedChange, + availableToMigrate.ToImmutableArray(), + null); + + public static MigrationReference Create( + string label, + O? lastMigrated, + Change? lastMigratedChange, + IEnumerable> availableToMigrate, + Change? lastResolvedChange) => + new( + label, + lastMigrated, + lastMigratedChange, + availableToMigrate.ToImmutableArray(), + lastResolvedChange); + + /// The name of this . + public string GetLabel() => _label; + + /// Returns the last migrated revision from the origin, or null if none. + public O? LastMigrated { get; } + + /// Returns the last migrated change from the origin, or null. + public Change? LastMigratedChange { get; } + + /// Returns the last available revision to migrate from the origin, or null. + public O? GetLastAvailableToMigrate() + { + O? last = null; + foreach (var c in _availableToMigrate) + { + last = c.GetRevision(); + } + return last; + } + + /// Returns a list of the next available changes to migrate from the origin. + public IReadOnlyList> GetAvailableToMigrate() => _availableToMigrate; + + /// Returns the most recent change that was resolved by the migration. + public Change? LastResolvedChange { get; } +} diff --git a/src/Copybara.Core/LabelFinder.cs b/src/Copybara.Core/LabelFinder.cs new file mode 100644 index 000000000..155a2bfc3 --- /dev/null +++ b/src/Copybara.Core/LabelFinder.cs @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.TemplateToken; + +namespace Copybara; + +/// +/// A simple line finder/parser for labels like: +/// +/// foo = bar +/// baz : foo +/// +/// +/// In general this class should only be used in Origins to create a labels map. During +/// transformations/destination, it can be used to check if a line is a label but never to find +/// labels. Use instead, since it looks in more places +/// for labels. +/// +public class LabelFinder +{ + private const string ValidLabelExpr = "([\\w-]+)"; + + public static readonly Regex VALID_LABEL = new("^" + ValidLabelExpr + "$", RegexOptions.Compiled); + + private static readonly Regex LabelVar = + new("^\\$\\{(" + ValidLabelExpr + ")}$", RegexOptions.Compiled); + + private static readonly Regex Url = new("^" + ValidLabelExpr + "://.*", RegexOptions.Compiled); + + private static readonly Regex LabelPattern = + new("^" + ValidLabelExpr + "( *[:=] ?)(.*)$", RegexOptions.Compiled | RegexOptions.Singleline); + + private readonly Match _match; + private readonly string _line; + + public LabelFinder(string line) + { + _match = LabelPattern.Match(line); + _line = line; + } + + /// A utility for resolving list of string labels to values. + public static IReadOnlyList MapLabels( + Func?> labelsMapper, IReadOnlyList list) + { + var result = ImmutableArray.CreateBuilder(); + foreach (var element in list) + { + var match = LabelVar.Match(element); + if (!match.Success) + { + result.Add(element); + continue; + } + var label = match.Groups[1].Value; + var values = labelsMapper(label); + if (values == null) + { + continue; + } + result.AddRange(values); + } + return result.ToImmutable(); + } + + public static string MapLabels( + Func?> labelsMapper, string template) => + MapLabels(labelsMapper, template, null); + + public static string MapLabels( + Func?> labelsMapper, string template, string? fieldName) + { + try + { + return new LabelTemplate(template).Resolve(label => + { + var values = labelsMapper(label); + if (values == null) + { + return null; + } + foreach (var v in values) + { + return v; + } + return null; + }); + } + catch (LabelTemplate.LabelNotFoundException e) + { + throw new ValidationException( + $"Cannot find '{e.Label}' label for template '{template}' defined in field '{fieldName}'", + e); + } + } + + public bool IsLabel() + { + // It is a label if it looks like a label but it doesn't look like a url (foo://bar) + return _match.Success && !Url.Match(_line).Success; + } + + public bool IsLabel(string labelName) => IsLabel() && GetName().Equals(labelName); + + /// + /// Returns the name of the label. + /// + /// Use before calling this method. + /// + public string GetName() + { + CheckIsLabel(); + return _match.Groups[1].Value; + } + + /// + /// Returns the separator of the label. + /// + /// Use before calling this method. + /// + public string GetSeparator() + { + CheckIsLabel(); + return _match.Groups[2].Value; + } + + /// + /// Returns the value of the label. + /// + /// Use before calling this method. + /// + public string GetValue() + { + CheckIsLabel(); + return _match.Groups[3].Value; + } + + private void CheckIsLabel() => + Preconditions.CheckState(IsLabel(), "Not a label: '{0}'. Please call isLabel() first", _line); + + public string GetLine() => _line; +} diff --git a/src/Copybara.Core/LazyResourceLoader.cs b/src/Copybara.Core/LazyResourceLoader.cs new file mode 100644 index 000000000..8a5b280b9 --- /dev/null +++ b/src/Copybara.Core/LazyResourceLoader.cs @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +// 'Console' collides with System.Console; qualify the domain Console interface. +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// +/// Load a resource (repository, API client...) lazily to avoid side effects. +/// +public interface LazyResourceLoader where T : class +{ + /// + /// Load the resource. + /// + T Load(Console? console); +} + +/// +/// Factory helpers for . +/// +public static class LazyResourceLoader +{ + /// + /// Constructs a object that defers the loading of the + /// resource until is called and after that always + /// returns the same instance. + /// + public static LazyResourceLoader Memoized(LazyResourceLoader @delegate) where T : class + { + return new MemoizedLoader(@delegate); + } + + /// + /// Constructs a memoized from a delegate function. + /// + public static LazyResourceLoader Memoized(Func loader) where T : class + { + return new MemoizedLoader(new FuncLoader(loader)); + } + + private sealed class FuncLoader : LazyResourceLoader where T : class + { + private readonly Func _loader; + + public FuncLoader(Func loader) => _loader = loader; + + public T Load(Console? console) => _loader(console); + } + + private sealed class MemoizedLoader : LazyResourceLoader where T : class + { + private readonly LazyResourceLoader _delegate; + private T? _resource; + + public MemoizedLoader(LazyResourceLoader @delegate) => _delegate = @delegate; + + public T Load(Console? console) + { + return _resource ??= Preconditions.CheckNotNull(_delegate.Load(console)); + } + } +} diff --git a/src/Copybara.Core/LocalParallelizer.cs b/src/Copybara.Core/LocalParallelizer.cs new file mode 100644 index 000000000..e0d40cc8c --- /dev/null +++ b/src/Copybara.Core/LocalParallelizer.cs @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Exceptions; + +namespace Copybara; + +/// +/// A class that allows to run a list of things in parallel batches. +/// +public class LocalParallelizer +{ + private readonly int _threads; + private readonly int _minSize; + + public LocalParallelizer(int threads, int minSize) + { + _threads = threads; + _minSize = minSize; + Preconditions.CheckState(threads >= 1, "Threads need to be positive"); + Preconditions.CheckState(threads < 1000, "Too many threads (max: 1000)"); + } + + /// + /// Run a list of things in batches, calling for each batch. + /// + public IReadOnlyList Run(IEnumerable list, TransformFunc func) + { + var newList = list as IReadOnlyList ?? list.ToList(); + if (_threads == 1 || newList.Count < _minSize) + { + return ImmutableArray.Create(func.Run(newList)); + } + + var batches = Partition(newList, Math.Max(1, newList.Count / _threads)); + var tasks = new List>(_threads); + foreach (var batch in batches) + { + var localBatch = batch; + tasks.Add(Task.Run(() => func.Run(localBatch))); + } + + try + { + Task.WaitAll(tasks.ToArray()); + } + catch (AggregateException e) + { + var inner = e.Flatten().InnerExceptions.FirstOrDefault() ?? e; + switch (inner) + { + case IOException io: + throw io; + case ValidationException ve: + throw ve; + default: + throw new InvalidOperationException("Unhandled error", inner); + } + } + + return tasks.Select(t => t.Result).ToImmutableArray(); + } + + private static IEnumerable> Partition(IReadOnlyList list, int size) + { + for (int i = 0; i < list.Count; i += size) + { + yield return list.Skip(i).Take(size).ToList(); + } + } + + /// Transforms a collection of K elements into T. + public interface TransformFunc + { + /// + /// Execute one batch. The number of elements is undefined. + /// + T Run(IEnumerable elements); + } +} diff --git a/src/Copybara.Core/MapMapper.cs b/src/Copybara.Core/MapMapper.cs new file mode 100644 index 000000000..ff806464d --- /dev/null +++ b/src/Copybara.Core/MapMapper.cs @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Transform; + +namespace Copybara; + +/// A reversible string-to-string mapper backed by an immutable map. +public class MapMapper : IReversibleFunction +{ + private readonly ImmutableDictionary _map; + + internal MapMapper(ImmutableDictionary map) + { + _map = Preconditions.CheckNotNull(map); + } + + public IReversibleFunction ReverseMapping() + { + var inverse = ImmutableDictionary.CreateBuilder(); + foreach (var kv in _map) + { + if (inverse.ContainsKey(kv.Value)) + { + throw new NonReversibleValidationException( + "Non-reversible map: " + _map + ": key '" + kv.Value + "' mapped more than once"); + } + inverse[kv.Value] = kv.Key; + } + return new MapMapper(inverse.ToImmutable()); + } + + public string Apply(string s) => _map.TryGetValue(s, out var v) ? v : s; +} diff --git a/src/Copybara.Core/MergeImportConfiguration.cs b/src/Copybara.Core/MergeImportConfiguration.cs new file mode 100644 index 000000000..6122a5665 --- /dev/null +++ b/src/Copybara.Core/MergeImportConfiguration.cs @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara; + +/// An object used to configure Merge Import. +[StarlarkBuiltin("core.merge_import_config", Documented = false)] +public sealed class MergeImportConfiguration : IStarlarkValue +{ + /// + /// The strategy to use for merging files. + /// DIFF3 shells out to diff3 with the -m flag to perform a 3-way merge. PATCH_MERGE creates + /// a patch file by diffing the baseline and destination files, and then applies the patch to the + /// origin file. + /// + public enum MergeStrategy + { + DIFF3, + PATCH_MERGE, + UNKNOWN, + } + + private readonly string _packagePath; + private readonly Glob _paths; + private readonly bool _useConsistencyFile; + private readonly MergeStrategy _mergeStrategy; + + private MergeImportConfiguration( + string packagePath, Glob paths, bool useConsistencyFile, MergeStrategy mergeStrategy) + { + _packagePath = packagePath; + _paths = paths; + _useConsistencyFile = useConsistencyFile; + _mergeStrategy = mergeStrategy; + } + + public static MergeImportConfiguration Create( + string packagePath, Glob paths, bool useConsistencyFile, MergeStrategy mergeStrategy) => + new(packagePath, paths, useConsistencyFile, mergeStrategy); + + public string PackagePath() => _packagePath; + + public Glob Paths() => _paths; + + public bool UseConsistencyFile() => _useConsistencyFile; + + public MergeStrategy Strategy() => _mergeStrategy; + + public override bool Equals(object? obj) => + obj is MergeImportConfiguration o + && _packagePath == o._packagePath + && Equals(_paths, o._paths) + && _useConsistencyFile == o._useConsistencyFile + && _mergeStrategy == o._mergeStrategy; + + public override int GetHashCode() => + HashCode.Combine(_packagePath, _paths, _useConsistencyFile, _mergeStrategy); +} diff --git a/src/Copybara.Core/Metadata.cs b/src/Copybara.Core/Metadata.cs new file mode 100644 index 000000000..4a69a4834 --- /dev/null +++ b/src/Copybara.Core/Metadata.cs @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Authoring; +using Copybara.Common; + +namespace Copybara; + +/// +/// Metadata associated with a change: Change message, author, etc. +/// +/// Note: upstream uses ImmutableSetMultimap for hidden labels; this port uses +/// but preserves the "no duplicates" semantics by +/// de-duplicating on merge. +/// +public sealed class Metadata +{ + private readonly string _message; + private readonly Author _author; + private readonly ImmutableListMultimap _hiddenLabels; + + public Metadata(string message, Author author, ImmutableListMultimap hiddenLabels) + { + _message = Preconditions.CheckNotNull(message); + _author = Preconditions.CheckNotNull(author); + _hiddenLabels = Preconditions.CheckNotNull(hiddenLabels); + } + + public Metadata WithAuthor(Author author) => + new(_message, Preconditions.CheckNotNull(author, "Author cannot be null"), _hiddenLabels); + + public Metadata WithMessage(string message) => + new(Preconditions.CheckNotNull(message, "Message cannot be null"), _author, _hiddenLabels); + + /// + /// We never allow deleting hidden labels. Use a different name if you want to rename one. + /// + public Metadata WithHiddenLabels(ImmutableListMultimap hiddenLabels) + { + Preconditions.CheckNotNull(hiddenLabels, "hidden labels cannot be null"); + var builder = ImmutableListMultimap.CreateBuilder(); + foreach (var key in _hiddenLabels.Keys) + { + foreach (var value in _hiddenLabels.Get(key)) + { + builder.Put(key, value); + } + } + foreach (var key in hiddenLabels.Keys) + { + foreach (var value in hiddenLabels.Get(key)) + { + // Preserve set semantics: don't add duplicate entries. + if (!_hiddenLabels.ContainsEntry(key, value)) + { + builder.Put(key, value); + } + } + } + return new Metadata(_message, _author, builder.Build()); + } + + /// Description to be used for the change. + public string GetMessage() => _message; + + /// Author to be used for the change. + public Author GetAuthor() => _author; + + /// + /// Hidden labels are labels added by transformations during transformations but that are not + /// visible in the message. + /// + public ImmutableListMultimap GetHiddenLabels() => _hiddenLabels; + + public override string ToString() => + $"Metadata{{message={_message}, author={_author}, hiddenLabels={_hiddenLabels}}}"; +} diff --git a/src/Copybara.Core/MigrationInfo.cs b/src/Copybara.Core/MigrationInfo.cs new file mode 100644 index 000000000..688c89249 --- /dev/null +++ b/src/Copybara.Core/MigrationInfo.cs @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Revision; + +namespace Copybara; + +/// Reflective information about the migration in progress. +public class MigrationInfo +{ + private readonly string? _originLabel; + private readonly IChangeVisitable? _destinationVisitable; + + public MigrationInfo(string? originLabel, IChangeVisitable? destinationVisitable) + { + _originLabel = originLabel; + _destinationVisitable = destinationVisitable; + } + + public string GetOriginLabel() => Preconditions.CheckNotNull(_originLabel); + + public IChangeVisitable? DestinationVisitable() => _destinationVisitable; +} diff --git a/src/Copybara.Core/ModuleSet.cs b/src/Copybara.Core/ModuleSet.cs new file mode 100644 index 000000000..4b267a82b --- /dev/null +++ b/src/Copybara.Core/ModuleSet.cs @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; + +namespace Copybara; + +/// +/// A set of modules and options for evaluating a Skylark config file. +/// +public class ModuleSet +{ + private readonly Options _options; + + // TODO(malcon): Remove this once all modules are @StarlarkMethod + private readonly IReadOnlySet _staticModules; + private readonly IReadOnlyDictionary _modules; + + internal ModuleSet( + Options options, + IReadOnlySet staticModules, + IReadOnlyDictionary modules) + { + _options = Preconditions.CheckNotNull(options); + _staticModules = Preconditions.CheckNotNull(staticModules); + _modules = Preconditions.CheckNotNull(modules); + } + + /// Copybara options. + public Options GetOptions() => _options; + + /// + /// Static modules. Will be deleted. + /// TODO(malcon): Delete + /// + public IReadOnlySet GetStaticModules() => _staticModules; + + /// Non-static Copybara modules. + public IReadOnlyDictionary GetModules() => _modules; +} diff --git a/src/Copybara.Core/ModuleSupplier.cs b/src/Copybara.Core/ModuleSupplier.cs new file mode 100644 index 000000000..8cd3b4031 --- /dev/null +++ b/src/Copybara.Core/ModuleSupplier.cs @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Starlark.Annot; + +// Domain 'Console' collides with System.Console. +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// +/// A supplier of modules and s for Copybara. +/// +public class ModuleSupplier +{ + // TODO(port): Upstream registers CoreGlobal.class here. CoreGlobal is not yet ported. + // TODO(malcon): Remove once no more static modules exist. + private static readonly ImmutableHashSet BasicModules = ImmutableHashSet.Empty; + + private readonly IReadOnlyDictionary _environment; + private readonly string _fileSystemRoot; + private readonly Console _console; + + public ModuleSupplier( + IReadOnlyDictionary environment, + string fileSystemRoot, + Console console) + { + _environment = Preconditions.CheckNotNull(environment); + _fileSystemRoot = Preconditions.CheckNotNull(fileSystemRoot); + _console = Preconditions.CheckNotNull(console); + } + + /// + /// Returns the set of static modules available. + /// TODO(malcon): Remove once no more static modules exist. + /// + protected virtual IReadOnlySet GetStaticModules() => BasicModules; + + /// + /// Get non-static modules available. + /// + public virtual IReadOnlySet GetModules(Options options) + { + GeneralOptions general = options.Get(); + + // TODO(port): The upstream ModuleSupplier wires up the full set of Starlark modules here: + // CoreModule (needs WorkflowOptions, DebugOptions, FolderModule), GitModule, HgModule, + // FolderModule, FormatModule, BuildozerModule, PatchModule, MetadataModule, + // Authoring.Module, RemoteFileModule, ArchiveModule, Re2Module, TomlModule, HtmlModule, + // XmlModule, StructModule, StarlarkDateTimeModule, StarlarkRandomModule, GoModule, + // RustModule, HashingModule, HttpModule, PythonModule, NpmModule, CredentialModule, + // and Json.INSTANCE. + // None of these module types are ported yet; register them here as they land. Returning an + // empty set for now so the engine can still be constructed. + _ = general; + return ImmutableHashSet.Empty; + } + + /// Returns a new set of s. + protected virtual Options NewOptions() + { + var generalOptions = new GeneralOptions(_environment, _fileSystemRoot, _console); + var workflowOptions = new WorkflowOptions(); + + // TODO(port): Upstream also constructs and registers the following options, none of which + // are ported yet. Add them to the list below as they become available: + // GitOptions, GitDestinationOptions, GitOriginOptions, GitHubOptions, + // GitHubPrOriginOptions, GitHubDestinationOptions, GerritOptions, GitMirrorOptions, + // GitLabOptions, BuildifierOptions, BuildozerOptions, FolderDestinationOptions, + // FolderOriginOptions, HgOptions, HgOriginOptions, PatchingOptions, RemoteFileOptions, + // DebugOptions, GeneratorOptions, HttpOptions, RegenerateOptions, CredentialOptions. + return new Options( + new IOption[] + { + generalOptions, + workflowOptions, + }); + } + + /// + /// A ModuleSet contains the collection of modules and flags for one Skylark copy.bara.sky + /// evaluation/execution. + /// + public ModuleSet Create() + { + Options options = NewOptions(); + return CreateWithOptions(options); + } + + public ModuleSet CreateWithOptions(Options options) + { + return new ModuleSet(options, GetStaticModules(), ModulesToVariableMap(options)); + } + + private IReadOnlyDictionary ModulesToVariableMap(Options options) + { + return GetModules(options) + .ToImmutableDictionary(FindClosestStarlarkBuiltinName, o => o); + } + + private static string FindClosestStarlarkBuiltinName(object o) + { + Type? cls = o.GetType(); + while (cls != null && cls != typeof(object)) + { + var annotation = + (StarlarkBuiltinAttribute?)Attribute.GetCustomAttribute( + cls, typeof(StarlarkBuiltinAttribute), inherit: false); + if (annotation != null) + { + return annotation.Name; + } + + cls = cls.BaseType; + } + + throw new InvalidOperationException( + "Cannot find @StarlarkBuiltin for " + o.GetType()); + } +} diff --git a/src/Copybara.Core/Monitor/ConsoleEventMonitor.cs b/src/Copybara.Core/Monitor/ConsoleEventMonitor.cs new file mode 100644 index 000000000..dd4628241 --- /dev/null +++ b/src/Copybara.Core/Monitor/ConsoleEventMonitor.cs @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Monitor; + +/// +/// An that logs every event to the console in verbose mode, then +/// forwards it to a delegate monitor. +/// +public class ConsoleEventMonitor : IEventMonitor +{ + private readonly Console _console; + private readonly IEventMonitor _delegate; + + public ConsoleEventMonitor(Console console, IEventMonitor @delegate) + { + _console = Preconditions.CheckNotNull(console); + _delegate = Preconditions.CheckNotNull(@delegate); + } + + public void OnMigrationStarted(IEventMonitor.MigrationStartedEvent @event) + { + _console.VerboseFmt("onMigrationStarted(): %s", @event); + _delegate.OnMigrationStarted(@event); + } + + public void OnChangeMigrationStarted(IEventMonitor.ChangeMigrationStartedEvent @event) + { + _console.VerboseFmt("onChangeMigrationStarted(): %s", @event); + _delegate.OnChangeMigrationStarted(@event); + } + + public void OnChangeMigrationFinished(IEventMonitor.ChangeMigrationFinishedEvent @event) + { + _console.VerboseFmt("onChangeMigrationFinished(): %s", @event); + _delegate.OnChangeMigrationFinished(@event); + } + + public void OnMigrationFinished(IEventMonitor.MigrationFinishedEvent @event) + { + _console.VerboseFmt("onMigrationFinished(): %s", @event); + _delegate.OnMigrationFinished(@event); + } + + public void OnInfoFinished(IEventMonitor.InfoFinishedEvent @event) + { + _console.VerboseFmt("onInfoFinished(): %s", @event); + _delegate.OnInfoFinished(@event); + } +} diff --git a/src/Copybara.Core/Monitor/EventMonitor.cs b/src/Copybara.Core/Monitor/EventMonitor.cs new file mode 100644 index 000000000..0b4de1bd9 --- /dev/null +++ b/src/Copybara.Core/Monitor/EventMonitor.cs @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Effect; +using Copybara.Util; +using Task = Copybara.Profiler.Task; + +namespace Copybara.Monitor; + +/// +/// A monitor that allows triggering actions when high-level actions take place during the execution. +/// +/// Default implementation logs in the console the events in verbose mode only. +/// +public interface IEventMonitor +{ + /// An empty monitor that does nothing on any event. + static readonly IEventMonitor EmptyMonitor = new EmptyEventMonitor(); + + /// Invoked when the migration starts, only once at the beginning of the execution. + void OnMigrationStarted(MigrationStartedEvent @event) { } + + /// Invoked when each change migration starts. + void OnChangeMigrationStarted(ChangeMigrationStartedEvent @event) { } + + /// Invoked when each change migration finishes. + void OnChangeMigrationFinished(ChangeMigrationFinishedEvent @event) { } + + /// Invoked when the migration finishes, only once at the end of the execution. + void OnMigrationFinished(MigrationFinishedEvent @event) { } + + /// Invoked when an info subcommand finishes, only once at the end of the execution. + void OnInfoFinished(InfoFinishedEvent @event) { } + + /// Invoked when an info subcommand fails, only once at the end of the execution. + void OnInfoFailed(InfoFailedEvent @event) { } + + /// Concrete no-op monitor backing . + private sealed class EmptyEventMonitor : IEventMonitor + { + } + + /// Event that happens for every migration that is started. + class MigrationStartedEvent + { + public override string ToString() => "MigrationStartedEvent"; + } + + /// Event that happens for every change migration that is started. + class ChangeMigrationStartedEvent + { + public override string ToString() => "ChangeMigrationStartedEvent{}"; + } + + /// Event that happens for every change migration that is finished. + class ChangeMigrationFinishedEvent + { + private readonly ImmutableArray _destinationEffects; + private readonly ImmutableListMultimap _originDescription; + private readonly ImmutableListMultimap _destinationDescription; + + public ChangeMigrationFinishedEvent( + ImmutableArray destinationEffects, + ImmutableListMultimap originDescription, + ImmutableListMultimap destinationDescription) + { + _destinationEffects = destinationEffects; + _originDescription = originDescription; + _destinationDescription = destinationDescription; + } + + public IReadOnlyList DestinationEffects => _destinationEffects; + + public ImmutableListMultimap DestinationDescription => _destinationDescription; + + public ImmutableListMultimap OriginDescription => _originDescription; + + public override string ToString() => + $"ChangeMigrationFinishedEvent{{destinationEffects=[{string.Join(", ", _destinationEffects)}]}}"; + } + + /// Event that happens for every migration that is finished. + class MigrationFinishedEvent + { + private readonly ExitCode _exitCode; + private readonly IReadOnlyList? _profileData; + + public MigrationFinishedEvent(ExitCode exitCode) + : this(exitCode, null) + { + } + + public MigrationFinishedEvent(ExitCode exitCode, IReadOnlyList? profileData) + { + _exitCode = exitCode; + _profileData = profileData?.ToImmutableArray(); + } + + public ExitCode ExitCode => _exitCode; + + /// Profiler task data, if any (Java's Optional<List<Task>>). + public IReadOnlyList? ProfileData => _profileData; + + public override string ToString() => + $"MigrationFinishedEvent{{exitCode={_exitCode}, profiler=" + + (_profileData == null ? "null" : $"[{string.Join(", ", _profileData)}]") + "}"; + } + + /// Event that happens for every info subcommand that is finished. + class InfoFinishedEvent + { + // Java holds an Info. The com.google.copybara.Info type has not been + // ported yet, so the payload is carried as object; consumers can cast once Info lands. + private readonly object _info; + private readonly ImmutableDictionary _context; + + public InfoFinishedEvent(object info, ImmutableDictionary context) + { + _info = Preconditions.CheckNotNull(info); + _context = Preconditions.CheckNotNull(context); + } + + public InfoFinishedEvent(object info) + : this(info, ImmutableDictionary.Empty) + { + } + + public object Info => _info; + + public IReadOnlyDictionary Context => _context; + + public override string ToString() => + $"InfoFinishedEvent{{info={_info}, context={{{string.Join(", ", _context.Select(kv => kv.Key + "=" + kv.Value))}}}}}"; + } + + /// Event that happens for every info subcommand that failed. + class InfoFailedEvent + { + private readonly string _error; + private readonly ImmutableDictionary _context; + + public InfoFailedEvent(string error, ImmutableDictionary context) + { + _error = Preconditions.CheckNotNull(error); + _context = Preconditions.CheckNotNull(context); + } + + public string Error => _error; + + public IReadOnlyDictionary Context => _context; + + public override string ToString() => + $"InfoFailedEvent{{error={_error}, context={{{string.Join(", ", _context.Select(kv => kv.Key + "=" + kv.Value))}}}}}"; + } + + /// Holder for all active event monitors. + class EventMonitors + { + private readonly IReadOnlyList _monitors; + + public EventMonitors(IReadOnlyList monitors) + { + _monitors = monitors; + } + + /// Accepts a functional to apply to all active monitors. + public void DispatchEvent(Action @event) + { + foreach (var monitor in _monitors) + { + @event(monitor); + } + } + } +} diff --git a/src/Copybara.Core/Option.cs b/src/Copybara.Core/Option.cs new file mode 100644 index 000000000..9c61f7686 --- /dev/null +++ b/src/Copybara.Core/Option.cs @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara; + +/// +/// An indicates a class has options usable by copybara's config system. +/// +public interface IOption +{ +} diff --git a/src/Copybara.Core/Options.cs b/src/Copybara.Core/Options.cs new file mode 100644 index 000000000..3a97fe9dc --- /dev/null +++ b/src/Copybara.Core/Options.cs @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; + +namespace Copybara; + +/// +/// A class that groups all the options used in the program. +/// +public class Options +{ + private readonly ImmutableDictionary _config; + + public Options(IEnumerable options) + { + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var option in options) + { + builder.Add(option.GetType(), option); + } + + _config = builder.ToImmutable(); + } + + /// + /// Get an option for a given type. + /// + /// If the configuration cannot be found. + public T Get() where T : IOption + { + return (T)Get(typeof(T)); + } + + /// + /// Get an option for a given type. + /// + /// If the configuration cannot be found. + public IOption Get(Type optionType) + { + if (_config.TryGetValue(optionType, out var option)) + { + return option; + } + + // If we didn't find the exact type, look for a subclass. + foreach (var entry in _config) + { + if (optionType.IsAssignableFrom(entry.Key)) + { + return entry.Value; + } + } + + throw new InvalidOperationException("No option type found for " + optionType); + } + + public IReadOnlyCollection GetAll() => _config.Values.ToList(); +} diff --git a/src/Copybara.Core/Origin.cs b/src/Copybara.Core/Origin.cs new file mode 100644 index 000000000..915b62d30 --- /dev/null +++ b/src/Copybara.Core/Origin.cs @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Approval; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Util; +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// An Origin represents a source control repository from which source is copied. +/// the origin type of the references/revisions this origin handles. +public interface IOrigin : IConfigItemDescription, Starlark.Eval.IStarlarkValue + where R : class, IRevision +{ + /// + /// Resolves a migration reference into a revision. For example for git it would resolve 'main' to + /// the SHA-1. + /// + R Resolve(string reference); + + /// Resolves a migration last migrated reference into a revision. + R ResolveLastRev(string reference) => Resolve(reference); + + /// + /// Resolves a reference into a revision, but only if the provided descendantRev is an ancestor of + /// ancestorRef. + /// + R ResolveAncestorRef(string ancestorRef, R descendantRev) => + throw new ValidationException("Pinning to an ancestor ref is unsupported by this origin."); + + /// + /// Show different changes between two references. Returns null if the origin doesn't support + /// generating differences. + /// + string? ShowDiff(R revisionFrom, R revisionTo) => null; + + /// + /// Get an approvals provider that is able to find for each change its approval status. + /// + /// By default we return all the changes without any approval. + /// + IApprovalsProvider GetApprovalsProvider() => new NoneApprovedProvider(); + + /// Creates a new reader of this origin. + IReader NewReader(Glob originFiles, Copybara.Authoring.Authoring authoring); + + /// + /// Label name to be used when creating a commit message in the destination to refer to a + /// revision. For example "Git-RevId". + /// + string GetLabelName(); + + /// + /// An object which is capable of checking out code from the origin at particular paths. This can + /// also enumerate changes in the history and transform authorship information. + /// + /// the revision type. + interface IReader : IChangeVisitable + where TR : class, IRevision + { + /// + /// Checks out the revision from the repository into the + /// directory. + /// + void Checkout(TR reference, string checkoutDir); + + /// Returns the list of versions from the origin repository. + IReadOnlyList> GetVersions() => ImmutableArray>.Empty; + + /// Returns the changes that happen in the interval (fromRef, toRef]. + Origin.ChangesResponse Changes(TR? fromRef, TR toRef); + + /// + /// Returns true if the origin repository supports maintaining a history of changes. + /// Generally this should be true. + /// + bool SupportsHistory() => true; + + /// Returns a change identified by . + Change Change(TR reference); + + /// Finds the baseline of startRevision. + Origin.Baseline? FindBaseline(TR startRevision, string label) + { + var visitor = new Origin.FindLatestWithLabel(startRevision, label); + VisitChanges(startRevision, visitor); + return visitor.GetBaseline(); + } + + /// Find the baseline of the change without using a label. + IReadOnlyList FindBaselinesWithoutLabel(TR startRevision, int limit) => + throw new ValidationException("Origin doesn't support this workflow mode"); + + /// Utility endpoint for accessing and adding feedback data. + IEndpoint GetFeedbackEndPoint(Console console) => IEndpoint.NoopEndpoint; + } +} + +/// +/// Non-generic holder for 's nested helper types (mirrors the types nested +/// inside the Java Origin / Origin.Reader interfaces). +/// +public static class Origin +{ + /// Represents a baseline pointer in the origin. + public sealed class Baseline + where R : class, IRevision + { + private readonly string _baseline; + private readonly R? _originRevision; + + public Baseline(string baseline, R? originRevision) + { + _baseline = Preconditions.CheckNotNull(baseline); + _originRevision = originRevision; + } + + /// The baseline reference that will be used in the destination. + public string GetBaseline() => _baseline; + + /// A reference to the origin revision where the baseline was found. + public R? GetOriginRevision() => _originRevision; + + public override string ToString() => + $"Baseline{{baseline={_baseline}, originRevision={_originRevision}}}"; + } + + /// Reason why Origin.Reader.Changes didn't return any change. + public enum EmptyReason + { + /// 'from' is ancestor of 'to' but all changes are for irrelevant files. + NoChanges, + + /// There is no parent/child relationship between 'from' and 'to'. + UnrelatedRevisions, + + /// 'to' is equal or ancestor of 'from'. + ToIsAncestor, + } + + public sealed class ChangesResponse + where R : class, IRevision + { + private readonly ImmutableArray> _changes; + private readonly EmptyReason? _emptyReason; + + /// + /// Changes in key will only be included if the value is included. The usage is for non-linear + /// histories like git where including a change depends if we end up including the merge commit. + /// + private readonly ImmutableDictionary, Change> _conditionalChanges; + + private ChangesResponse( + ImmutableArray> changes, + ImmutableDictionary, Change> conditionalChanges, + EmptyReason? emptyReason) + { + _changes = changes; + _conditionalChanges = Preconditions.CheckNotNull(conditionalChanges); + _emptyReason = emptyReason; + Preconditions.CheckArgument( + _changes.IsEmpty ^ (emptyReason == null), + "Either we have changes or we have an empty reason"); + } + + public static ChangesResponse ForChanges(IEnumerable> changes) + { + var arr = changes.ToImmutableArray(); + Preconditions.CheckArgument(!arr.IsEmpty, "Empty changes not allowed"); + return new ChangesResponse( + arr, ImmutableDictionary, Change>.Empty, emptyReason: null); + } + + /// + /// Build a ChangesResponse object with changes where some of them are conditional to their + /// closest first-parent root being included (merge commit). + /// + public static ChangesResponse ForChangesWithMerges(IEnumerable> changes) + { + var all = changes.ToList(); + Preconditions.CheckArgument(all.Count != 0, "Shouldn't be called for empty changes"); + + var byRevision = new Dictionary>(); + foreach (var e in all) + { + byRevision[e.GetRevision()] = e; + } + + var firstParents = new List>(); + var toSkip = new HashSet(); + var latest = all[^1]; + + // Compute first parents and add them to toSkip so they are not counted as conditional + // changes later. + while (true) + { + firstParents.Add(latest); + toSkip.Add(latest.GetRevision()); + var parents = Parents(latest); + if (parents.Length == 0) + { + break; + } + R firstParent = parents[0]; + if (!byRevision.TryGetValue(firstParent, out var firstParentChange)) + { + break; + } + latest = firstParentChange; + } + + var conditionalChanges = ImmutableDictionary.CreateBuilder, Change>(); + + // Traverse from old to new so we use oldest first-parent as the conditional change. + for (int i = firstParents.Count - 1; i >= 0; i--) + { + var firstParent = firstParents[i]; + var fpParents = Parents(firstParent); + if (fpParents.Length < 2) + { + continue; + } + var toVisit = new Queue(); + for (int j = 1; j < fpParents.Length; j++) + { + toVisit.Enqueue(fpParents[j]); + } + while (toVisit.Count != 0) + { + R revision = toVisit.Dequeue(); + if (!toSkip.Add(revision)) + { + continue; + } + if (!byRevision.TryGetValue(revision, out var change)) + { + continue; + } + conditionalChanges[change] = firstParent; + foreach (var p in Parents(change)) + { + toVisit.Enqueue(p); + } + } + } + return new ChangesResponse( + all.ToImmutableArray(), conditionalChanges.ToImmutable(), emptyReason: null); + } + + private static ImmutableArray Parents(Change change) => + Preconditions.CheckNotNull( + change.GetParents(), + "Don't use forChangesWithParents for changes that don't support parents: {0}", + change); + + /// Create a ChangesResponse that doesn't contain any change. + public static ChangesResponse NoChanges(EmptyReason emptyReason) => + new( + ImmutableArray>.Empty, + ImmutableDictionary, Change>.Empty, + emptyReason); + + /// Returns true if there are no changes. + public bool IsEmpty() => _changes.IsEmpty; + + public EmptyReason GetEmptyReason() => + _emptyReason ?? throw new InvalidOperationException("Use isEmpty() first"); + + /// The changes that happen in the interval (fromRef, toRef]. + public IReadOnlyList> GetChanges() => _changes; + + /// Changes that should only be included if the change in the value is also included. + public IReadOnlyDictionary, Change> GetConditionalChanges() => + _conditionalChanges; + } + + public sealed class FindLatestWithLabel : IChangesVisitor + where R : class, IRevision + { + private readonly R _startRevision; + private readonly string _label; + private Baseline? _baseline; + + public FindLatestWithLabel(R startRevision, string label) + { + _startRevision = Preconditions.CheckNotNull(startRevision); + _label = Preconditions.CheckNotNull(label); + } + + public Baseline? GetBaseline() => _baseline; + + public VisitResult Visit(Change input) + { + if (input.GetRevision().AsString().Equals(_startRevision.AsString())) + { + return VisitResult.Continue; + } + var labels = input.GetLabels(); + if (!labels.ContainsKey(_label)) + { + return VisitResult.Continue; + } + var values = labels.Get(_label); + _baseline = new Baseline(values[values.Length - 1], (R)input.GetRevision()); + return VisitResult.Terminate; + } + } +} diff --git a/src/Copybara.Core/PathOps.cs b/src/Copybara.Core/PathOps.cs new file mode 100644 index 000000000..6cdf35e21 --- /dev/null +++ b/src/Copybara.Core/PathOps.cs @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara; + +/// +/// Small helper mirroring the subset of java.nio.file.Path semantics used by +/// / . Paths are treated as +/// forward-slash-separated strings. +/// +internal static class PathOps +{ + public static bool IsAbsolute(string path) => + path.StartsWith('/') || System.IO.Path.IsPathRooted(path); + + /// Resolves against . + public static string Resolve(string basePath, string other) + { + if (other.Length == 0) + { + return basePath; + } + if (IsAbsolute(other)) + { + return other; + } + if (basePath.Length == 0) + { + return other; + } + return TrimTrailingSlash(basePath) + "/" + other; + } + + /// Resolves against the parent of . + public static string ResolveSibling(string path, string other) + { + string? parent = GetParent(path); + return parent == null ? other : Resolve(parent, other); + } + + /// Normalizes a path, resolving . and .. segments. + public static string Normalize(string path) + { + bool absolute = path.StartsWith('/'); + var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries); + var stack = new List(); + foreach (var part in parts) + { + if (part == ".") + { + continue; + } + if (part == "..") + { + if (stack.Count > 0 && stack[^1] != "..") + { + stack.RemoveAt(stack.Count - 1); + } + else if (!absolute) + { + stack.Add(".."); + } + continue; + } + stack.Add(part); + } + string joined = string.Join('/', stack); + return absolute ? "/" + joined : joined; + } + + /// Returns true if starts with . + public static bool StartsWith(string path, string prefix) + { + string p = TrimTrailingSlash(Normalize(path)); + string pre = TrimTrailingSlash(Normalize(prefix)); + if (pre.Length == 0) + { + return true; + } + return p == pre || p.StartsWith(pre + "/", StringComparison.Ordinal); + } + + public static string GetFileName(string path) + { + string trimmed = TrimTrailingSlash(path); + int idx = trimmed.LastIndexOf('/'); + return idx == -1 ? trimmed : trimmed.Substring(idx + 1); + } + + /// Returns the parent path, or null if there is none. + public static string? GetParent(string path) + { + string trimmed = TrimTrailingSlash(path); + int idx = trimmed.LastIndexOf('/'); + if (idx == -1) + { + return null; + } + if (idx == 0) + { + return "/"; + } + return trimmed.Substring(0, idx); + } + + /// Constructs a relative path from to . + public static string Relativize(string basePath, string other) + { + var baseParts = Normalize(basePath).Split('/', StringSplitOptions.RemoveEmptyEntries); + var otherParts = Normalize(other).Split('/', StringSplitOptions.RemoveEmptyEntries); + int common = 0; + while (common < baseParts.Length && common < otherParts.Length + && baseParts[common] == otherParts[common]) + { + common++; + } + var result = new List(); + for (int i = common; i < baseParts.Length; i++) + { + result.Add(".."); + } + for (int i = common; i < otherParts.Length; i++) + { + result.Add(otherParts[i]); + } + return string.Join('/', result); + } + + private static string TrimTrailingSlash(string path) => + path.Length > 1 && path.EndsWith('/') ? path.TrimEnd('/') : path; +} diff --git a/src/Copybara.Core/Profiler/ConsoleProfilerListener.cs b/src/Copybara.Core/Profiler/ConsoleProfilerListener.cs new file mode 100644 index 000000000..a870e6b7f --- /dev/null +++ b/src/Copybara.Core/Profiler/ConsoleProfilerListener.cs @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Profiler; + +/// +/// A profiler that prints profiling stats to the console in verbose mode. +/// +public class ConsoleProfilerListener : IListener +{ + private readonly Console _console; + + public ConsoleProfilerListener(Console console) + { + _console = Preconditions.CheckNotNull(console); + } + + public void TaskStarted(Task task) + { + // Ignored. We only record the finish event. + } + + public void TaskFinished(Task task) + { + _console.VerboseFmt("PROFILE: %6d %s", task.ElapsedNanos() / 1_000_000, task.Description); + } +} diff --git a/src/Copybara.Core/Profiler/Listener.cs b/src/Copybara.Core/Profiler/Listener.cs new file mode 100644 index 000000000..f458531a1 --- /dev/null +++ b/src/Copybara.Core/Profiler/Listener.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Profiler; + +/// +/// A listener that, when registered in a , will be notified every time +/// a task is started/finished. +/// +public interface IListener +{ + /// A notification about a task that has started. + void TaskStarted(Task task); + + /// + /// A notification about a task finish. It is guaranteed that + /// will return true. + /// + void TaskFinished(Task task); +} diff --git a/src/Copybara.Core/Profiler/LogProfilerListener.cs b/src/Copybara.Core/Profiler/LogProfilerListener.cs new file mode 100644 index 000000000..46168f394 --- /dev/null +++ b/src/Copybara.Core/Profiler/LogProfilerListener.cs @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Copybara.Profiler; + +/// +/// A simple callback for the profiler that logs the execution of the tasks when they finish. +/// +/// +/// +/// The Java original injects the caller's log site via a StackWalker so the log message is +/// attributed to the caller rather than to the profiler. That log-site injection has no equivalent +/// in Microsoft.Extensions.Logging, so this port logs at Information level directly. +/// +public class LogProfilerListener : IListener +{ + private readonly ILogger _logger; + + public LogProfilerListener() + : this(NullLogger.Instance) + { + } + + public LogProfilerListener(ILogger logger) + { + _logger = logger; + } + + public void TaskStarted(Task task) + { + // Ignored. We only record the finish event. + } + + public void TaskFinished(Task task) + { + long millis = task.ElapsedNanos() / 1_000_000; + _logger.LogInformation("PROFILE: {Millis,6} {Description}", millis, task.Description); + } +} diff --git a/src/Copybara.Core/Profiler/Profiler.cs b/src/Copybara.Core/Profiler/Profiler.cs new file mode 100644 index 000000000..5d8e43d80 --- /dev/null +++ b/src/Copybara.Core/Profiler/Profiler.cs @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using System.Collections.Immutable; +using Copybara.Common; + +namespace Copybara.Profiler; + +/// +/// A profiler that allows to record hierarchical time statistics of the different Copybara +/// components. +/// +/// +/// +/// The Java original stores its per-thread task stack in an InheritableThreadLocal +/// whose childValue hook copies only the top element into a spawned thread so a child can +/// build (but not finish) descendants of its parent's current task. .NET has no +/// InheritableThreadLocal with a childValue hook; this port uses a +/// whose factory seeds a fresh stack with a "//detached_thread" root, +/// mirroring the Java initialValue path. The common single-threaded start/stop/scope +/// behaviour is preserved exactly. +/// +public sealed class Profiler +{ + public const string RootName = "//copybara"; + public const string TypeKey = "type"; + + private readonly Ticker _ticker; + private readonly ProfilerTask _nullProfilerTask; + + /// + /// A stack of tasks to be finished. Each thread keeps its own stack; a freshly seen thread gets + /// a "//detached_thread" root, matching Java's initialValue. + /// + private readonly ThreadLocal?> _taskQueue; + + private volatile bool _stopped; + private IReadOnlyList _listeners = ImmutableArray.Empty; + private ProfilerTask? _rootProfilerTask; + + public Profiler(Ticker ticker) + { + _ticker = ticker; + _nullProfilerTask = new ProfilerTask(this, expectedTask: null); + _taskQueue = new ThreadLocal?>(() => + { + if (_stopped) + { + return null; + } + return CreateQueue(new Task("//detached_thread", _ticker.Read())); + }); + } + + private static Stack CreateQueue(Task element) + { + var tasks = new Stack(2); + tasks.Push(element); + return tasks; + } + + /// + /// Call this method once at the beginning of the Copybara binary run. + /// + /// the listeners to be notified of the task events + public void Init(IReadOnlyList listeners) + { + _listeners = listeners; + if (listeners.Count == 0) + { + return; + } + var task = new Task(RootName, _ticker.Read()); + _taskQueue.Value = CreateQueue(task); + foreach (var listener in listeners) + { + listener.TaskStarted(task); + } + _rootProfilerTask = new ProfilerTask(this, task); + } + + /// + /// Call this method once at the end of the Copybara binary run. + /// + public void Stop() + { + if (_listeners.Count == 0) + { + return; + } + var queue = _taskQueue.Value!; + Preconditions.CheckState(queue.Peek().Description.Equals(RootName)); + _rootProfilerTask!.Close(); + Preconditions.CheckState(queue.Count == 0); + _stopped = true; + } + + public ImmutableDictionary TaskType(string type) => + ImmutableDictionary.Empty.Add(TypeKey, type); + + /// + /// Create a new profiler task that can be closed using a using statement. + /// + /// The profiler tasks are reentrant. So you can stack them in multiple nested + /// using blocks. + /// + /// Example usage: + /// + /// using (var p = profiler.Start("migration")) + /// { + /// using (var p2 = profiler.Start("subtask")) + /// { + /// // Do job + /// } + /// } + /// + /// + /// description of the task + /// an task that can be closed manually or with a + /// using statement + public ProfilerTask Start(string description) => + Start(description, ImmutableDictionary.Empty); + + /// + /// Overloaded method for , that allows adding + /// to the context of this task. + /// + public ProfilerTask Start(string description, ImmutableDictionary fields) + { + if (_stopped || _listeners.Count == 0) + { + return _nullProfilerTask; + } + var tasks = _taskQueue.Value!; + Preconditions.CheckState(tasks.Count != 0); + var parent = tasks.Peek(); + var child = new Task(parent.Description + "/" + description, fields, _ticker.Read()); + tasks.Push(child); + foreach (var listener in _listeners) + { + listener.TaskStarted(child); + } + return new ProfilerTask(this, child); + } + + /// + /// Record a simple task metric. The user is in charge of providing its own time. + /// + public void SimpleTask(string description, long startNanos, long endNanos) + { + if (_stopped || _listeners.Count == 0) + { + return; + } + var tasks = _taskQueue.Value!; + Preconditions.CheckState(tasks.Count != 0); + var parent = tasks.Peek(); + var child = new Task(parent.Description + "/" + description, startNanos); + var finishedChild = child.Finish(endNanos); + foreach (var listener in _listeners) + { + listener.TaskStarted(child); + listener.TaskFinished(finishedChild); + } + } + + /// + /// A profiler task that can be closed to send the finish metric. + /// + public sealed class ProfilerTask : IDisposable + { + private readonly Profiler _profiler; + private readonly Task? _expectedTask; + + internal ProfilerTask(Profiler profiler, Task? expectedTask) + { + _profiler = profiler; + _expectedTask = expectedTask; + } + + /// Close the task if it's not null. + public void Close() + { + if (_expectedTask != null && !_profiler._stopped) + { + var task = _profiler._taskQueue.Value!.Pop(); + if (task != _expectedTask) + { + throw new InvalidOperationException( + "Trying to finish a task that is different from the registered one: " + + task.Description + ". Expecting: " + _expectedTask.Description); + } + task = task.Finish(_profiler._ticker.Read()); + foreach (var listener in _profiler._listeners) + { + listener.TaskFinished(task); + } + } + } + + public void Dispose() => Close(); + } +} diff --git a/src/Copybara.Core/Profiler/RecordingListener.cs b/src/Copybara.Core/Profiler/RecordingListener.cs new file mode 100644 index 000000000..5362e8762 --- /dev/null +++ b/src/Copybara.Core/Profiler/RecordingListener.cs @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; + +namespace Copybara.Profiler; + +/// +/// A profiler listener storing all completed tasks. +/// +public class RecordingListener : IListener +{ + private readonly List _finishedTasks = new(); + private readonly object _lock = new(); + + public void TaskStarted(Task task) + { + // Ignored. We only record the finish event. + } + + public void TaskFinished(Task task) + { + // For now, just finished tasks. In the future we should consider exporting open tasks as they + // might help pinpoint timeouts. + lock (_lock) + { + _finishedTasks.Add(task); + } + } + + /// List of all completed tasks, immutable. + public IReadOnlyList CompletedTasks + { + get + { + lock (_lock) + { + return _finishedTasks.ToImmutableArray(); + } + } + } +} diff --git a/src/Copybara.Core/Profiler/Task.cs b/src/Copybara.Core/Profiler/Task.cs new file mode 100644 index 000000000..ddc895488 --- /dev/null +++ b/src/Copybara.Core/Profiler/Task.cs @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; + +namespace Copybara.Profiler; + +/// +/// Represents a task run by Copybara. +/// +public sealed class Task +{ + private const long NotFinished = -1; + + private readonly string _description; + private readonly ImmutableDictionary _fields; + private readonly long _startNanos; + private readonly long _finishNanos; + + internal Task(string description, long startNanos) + : this(description, startNanos, NotFinished) + { + } + + internal Task(string description, ImmutableDictionary fields, long startNanos) + : this(description, fields, startNanos, NotFinished) + { + } + + internal Task(string description, long startNanos, long finishNanos) + : this(description, ImmutableDictionary.Empty, startNanos, finishNanos) + { + } + + internal Task( + string description, + ImmutableDictionary fields, + long startNanos, + long finishNanos) + { + _description = Preconditions.CheckNotNull(description); + _fields = Preconditions.CheckNotNull(fields); + _startNanos = startNanos; + _finishNanos = finishNanos; + } + + internal Task Finish(long finishNanos) + { + Preconditions.CheckArgument(finishNanos != -1, "Already finished!"); + return new Task(_description, _fields, _startNanos, finishNanos); + } + + /// + /// Description of the task. Follows a pattern like: + /// //copybara/task/subtask/subsubtask + /// + public string Description => _description; + + /// + /// Context fields of the task. + /// + /// They are not part of the profiler path, but they give more context information on this + /// task and it's type. Might be used to implement more extensive monitoring. + /// + public IReadOnlyDictionary Fields => _fields; + + /// + /// Time elapsed (in nanoseconds) running the task. Should only be called if + /// returns true. + /// + public long ElapsedNanos() + { + Preconditions.CheckState(_finishNanos != NotFinished, "Not finished!"); + return _finishNanos - _startNanos; + } + + /// Returns true if the task is finished. + public bool IsFinished => _finishNanos != NotFinished; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var task = (Task)o; + return _startNanos == task._startNanos + && _finishNanos == task._finishNanos + && string.Equals(_description, task._description); + } + + public override int GetHashCode() => HashCode.Combine(_description, _startNanos, _finishNanos); + + public override string ToString() => + $"Task{{description={_description}, startNanos={_startNanos}, finishNanos={_finishNanos}}}"; +} diff --git a/src/Copybara.Core/Profiler/Ticker.cs b/src/Copybara.Core/Profiler/Ticker.cs new file mode 100644 index 000000000..60d511edd --- /dev/null +++ b/src/Copybara.Core/Profiler/Ticker.cs @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Diagnostics; + +namespace Copybara.Profiler; + +/// +/// A time source that returns a monotonic reading in nanoseconds. Port of Guava's +/// com.google.common.base.Ticker restricted to the surface the uses. +/// +public abstract class Ticker +{ + /// Returns the number of nanoseconds elapsed since this ticker's fixed point. + public abstract long Read(); + + /// + /// A ticker backed by , converted to nanoseconds. This is the + /// .NET equivalent of Guava's Ticker.systemTicker() / System.nanoTime(). + /// + public static Ticker SystemTicker { get; } = new SystemTickerImpl(); + + private sealed class SystemTickerImpl : Ticker + { + private static readonly double NanosPerTick = 1_000_000_000.0 / Stopwatch.Frequency; + + public override long Read() => (long)(Stopwatch.GetTimestamp() * NanosPerTick); + } +} diff --git a/src/Copybara.Core/ReadConfigFromChangeWorkflow.cs b/src/Copybara.Core/ReadConfigFromChangeWorkflow.cs new file mode 100644 index 000000000..e4896f8af --- /dev/null +++ b/src/Copybara.Core/ReadConfigFromChangeWorkflow.cs @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using Copybara.Common; +using Copybara.Config; +using Copybara.Exceptions; +using Copybara.Revision; +using ChangeMigrationFinishedEvent = Copybara.Monitor.IEventMonitor.ChangeMigrationFinishedEvent; + +namespace Copybara; + +/// +/// An extension of that is capable of reloading itself, reading the +/// configuration from the origin location provided. +/// +/// The core implementation returns a regular run helper that always returns this for any +/// changes, which means that no config is read and the workflow remains immutable. This +/// implementation provides a ReloadingRunHelper that reads the configuration for the current +/// change being migrated, performs security and validation checks, and provides a new run helper +/// instance. +/// +/// Origin revision type. +/// Destination revision type. +public class ReadConfigFromChangeWorkflow : Workflow + where O : class, IRevision + where D : class, IRevision +{ + private readonly ConfigLoader _configLoader; + private readonly ConfigValidator _configValidator; + + internal ReadConfigFromChangeWorkflow( + Workflow workflow, + Options options, + ConfigLoader configLoader, + ConfigValidator configValidator) + : base( + workflow.GetName(), + workflow.GetDescription(), + workflow.GetOrigin(), + workflow.GetDestination(), + workflow.GetAuthoring(), + workflow.GetTransformation(), + workflow.GetLastRevisionFlag(), + workflow.IsInitHistory(), + options.Get(), + workflow.GetOriginFiles(), + workflow.GetDestinationFiles(), + workflow.GetMode(), + workflow.GetWorkflowOptions(), + workflow.GetReverseTransformForCheck(), + workflow.GetReversibleCheckIgnoreFiles(), + workflow.IsAskForConfirmation(), + workflow.GetMainConfigFile(), + workflow.GetAllConfigFiles(), + workflow.IsDryRunMode(), + workflow.IsCheckLastRevState(), + workflow.GetAfterMigrationActions().ToImmutableArray(), + workflow.GetAfterAllMigrationActions().ToImmutableArray(), + workflow.GetChangeIdentity(), + workflow.IsSetRevId(), + workflow.IsSmartPrune(), + workflow.GetMergeImport(), + workflow.GetAutoPatchfileConfiguration(), + workflow.AfterMergeTransformations, + workflow.IsMigrateNoopChanges(), + workflow.CustomRevId(), + workflow.IsCheckout(), + workflow.GetConsistencyFileConfig(), + workflow.GetExpectedFixedRef(), + workflow.GetPinnedFixedRef(), + workflow.GetDefinitionStack(), + workflow.GetDefinitionStackLocals()) + { + _configLoader = Preconditions.CheckNotNull(configLoader, "configLoaderProvider"); + _configValidator = Preconditions.CheckNotNull(configValidator, "configValidator"); + } + + public override WorkflowRunHelper NewRunHelper( + string workdir, + O resolvedRef, + string? rawSourceRef, + Action migrationFinishedMonitor) + { + IOrigin.IReader reader = GetOrigin().NewReader(GetOriginFiles(), GetAuthoring()); + return new ReloadingRunHelper( + this, + this, + GetName(), + workdir, + resolvedRef, + CreateWriter(resolvedRef), + reader, + rawSourceRef, + migrationFinishedMonitor); + } + + public override string ToString() => "ReadConfigFromChangeWorkflow{}"; + + /// + /// A that reloads itself based on the change being imported, + /// loading the configuration from the origin, after performing security and validation checks. + /// + private sealed class ReloadingRunHelper : WorkflowRunHelper + { + private readonly ReadConfigFromChangeWorkflow _outer; + private readonly Workflow _workflow; + private readonly string _workflowName; + + internal ReloadingRunHelper( + ReadConfigFromChangeWorkflow outer, + Workflow workflow, + string workflowName, + string workdir, + O resolvedRef, + IDestination.IWriter writer, + IOrigin.IReader originReader, + string? rawSourceRef, + Action migrationFinishedMonitor) + : base(workflow, workdir, resolvedRef, originReader, writer, rawSourceRef, + migrationFinishedMonitor) + { + _outer = outer; + _workflow = workflow; + _workflowName = Preconditions.CheckNotNull(workflowName, "workflowName"); + } + + internal override ChangeMigrator GetMigratorForChangeAndWriter( + Change change, IDestination.IWriter writer) + { + Preconditions.CheckNotNull(change); + + // logger.info("Loading configuration for change '%s %s'", change.getRef(), ...); + + Config.Config config = _outer._configLoader.LoadForRevision( + GetConsole(), change.GetRevision()); + // The service config validator already checks that the configuration matches the + // registry, checking that the origin and destination haven't changed. + IReadOnlyList errors = _outer._configValidator + .Validate(config, _workflowName) + .GetErrors(); + ValidationException.CheckCondition( + errors.Count == 0, + "Invalid configuration [ref '%s': %s ]: '%s': \n%s", + change.Ref, + _outer._configLoader.Location(), + _workflowName, + string.Join("\n", errors)); + + IMigration migration = config.GetMigration(_workflowName); + ValidationException.CheckCondition( + migration is Workflow, + "Invalid configuration [ref '%s': %s ]: '%s' is not a workflow", + change.Ref, + _outer._configLoader.Location(), + _workflowName); + var workflowForChange = (Workflow)migration; + IOrigin.IReader newReader = workflowForChange.GetOrigin() + .NewReader(workflowForChange.GetOriginFiles(), workflowForChange.GetAuthoring()); + return new ReloadingChangeMigrator( + _workflow, + workflowForChange, + GetWorkdir(), + newReader, + writer, + GetResolvedRef(), + RawSourceRef, + GetMigrationFinishedMonitor()); + } + } + + private sealed class ReloadingChangeMigrator : ChangeMigrator + { + private readonly Workflow _changeWorkflow; + + internal ReloadingChangeMigrator( + Workflow headWorkflow, + Workflow changeWorkflow, + string workdir, + IOrigin.IReader reader, + IDestination.IWriter writer, + O resolvedRef, + string? rawSourceRef, + Action migrationFinishedMonitor) + : base(headWorkflow, workdir, reader, writer, resolvedRef, rawSourceRef, + migrationFinishedMonitor) + { + _changeWorkflow = Preconditions.CheckNotNull(changeWorkflow); + } + + protected override Workflow GetWorkflow() => _changeWorkflow; + } +} diff --git a/src/Copybara.Core/SkylarkContext.cs b/src/Copybara.Core/SkylarkContext.cs new file mode 100644 index 000000000..9c63835ac --- /dev/null +++ b/src/Copybara.Core/SkylarkContext.cs @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Starlark.Eval; + +namespace Copybara; + +/// A context object that can be enhanced with Skylark information. +public interface ISkylarkContext +{ + /// Create a copy instance with Skylark function parameters. + T WithParams(Dict @params); + + /// Performs tasks after Starlark code finishes. + void OnFinish(object? result, object context); +} diff --git a/src/Copybara.Core/StarlarkDateTimeModule.cs b/src/Copybara.Core/StarlarkDateTimeModule.cs new file mode 100644 index 000000000..7a6b49d74 --- /dev/null +++ b/src/Copybara.Core/StarlarkDateTimeModule.cs @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; +using Starlark.Syntax; + +namespace Copybara; + +/// Starlark wrapper for zoned datetimes. +[StarlarkBuiltin("datetime", Doc = "Module for datetime manipulation.")] +public sealed class StarlarkDateTimeModule : IStarlarkValue +{ + [StarlarkMethod("now", + Doc = "Returns a starlark_datetime object. The object is timezone aware.")] + public StarlarkDateTime CreateFromNow( + [Param(Name = "tz", Named = true, DefaultValue = "'America/Los_Angeles'", + Doc = "The timezone. E.g. America/New_York, Asia/Tokyo, Europe/Rome", + AllowedTypes = new[] { typeof(string) })] + string zoneIdString) => + new(zoneIdString); + + [StarlarkMethod("fromtimestamp", + Doc = + "Returns a starlark_datetime object representation of the epoch time. The object is" + + " timezone aware.")] + public StarlarkDateTime CreateFromEpochSeconds( + [Param(Name = "timestamp", Named = true, DefaultValue = "0", + Doc = "Epoch time in seconds.", + AllowedTypes = new[] { typeof(StarlarkInt) })] + StarlarkInt timeInEpochSeconds, + [Param(Name = "tz", Named = true, DefaultValue = "'America/Los_Angeles'", + Doc = "The timezone. E.g. America/New_York, Asia/Tokyo, Europe/Rome, etc.", + AllowedTypes = new[] { typeof(string) })] + string zoneId) => + new(timeInEpochSeconds.ToLong("timestamp"), zoneId); + + /// The Starlark-facing wrapper for a zoned datetime. + [StarlarkBuiltin("StarlarkDateTime", Doc = "Starlark datetime object")] + public sealed class StarlarkDateTime : IStarlarkValue, IHasBinary + { + private readonly DateTimeOffset _dateTime; + private readonly TimeZoneInfo _zone; + + public StarlarkDateTime(string zoneIdString) + { + _zone = ConvertStringToZone(zoneIdString); + _dateTime = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, _zone); + } + + public StarlarkDateTime(long timeInEpochSeconds, string zoneIdString) + { + _zone = ConvertStringToZone(zoneIdString); + _dateTime = TimeZoneInfo.ConvertTime( + DateTimeOffset.FromUnixTimeSeconds(timeInEpochSeconds), _zone); + } + + public object? BinaryOp(TokenKind op, object that, bool thisLeft) + { + if (that is not StarlarkDateTime otherDateTime) + { + throw new EvalException( + "Binary operators are supported between StarkDateTime objects only."); + } + + switch (op) + { + case TokenKind.MINUS: + // TODO(port): PLUS between StarlarkDatetime and StarlarkTimeDelta in the future. + long seconds = (long)(_dateTime - otherDateTime._dateTime).TotalSeconds; + return new StarlarkTimeDelta(seconds); + default: + throw new EvalException($"Glob does not support {op} operator"); + } + } + + private static TimeZoneInfo ConvertStringToZone(string zoneIdString) + { + try + { + return string.IsNullOrEmpty(zoneIdString) + ? TimeZoneInfo.Local + : TimeZoneInfo.FindSystemTimeZoneById(zoneIdString); + } + catch (Exception e) when (e is TimeZoneNotFoundException or InvalidTimeZoneException) + { + throw new ValidationException( + "An error was thrown creating StarlarkDateTime from zone id. Make sure your" + + " timezone is available in the system tz database", + e); + } + } + + [StarlarkMethod("in_epoch_seconds", + Doc = "Returns the time in epoch seconds for the starlark_datetime instance")] + public long GetTimeInEpochSeconds() => _dateTime.ToUnixTimeSeconds(); + + [StarlarkMethod("strftime", + Doc = + "Returns a string representation of the StarlarkDateTime object with your chosen" + + " formatting")] + public string FormatToString( + [Param(Name = "format", Named = true, + Doc = + "Format string used to present StarlarkDateTime object. See" + + " https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html" + + " for patterns.", + AllowedTypes = new[] { typeof(string) })] + string format) + { + // NOTE(port): upstream uses Java's DateTimeFormatter patterns. This port uses .NET custom + // format strings; the majority of simple patterns behave similarly but complex Java + // patterns may diverge. This is an accepted deviation for the .NET port. + try + { + return _dateTime.ToString(format, System.Globalization.CultureInfo.InvariantCulture); + } + catch (FormatException e) + { + throw new ValidationException( + $"The StarlarkDateTime object '{this}' could not be formatted using format" + + $" string '{format}':", + e); + } + } + + public override string ToString() => + _dateTime.ToString("yyyy-MM-ddTHH:mm:sszzz", + System.Globalization.CultureInfo.InvariantCulture); + + public override bool Equals(object? obj) => + obj is StarlarkDateTime other && _dateTime.Equals(other._dateTime); + + public override int GetHashCode() => _dateTime.GetHashCode(); + } + + /// Time delta, used to do binary operations with Starlark Datetime. + [StarlarkBuiltin("time_delta", Doc = "A time delta.")] + public sealed class StarlarkTimeDelta : IStarlarkValue + { + private readonly TimeSpan _duration; + + public StarlarkTimeDelta(long seconds) + { + _duration = TimeSpan.FromSeconds(seconds); + } + + [StarlarkMethod("total_seconds", + Doc = "Total number of seconds in a timedelta object.")] + public long TotalSeconds() => (long)_duration.TotalSeconds; + + // TODO(port): implement timedelta + StarlarkDatetime. + } +} diff --git a/src/Copybara.Core/StarlarkRandomModule.cs b/src/Copybara.Core/StarlarkRandomModule.cs new file mode 100644 index 000000000..c98bb918d --- /dev/null +++ b/src/Copybara.Core/StarlarkRandomModule.cs @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Starlark.Annot; +using Starlark.Eval; + +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara; + +/// A Starlark module for randomization-related functions. +[StarlarkBuiltin("random", Doc = "A module for randomization-related functions.")] +public sealed class StarlarkRandomModule : IStarlarkValue +{ + private static readonly Random Rng = new(); + + [StarlarkMethod("sample", + Doc = "Returns a list of k unique elements randomly sampled from the list.")] + public StarlarkList SampleStarlarkList( + [Param(Name = "population", Named = true, + Doc = "The list to sample from.", + AllowedTypes = new[] { typeof(StarlarkList) })] + StarlarkList population, + [Param(Name = "k", Named = true, + Doc = "The number of elements to sample from the population list.", + AllowedTypes = new[] { typeof(StarlarkInt) })] + StarlarkInt k) + { + // A StarlarkList might be immutable. Make a mutable copy that we can shuffle and return. + var mutableList = new List(population); + Shuffle(mutableList); + int kInt = k.ToInt("k"); + if (kInt < 0 || kInt > mutableList.Count) + { + throw new EvalException(string.Format( + "k is out of bounds. Must be >= 0 and <= {0}. Current value: {1}", + population.Count, kInt)); + } + + return StarlarkList.ImmutableCopyOf(mutableList.GetRange(0, kInt)); + } + + private static void Shuffle(IList list) + { + for (int i = list.Count - 1; i > 0; i--) + { + int j = Rng.Next(i + 1); + (list[i], list[j]) = (list[j], list[i]); + } + } +} diff --git a/src/Copybara.Core/StructModule.cs b/src/Copybara.Core/StructModule.cs new file mode 100644 index 000000000..2230ad3d4 --- /dev/null +++ b/src/Copybara.Core/StructModule.cs @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara; + +/// struct() constructor. +[StarlarkBuiltin("struct", Doc = "Immutable struct type.")] +public sealed class StructModule : IStarlarkValue +{ + [StarlarkMethod("constructor", + Doc = + "Creates a new immutable struct. Structs with the same keys/values are equal. The " + + "struct's keys and values are passed in as keyword arguments.", + SelfCall = true)] + public StructImpl Create( + [Param(Name = "kwargs", DefaultValue = "{}", Doc = "Dictionary of Args.")] + Dict kwargs) + { + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var e in kwargs.Entries) + { + builder[(string)e.Key!] = e.Value; + } + + return new StructImpl(builder.ToImmutable()); + } + + /// Trivial struct implementation based on an immutable map. + public sealed class StructImpl : IStructure, IStarlarkPrintableValue + { + private readonly ImmutableDictionary _dict; + + public StructImpl(ImmutableDictionary dict) + { + _dict = Preconditions.CheckNotNull(dict); + } + + public object? GetValue(string name) + { + if (!_dict.ContainsKey(name)) + { + throw new EvalException(GetErrorMessageForUnknownField(name)!); + } + + return _dict[name]; + } + + public IReadOnlyCollection GetFieldNames() => _dict.Keys.ToImmutableArray(); + + public string? GetErrorMessageForUnknownField(string field) => + string.Format( + "Field {0} is unknown, available fields are {1}.", + field, string.Join(", ", _dict.Keys)); + + public void Repr(Printer printer, StarlarkSemantics semantics) + { + printer.Append("struct("); + string sep = ""; + foreach (var e in _dict) + { + printer.Append(sep).Append(e.Key).Append('=').Repr(e.Value, semantics); + sep = ", "; + } + + printer.Append(")"); + } + + public bool IsImmutable() => true; + + public override bool Equals(object? other) => + other is StructImpl s && DictEquals(_dict, s._dict); + + public override int GetHashCode() + { + int h = 0; + foreach (var e in _dict) + { + h ^= HashCode.Combine(e.Key, e.Value); + } + + return h; + } + + private static bool DictEquals( + ImmutableDictionary a, ImmutableDictionary b) + { + if (a.Count != b.Count) + { + return false; + } + + foreach (var e in a) + { + if (!b.TryGetValue(e.Key, out var v) || !Equals(e.Value, v)) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Copybara.Core/Transform/CopyMoveRegexVisitor.cs b/src/Copybara.Core/Transform/CopyMoveRegexVisitor.cs new file mode 100644 index 000000000..c95c70eda --- /dev/null +++ b/src/Copybara.Core/Transform/CopyMoveRegexVisitor.cs @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.TemplateToken; +using Copybara.Util; + +namespace Copybara.Transform; + +/// A visitor which copy or moves files recursively from the path it is visiting. +internal sealed class CopyMoveRegexVisitor +{ + private readonly RegexTemplateTokens _before; + private readonly RegexTemplateTokens _after; + private readonly IPathMatcher _pathMatcher; + private readonly string _workDir; + private readonly bool _isCopy; + private readonly bool _overwrite; + + private readonly List _actionsToTake = new(); + + private CopyMoveRegexVisitor( + RegexTemplateTokens before, + RegexTemplateTokens after, + IPathMatcher pathMatcher, + string workDir, + bool overwrite, + bool isCopy) + { + _before = Preconditions.CheckNotNull(before); + _after = Preconditions.CheckNotNull(after); + _pathMatcher = Preconditions.CheckNotNull(pathMatcher); + _workDir = Preconditions.CheckNotNull(workDir); + _isCopy = isCopy; + _overwrite = overwrite; + } + + private void VisitFile(string file) + { + if (_pathMatcher.Matches(file)) + { + string relativeFile = PathOps.Relativize(_workDir, file); + string relativeDest = _before + .CreateReplacer(_after, firstOnly: true, multiline: false, ImmutableArray.Empty) + .Replace(relativeFile); + if (relativeFile.Equals(relativeDest)) + { + // Either the regex didn't match, or it did match but returned the same file name. + return; + } + string dest = PathOps.Resolve(_workDir, relativeDest); + _actionsToTake.Add(new CopyOrMoveAction(this, file, dest)); + } + } + + private void WalkDir(string dir) + { + foreach (var sub in Directory.EnumerateDirectories(dir)) + { + WalkDir(sub); + } + foreach (var file in Directory.EnumerateFiles(dir)) + { + VisitFile(file); + } + // postVisitDirectory: schedule dir deletion for moves. + if (!_isCopy) + { + _actionsToTake.Add(new DeleteDirectoryAction(dir)); + } + } + + public static bool Run( + string root, + RegexTemplateTokens before, + RegexTemplateTokens after, + IPathMatcher pathMatcher, + string workDir, + bool overwrite, + bool isCopy) + { + var visitor = new CopyMoveRegexVisitor(before, after, pathMatcher, workDir, overwrite, isCopy); + visitor.WalkDir(root); + + // Start to execute actions only after we finish walking the tree, to make sure we don't + // copy/move the same file twice. + bool someActionSucceeded = false; + foreach (IAction action in visitor._actionsToTake) + { + someActionSucceeded |= action.Run(); + } + return someActionSucceeded; + } + + private interface IAction + { + bool Run(); + } + + private sealed class CopyOrMoveAction : IAction + { + private readonly CopyMoveRegexVisitor _owner; + private readonly string _file; + private readonly string _dest; + + public CopyOrMoveAction(CopyMoveRegexVisitor owner, string file, string dest) + { + _owner = owner; + _file = file; + _dest = dest; + } + + public bool Run() + { + var destParent = PathOps.GetParent(_dest); + if (destParent != null) + { + Directory.CreateDirectory(destParent); + } + if (File.Exists(_dest)) + { + if (!_owner._overwrite) + { + throw new IOException( + $"Cannot move file to '{_dest}' because it already exists"); + } + File.Delete(_dest); + } + if (_owner._isCopy) + { + File.Copy(_file, _dest, overwrite: _owner._overwrite); + } + else + { + File.Move(_file, _dest); + } + return true; + } + } + + private sealed class DeleteDirectoryAction : IAction + { + private readonly string _dir; + + public DeleteDirectoryAction(string dir) + { + _dir = dir; + } + + public bool Run() + { + try + { + if (Directory.Exists(_dir) && + !Directory.EnumerateFileSystemEntries(_dir).Any()) + { + Directory.Delete(_dir); + return true; + } + return false; + } + catch (IOException) + { + return false; + } + } + } +} diff --git a/src/Copybara.Core/Transform/CopyMoveVisitor.cs b/src/Copybara.Core/Transform/CopyMoveVisitor.cs new file mode 100644 index 000000000..62ed06bff --- /dev/null +++ b/src/Copybara.Core/Transform/CopyMoveVisitor.cs @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Util; + +namespace Copybara.Transform; + +/// A visitor which copy or moves files recursively from the path it is visiting. +internal sealed class CopyMoveVisitor +{ + private readonly string _before; + private readonly string _after; + private readonly IPathMatcher? _pathMatcher; + private readonly bool _isCopy; + private readonly bool _overwrite; + + public CopyMoveVisitor( + string before, string after, IPathMatcher? pathMatcher, bool overwrite, bool isCopy) + { + _before = PathOps.Normalize(before); + _after = PathOps.Normalize(after); + _pathMatcher = pathMatcher; + _isCopy = isCopy; + _overwrite = overwrite; + } + + /// Walks the before tree, copying/moving matched files into after. + public void Walk() + { + if (!Directory.Exists(_before)) + { + // 'before' may be a single file. + if (File.Exists(_before)) + { + VisitFile(_before); + } + return; + } + WalkDir(_before); + } + + private void WalkDir(string dir) + { + // Mirror preVisitDirectory: skip the 'after' subtree. + if (PathOps.Normalize(dir) == _after) + { + return; + } + foreach (var sub in Directory.EnumerateDirectories(dir)) + { + WalkDir(sub); + } + foreach (var file in Directory.EnumerateFiles(dir)) + { + VisitFile(file); + } + } + + private void VisitFile(string source) + { + if (_pathMatcher == null || _pathMatcher.Matches(source)) + { + string relative = PathOps.Relativize(_before, source); + string dest = PathOps.Resolve(_after, relative); + var destParent = PathOps.GetParent(dest); + if (destParent != null) + { + Directory.CreateDirectory(destParent); + } + if (File.Exists(dest)) + { + if (!_overwrite) + { + throw new IOException($"Cannot move file to '{dest}' because it already exists"); + } + File.Delete(dest); + } + if (_isCopy) + { + File.Copy(source, dest, overwrite: _overwrite); + } + else + { + File.Move(source, dest); + } + } + } +} diff --git a/src/Copybara.Core/Transform/CopyOrMove.cs b/src/Copybara.Core/Transform/CopyOrMove.cs new file mode 100644 index 000000000..431893b50 --- /dev/null +++ b/src/Copybara.Core/Transform/CopyOrMove.cs @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.TemplateToken; +using Copybara.Util; +using Starlark.Syntax; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Transform; + +/// Transformation that moves (renames) or copies a single file or directory. +public class CopyOrMove : ITransformation +{ + private readonly RegexTemplateTokens _before; + private readonly RegexTemplateTokens _after; + private readonly Glob _paths; + private readonly ImmutableDictionary _regexGroups; + private readonly bool _overwrite; + private readonly Location _location; + private readonly bool _isCopy; + + private CopyOrMove( + RegexTemplateTokens before, + RegexTemplateTokens after, + IReadOnlyDictionary regexGroups, + Glob paths, + bool overwrite, + Location location, + bool isCopy) + { + _before = Preconditions.CheckNotNull(before); + _after = Preconditions.CheckNotNull(after); + _regexGroups = regexGroups.ToImmutableDictionary(); + _paths = paths; + _overwrite = overwrite; + _location = location; + _isCopy = isCopy; + } + + public static CopyOrMove Create( + string before, + string after, + IReadOnlyDictionary regexGroups, + Glob paths, + bool overwrite, + Location location, + bool isCopy) + { + var parsedRegexGroups = Replace.ParsePatterns(regexGroups); + var beforeTokens = new RegexTemplateTokens( + ValidatePath(before), parsedRegexGroups, repeatedGroups: true, matchExactly: true, location); + beforeTokens.ValidateUnused(); + var afterTokens = new RegexTemplateTokens( + ValidatePath(after), parsedRegexGroups, repeatedGroups: true, matchExactly: true, location); + return new CopyOrMove( + beforeTokens, afterTokens, parsedRegexGroups, paths, overwrite, location, isCopy); + } + + public static CopyOrMove CreateMove( + string before, + string after, + IReadOnlyDictionary regexGroups, + Glob paths, + bool overwrite, + Location location) => + Create(before, after, regexGroups, paths, overwrite, location, isCopy: false); + + public static CopyOrMove CreateCopy( + string before, + string after, + IReadOnlyDictionary regexGroups, + Glob paths, + bool overwrite, + Location location) => + Create(before, after, regexGroups, paths, overwrite, location, isCopy: true); + + public override string ToString() => + $"CopyOrMove{{before={_before}, after={_after}," + + $" regexGroups=[{string.Join(", ", _regexGroups.Keys)}], paths={_paths}," + + $" overwrite={_overwrite}}}"; + + public TransformationStatus Transform(TransformWork work) + { + work.GetConsole().Progress("Moving " + _before); + return _before.IsLiteral() ? TransformNoRegex(work) : TransformWithRegex(work); + } + + private TransformationStatus TransformNoRegex(TransformWork work) + { + string before = PathOps.Normalize(PathOps.Resolve(work.GetCheckoutDir(), _before.ToString())); + if (!File.Exists(before) && !Directory.Exists(before)) + { + return TransformationStatus.Noop( + $"Error moving '{_before}'. It doesn't exist in the workdir"); + } + string after = PathOps.Normalize(PathOps.Resolve(work.GetCheckoutDir(), _after.ToString())); + if (Directory.Exists(after) && PathOps.StartsWith(after, before)) + { + // When moving from a parent dir to a sub-directory, make sure after doesn't already + // have files in it - this is most likely a mistake. + new VerifyDirIsEmptyVisitor( + after, + Directory.Exists(before) && !_paths.Equals(Glob.AllFiles) + ? _paths.RelativeTo(after) + : null) + .Walk(); + } + CreateParentDirs(after); + + bool beforeIsDir = Directory.Exists(before); + ValidationException.CheckCondition( + _paths.Equals(Glob.AllFiles) || beforeIsDir, + "Cannot use user defined 'paths' filter when the 'before' is not a directory: " + _paths); + ValidationException.CheckCondition( + !_after.IsEmpty() || beforeIsDir, + "Can only move a path to the root when the path is a folder. But '{0}' is a file. Use" + + " instead core.move('{1}', '{2}')", + _before, _before, PathOps.GetFileName(before)); + + // Simple move of all the contents of a directory. + if (beforeIsDir && !_isCopy && _paths.Equals(Glob.AllFiles)) + { + MoveAllFilesInDir(before, after, work.GetCheckoutDir()); + return TransformationStatus.Success(); + } + + new CopyMoveVisitor( + before, after, beforeIsDir ? _paths.RelativeTo(before) : null, _overwrite, _isCopy) + .Walk(); + + // Delete 'before' folder if we moved all the files. + if (beforeIsDir && !_isCopy) + { + RecursiveDeleteIfEmpty(before); + } + return TransformationStatus.Success(); + } + + private TransformationStatus TransformWithRegex(TransformWork work) + { + // Optimize by only visiting files within rootPath. + string rootPath = PathOps.Normalize(PathOps.Resolve(work.GetCheckoutDir(), GetRoot(_before))); + if (!Directory.Exists(rootPath)) + { + return TransformationStatus.Noop( + "Transformation '" + this + "' was a no-op because it didn't match any file"); + } + bool atLeastOneFileMatched = CopyMoveRegexVisitor.Run( + rootPath, _before, _after, _paths.RelativeTo(rootPath), work.GetCheckoutDir(), + _overwrite, _isCopy); + if (!atLeastOneFileMatched) + { + return TransformationStatus.Noop( + "Transformation '" + this + "' was a no-op because it didn't match any file"); + } + return TransformationStatus.Success(); + } + + private static string GetRoot(RegexTemplateTokens templateTokens) + { + var tokens = templateTokens.GetTokens(); + if (tokens.Count == 0) + { + return ""; + } + if (tokens.Count == 1) + { + Token token = tokens[0]; + return token.GetTokenType() == TokenType.Literal ? token.GetValue() : ""; + } + Token first = tokens[0]; + string prefix = first.GetTokenType() == TokenType.Literal ? first.GetValue() : ""; + if (prefix.Contains('/')) + { + return prefix.Substring(0, prefix.LastIndexOf('/')); + } + return ""; + } + + /// Traverse a directory files/folders recursively and delete any empty folder. + private static void RecursiveDeleteIfEmpty(string dir) + { + if (!Directory.Exists(dir)) + { + return; + } + foreach (var sub in Directory.EnumerateDirectories(dir)) + { + RecursiveDeleteIfEmpty(sub); + } + if (!Directory.EnumerateFileSystemEntries(dir).Any()) + { + try + { + Directory.Delete(dir); + } + catch (IOException) + { + // Folder not empty. Ignore. + } + } + } + + /// + /// Move all the files and directories inside to + /// . + /// + private void MoveAllFilesInDir(string before, string after, string checkoutDir) + { + var beforeFiles = ListDirFiles(before); + string tmp = Directory.CreateTempSubdirectory("core.move").FullName; + // Ensure tmp is inside the checkout so relative moves behave; mirror createTempDirectory in + // checkoutDir by moving into a subdir of checkoutDir. + string tmpInCheckout = PathOps.Resolve(checkoutDir, "core.move" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tmpInCheckout); + Directory.Delete(tmp); + tmp = tmpInCheckout; + foreach (string file in beforeFiles) + { + string name = PathOps.GetFileName(file); + string target = PathOps.Resolve(tmp, name); + if (Directory.Exists(file)) + { + Directory.Move(file, target); + } + else + { + File.Move(file, target); + } + } + + if (!PathOps.Normalize(checkoutDir).Equals(PathOps.Normalize(before))) + { + RecursiveDeleteIfEmpty(before); + } + + // If directory exists after the move to tmp, it can contain files. + if (!Directory.Exists(after)) + { + var afterParent = PathOps.GetParent(after); + if (afterParent != null) + { + Directory.CreateDirectory(afterParent); + } + Directory.Move(tmp, after); + return; + } + + // Use our less-efficient move per file. + new CopyMoveVisitor(tmp, after, pathMatcher: null, _overwrite, isCopy: false).Walk(); + + RecursiveDeleteIfEmpty(tmp); + } + + private static List ListDirFiles(string before) => + Directory.EnumerateFileSystemEntries(before).ToList(); + + public ITransformation Reverse() + { + if (!_before.IsLiteral()) + { + throw new NonReversibleValidationException( + "core." + (_isCopy ? "copy" : "move") + + "() with regex templating is not automatically reversible. Use core.transform" + + " to define an explicit reverse"); + } + if (_overwrite) + { + throw new NonReversibleValidationException( + "core." + (_isCopy ? "copy" : "move") + + "() with overwrite set is not automatically reversible. Use core.transform to" + + " define an explicit reverse"); + } + if (_isCopy) + { + string afterPath = PathOps.Normalize(_after.ToString()); + if (!_paths.Equals(Glob.AllFiles)) + { + throw new NonReversibleValidationException( + "core.copy not automatically reversible when using 'paths'"); + } + if (_after.IsEmpty() || + PathOps.StartsWith(PathOps.Normalize(_before.ToString()), afterPath)) + { + throw new NonReversibleValidationException( + "core.copy not automatically reversible when copying to a parent directory"); + } + return new ExplicitReversal( + new Remove( + // After might be a directory or a file. Delete both. + Glob.CreateGlob(ImmutableArray.Create(_after.ToString(), afterPath + "/**")), + _location), + this); + } + return new CopyOrMove( + _after, _before, _regexGroups, _paths, overwrite: false, _location, isCopy: false); + } + + private static void CreateParentDirs(string after) + { + try + { + var parent = PathOps.GetParent(after); + if (parent != null) + { + Directory.CreateDirectory(parent); + } + } + catch (IOException e) + { + throw new ValidationException( + $"Cannot create '{PathOps.GetParent(after)}' because a path component already exists" + + $" and is not a directory: {e.Message}"); + } + } + + public string Describe() => (_isCopy ? "Copying " : "Moving ") + _before; + + private static string ValidatePath(string strPath) + { + try + { + return FileUtil.CheckNormalizedRelative(strPath); + } + catch (ArgumentException e) + { + throw StarlarkRt.Errorf("'{0}' is not a valid path: {1}", strPath, e.Message); + } + } + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Debug/DebugOptions.cs b/src/Copybara.Core/Transform/Debug/DebugOptions.cs new file mode 100644 index 000000000..29ed98816 --- /dev/null +++ b/src/Copybara.Core/Transform/Debug/DebugOptions.cs @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Util; + +namespace Copybara.Transform.Debug; + +/// Workflow debugging tools. +public class DebugOptions : IOption +{ + [Flag("--debug-metadata-break", "Stop when message and/or author changes")] + public bool DebugMetadataBreak { get; set; } = false; + + [Flag("--debug-file-break", "Stop when file matching the glob changes")] + public string? DebugFileBreak { get; set; } = null; + + [Flag("--debug-transform-break", "Stop when transform description matches")] + public string? DebugTransformBreak { get; set; } = null; + + private readonly GeneralOptions _generalOptions; + + public DebugOptions(GeneralOptions generalOptions) + { + _generalOptions = Preconditions.CheckNotNull(generalOptions); + } + + internal Glob? GetDebugFileBreak() => + DebugFileBreak != null + ? Glob.CreateGlob(ImmutableArray.Create(DebugFileBreak)) + : null; + + internal Regex? GetDebugTransformBreak() => + DebugTransformBreak != null ? new Regex(DebugTransformBreak) : null; + + public ITransformation TransformWrapper(ITransformation transformation) + { + if (!DebuggerEnabled()) + { + return transformation; + } + return TransformDebug.WithDebugger( + transformation, this, _generalOptions.GetEnvironment()); + } + + private bool DebuggerEnabled() => + DebugMetadataBreak + || DebugFileBreak != null + || DebugTransformBreak != null; + + internal string CreateDiffDirectory() => + _generalOptions.GetDirFactory().NewTempDir("debug"); +} diff --git a/src/Copybara.Core/Transform/Debug/TransformDebug.cs b/src/Copybara.Core/Transform/Debug/TransformDebug.cs new file mode 100644 index 000000000..d05143dd7 --- /dev/null +++ b/src/Copybara.Core/Transform/Debug/TransformDebug.cs @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; +using Starlark.Syntax; +using Copybara.Util.Console; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Transform.Debug; + +/// +/// A transformation that delegates to another transformation and allows debugging its execution. +/// +public sealed class TransformDebug : ITransformation +{ + private const string CopybaraMetadataFakeFile = + " Copybara metadata(Author, description, etc.)"; + + private readonly ITransformation _delegate; + private readonly DebugOptions _debugOptions; + private readonly IReadOnlyDictionary _environment; + + private TransformDebug( + ITransformation delegateTransform, + DebugOptions debugOptions, + IReadOnlyDictionary environment) + { + _delegate = Preconditions.CheckNotNull(delegateTransform); + _debugOptions = Preconditions.CheckNotNull(debugOptions); + _environment = Preconditions.CheckNotNull(environment); + } + + public TransformationStatus Transform(TransformWork work) + { + Console console = work.GetConsole(); + bool fileDebug = _debugOptions.GetDebugFileBreak() != null; + bool metadataDebug = _debugOptions.DebugMetadataBreak; + + Regex? debugTransformBreak = _debugOptions.GetDebugTransformBreak(); + bool transformMatch = + debugTransformBreak != null && debugTransformBreak.IsMatch(_delegate.Describe()); + + if (!fileDebug && !metadataDebug && !transformMatch) + { + // Nothing to debug! + return _delegate.Transform(work); + } + + var before = ReadState(work, fileDebug || transformMatch, work.GetTreeState()); + TransformationStatus status = _delegate.Transform(work); + work.ValidateTreeStateCache(); + var after = ReadState(work, fileDebug || transformMatch, work.GetTreeState()); + + var difference = new MapDifference(before, after); + + bool stop = transformMatch; + if (fileDebug) + { + IPathMatcher debugFileBreak = _debugOptions.GetDebugFileBreak()!.RelativeTo("/"); + foreach (string path in difference.OnlyOnLeft.Keys + .Concat(difference.OnlyOnRight.Keys) + .Concat(difference.Differing.Keys)) + { + if (path.Equals(CopybaraMetadataFakeFile)) + { + continue; + } + if (debugFileBreak.Matches("/" + path)) + { + stop = true; + console.InfoFmt("File '{0}' change matched. Stopping", path); + break; + } + } + } + else if (metadataDebug + && !ByteArraysEqual( + before.GetValueOrDefault(CopybaraMetadataFakeFile), + after.GetValueOrDefault(CopybaraMetadataFakeFile))) + { + stop = true; + console.InfoFmt("Message, author and/or labels changed"); + } + + if (!stop) + { + return status; + } + if (!transformMatch) + { + // Stopped because of file/metadata change. Show the diff directly. + ShowDiff(console, difference); + } + while (true) + { + string answer = console.Ask( + "Debugger stopped after '" + _delegate.Describe() + "' " + + console.Colorize(AnsiColor.Purple, _delegate.Location().ToString()) + ".\n" + + " Current file state can be checked at " + work.GetCheckoutDir() + "\n" + + "Diff (d), Continue (c), Stop (s): ", + "d", + input => input is "d" or "c" or "s"); + + switch (answer) + { + case "d": + ShowDiff(console, difference); + break; + case "c": + return status; + case "s": + throw new ValidationException("Stopped by user"); + } + } + } + + private void ShowDiff(Console console, MapDifference difference) + { + if (difference.AreEqual()) + { + console.Info("No changes detected"); + return; + } + string debug = _debugOptions.CreateDiffDirectory(); + FileUtil.DeleteRecursively(debug); + Directory.CreateDirectory(debug); + string beforePath = Path.Combine(debug, "before"); + Directory.CreateDirectory(beforePath); + string afterPath = Path.Combine(debug, "after"); + Directory.CreateDirectory(afterPath); + + foreach (var entry in difference.OnlyOnLeft) + { + WriteFile(beforePath, entry.Key, entry.Value); + } + foreach (var entry in difference.OnlyOnRight) + { + WriteFile(afterPath, entry.Key, entry.Value); + } + foreach (var entry in difference.Differing) + { + WriteFile(beforePath, entry.Key, entry.Value.Left); + WriteFile(afterPath, entry.Key, entry.Value.Right); + } + + try + { + console.Info( + DiffUtil.Colorize( + console, + Encoding.UTF8.GetString( + DiffUtil.Diff(beforePath, afterPath, verbose: false, _environment)))); + } + catch (InsideGitDirException e) + { + throw new ValidationException( + "Cannot debug if temporary directory is inside a git directory", e); + } + } + + private static void WriteFile(string basePath, string path, byte[] content) + { + string target = Path.Combine(basePath, path); + string? parent = Path.GetDirectoryName(target); + if (parent != null) + { + Directory.CreateDirectory(parent); + } + File.WriteAllBytes(target, content); + } + + private static SortedDictionary ReadState( + TransformWork work, bool filesNeeded, TreeState.TreeState treeState) + { + var result = new SortedDictionary(StringComparer.Ordinal) + { + [CopybaraMetadataFakeFile] = Encoding.UTF8.GetBytes(work.GetMetadata().ToString()), + }; + + if (filesNeeded) + { + var files = treeState.Find(Glob.AllFiles.RelativeTo(work.GetCheckoutDir())); + foreach (var beforeFile in files) + { + string filePath = beforeFile.GetPath(); + // Ignore symlinks. + var info = new FileInfo(filePath); + if (info.LinkTarget != null) + { + continue; + } + string relative = Path.GetRelativePath(work.GetCheckoutDir(), filePath); + byte[] bytes = File.ReadAllBytes(filePath); + result[relative] = bytes.Length > 100_000 + ? Encoding.UTF8.GetBytes( + "File too big. Hash: " + Convert.ToHexString(SHA256.HashData(bytes))) + : bytes; + } + } + return result; + } + + /// Returns the inner transformation. + public ITransformation GetDelegate() => _delegate; + + public ITransformation Reverse() => + new TransformDebug(_delegate.Reverse(), _debugOptions, _environment); + + public string Describe() => _delegate.Describe(); + + public bool CanJoin(ITransformation transformation) => false; + + public ITransformation Join(ITransformation next) => + throw new InvalidOperationException( + $"Debugger doesn't support join!: delegate = {_delegate}, next = {next}"); + + public Location Location() => _delegate.Location(); + + internal static ITransformation WithDebugger( + ITransformation t, + DebugOptions debugOptions, + IReadOnlyDictionary environment) => + t is TransformDebug ? t : new TransformDebug(t, debugOptions, environment); + + private static bool ByteArraysEqual(byte[]? one, byte[]? other) + { + if (ReferenceEquals(one, other)) + { + return true; + } + if (one == null || other == null) + { + return false; + } + return one.AsSpan().SequenceEqual(other); + } + + /// + /// A minimal replacement for Guava's MapDifference over byte-array-valued maps. + /// + private sealed class MapDifference + { + public SortedDictionary OnlyOnLeft { get; } = new(StringComparer.Ordinal); + public SortedDictionary OnlyOnRight { get; } = new(StringComparer.Ordinal); + + public SortedDictionary Differing { get; } = + new(StringComparer.Ordinal); + + public MapDifference( + SortedDictionary left, SortedDictionary right) + { + foreach (var entry in left) + { + if (!right.TryGetValue(entry.Key, out var rightValue)) + { + OnlyOnLeft[entry.Key] = entry.Value; + } + else if (!ByteArraysEqual(entry.Value, rightValue)) + { + Differing[entry.Key] = (entry.Value, rightValue); + } + } + foreach (var entry in right) + { + if (!left.ContainsKey(entry.Key)) + { + OnlyOnRight[entry.Key] = entry.Value; + } + } + } + + public bool AreEqual() => + OnlyOnLeft.Count == 0 && OnlyOnRight.Count == 0 && Differing.Count == 0; + } +} diff --git a/src/Copybara.Core/Transform/ExplicitReversal.cs b/src/Copybara.Core/Transform/ExplicitReversal.cs new file mode 100644 index 000000000..f631523ae --- /dev/null +++ b/src/Copybara.Core/Transform/ExplicitReversal.cs @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Starlark.Syntax; + +namespace Copybara.Transform; + +/// +/// A transformation which delegates to some arbitrary transformation and reverses to some arbitrary +/// transformation. +/// +public sealed class ExplicitReversal : ITransformation +{ + private readonly ITransformation _forward; + private readonly ITransformation _reverse; + + public ExplicitReversal(ITransformation forward, ITransformation reverse) + { + _forward = Preconditions.CheckNotNull(forward); + _reverse = Preconditions.CheckNotNull(reverse); + } + + public TransformationStatus Transform(TransformWork work) + { + TransformWork newWork = work.InsideExplicitTransform(); + TransformationStatus status = _forward.Transform(newWork); + work.UpdateFrom(newWork); + return status; + } + + /// Returns the forward transformation, for introspection and config validation. + public ITransformation GetForward() => _forward; + + /// Returns the reverse transformation, for introspection and config validation. + public ITransformation GetReverse() => _reverse; + + public ITransformation Reverse() => new ExplicitReversal(_reverse, _forward); + + public string Describe() => _forward.Describe(); + + public Location Location() => _forward.Location(); + + public override string ToString() => $"ExplicitReversal{{forward={_forward}, reverse={_reverse}}}"; +} diff --git a/src/Copybara.Core/Transform/FilterReplace.cs b/src/Copybara.Core/Transform/FilterReplace.cs new file mode 100644 index 000000000..29d3b1c3b --- /dev/null +++ b/src/Copybara.Core/Transform/FilterReplace.cs @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Exceptions; +using Copybara.TreeState; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Syntax; +using FileState = Copybara.TreeState.TreeState.FileState; + +namespace Copybara.Transform; + +// Module needed because both Transformation and ReversibleFunction are Starlark objects but neither +// of them extend each other. + +/// A core.filter_replace transformation. +[StarlarkBuiltin("filter_replace", Doc = "A core.filter_replace transformation")] +public class FilterReplace : ITransformation, IReversibleFunction +{ + private readonly WorkflowOptions _workflowOptions; + private readonly Regex _before; + private readonly Regex? _after; + private readonly int _group; + private readonly int _reverseGroup; + private readonly IReversibleFunction _mapping; + private readonly Glob _glob; + private readonly Location _location; + + public FilterReplace( + WorkflowOptions workflowOptions, + Regex before, + Regex? after, + int group, + int reverseGroup, + IReversibleFunction mapping, + Glob glob, + Location location) + { + _workflowOptions = workflowOptions; + _before = before; + _after = after; + _group = group; + _reverseGroup = reverseGroup; + _mapping = mapping; + _glob = glob; + _location = location; + } + + public TransformationStatus Transform(TransformWork work) + { + string checkoutDir = work.GetCheckoutDir(); + + var files = work.GetTreeState().Find(_glob.RelativeTo(checkoutDir)).ToList(); + var batchReplace = new BatchReplace(this); + _workflowOptions.Parallelizer().Run(files, batchReplace); + var changed = batchReplace.GetChanged(); + bool matchedFile = batchReplace.MatchedFile; + + work.GetTreeState().NotifyModify(changed); + if (changed.Count == 0) + { + return TransformationStatus.Noop( + "Transformation '" + ToString() + "' was a no-op because it didn't " + + (matchedFile ? "change any of the matching files" : "match any file")); + } + return TransformationStatus.Success(); + } + + public ITransformation Reverse() => InternalReverse(); + + private FilterReplace InternalReverse() + { + if (_after == null) + { + throw new NonReversibleValidationException("No 'after' defined"); + } + + return new FilterReplace( + _workflowOptions, _after, _before, _reverseGroup, _group, _mapping.ReverseMapping(), + _glob, _location); + } + + public string Describe() => "Nested replaceString"; + + public Location Location() => _location; + + public string Apply(string s) => ReplaceString(s); + + public IReversibleFunction ReverseMapping() => InternalReverse(); + + private sealed class BatchReplace : LocalParallelizer.TransformFunc + { + private readonly FilterReplace _owner; + private readonly List _changed = new(); + private readonly object _lock = new(); + public bool MatchedFile { get; private set; } + + public BatchReplace(FilterReplace owner) + { + _owner = owner; + } + + public List GetChanged() => _changed; + + public bool Run(IEnumerable elements) + { + var changed = new List(); + bool matchedFile = false; + foreach (FileState file in elements) + { + var fileInfo = new FileInfo(file.GetPath()); + if (fileInfo.LinkTarget != null) + { + continue; + } + matchedFile = true; + string originalContent = Encoding.UTF8.GetString(File.ReadAllBytes(file.GetPath())); + string transformed = _owner.ReplaceString(originalContent); + // ReplaceString returns the same instance if no replacement happens. This avoids + // comparing the whole file content. + if (ReferenceEquals(transformed, originalContent)) + { + continue; + } + changed.Add(file); + File.WriteAllBytes(file.GetPath(), Encoding.UTF8.GetBytes(transformed)); + } + + lock (_lock) + { + MatchedFile |= matchedFile; + _changed.AddRange(changed); + } + // We cannot return null here. + return true; + } + } + + private string ReplaceString(string originalContent) + { + bool anyReplace = false; + var result = new StringBuilder(originalContent.Length); + int lastAppend = 0; + foreach (Match matcher in _before.Matches(originalContent)) + { + Group g = matcher.Groups[_group]; + // Append text between the previous match end and this match start. + result.Append(originalContent, lastAppend, matcher.Index - lastAppend); + if (!g.Success) + { + result.Append(matcher.Value); + lastAppend = matcher.Index + matcher.Length; + continue; + } + string val = g.Value; + string res = _mapping.Apply(val); + anyReplace |= !val.Equals(res); + if (_group == 0) + { + result.Append(res); + } + else + { + string prefix = originalContent.Substring(matcher.Index, g.Index - matcher.Index); + string suffix = originalContent.Substring( + g.Index + g.Length, matcher.Index + matcher.Length - (g.Index + g.Length)); + result.Append(prefix).Append(res).Append(suffix); + } + lastAppend = matcher.Index + matcher.Length; + } + + if (!anyReplace) + { + return originalContent; + } + + result.Append(originalContent, lastAppend, originalContent.Length - lastAppend); + return result.ToString(); + } + + public override string ToString() => $"FilterReplace{{before={_before}}}"; +} diff --git a/src/Copybara.Core/Transform/IReversibleFunction.cs b/src/Copybara.Core/Transform/IReversibleFunction.cs new file mode 100644 index 000000000..a9209f3b0 --- /dev/null +++ b/src/Copybara.Core/Transform/IReversibleFunction.cs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Transform; + +/// A function that given an object can map to another object. +[StarlarkBuiltin( + "mapping_function", + Doc = "A function that given an object can map to another object")] +public interface IReversibleFunction : IStarlarkValue +{ + /// Applies the function to the given input. + TOut Apply(TIn input); + + /// Create a reverse of the function. + /// if the mapping is not reversible. + IReversibleFunction ReverseMapping(); +} diff --git a/src/Copybara.Core/Transform/IntentionalNoop.cs b/src/Copybara.Core/Transform/IntentionalNoop.cs new file mode 100644 index 000000000..1200a7ef8 --- /dev/null +++ b/src/Copybara.Core/Transform/IntentionalNoop.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Transform; + +/// +/// A transformation which does nothing. This is a no-op which is not considered an error (unlike a +/// core.move which has no matching files for the 'before' path), so it does not generate a warning. +/// +public sealed class IntentionalNoop : ITransformation +{ + public static readonly IntentionalNoop Instance = new(); + + private IntentionalNoop() + { + } + + public TransformationStatus Transform(TransformWork work) => TransformationStatus.Success(); + + public ITransformation Reverse() => this; + + public string Describe() => "no-op"; +} diff --git a/src/Copybara.Core/Transform/Metadata/ExposeLabelInMessage.cs b/src/Copybara.Core/Transform/Metadata/ExposeLabelInMessage.cs new file mode 100644 index 000000000..42000a10a --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/ExposeLabelInMessage.cs @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Transform; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// +/// Given a label that is not present in the change message but it is in the changes metadata, expose +/// it as a text label. +/// +public class ExposeLabelInMessage : ITransformation +{ + private readonly string _label; + private readonly string _newLabelName; + private readonly string _separator; + private readonly bool _ignoreNotFound; + private readonly bool _all; + private readonly string? _joiner; + private readonly Location _location; + + internal ExposeLabelInMessage( + string label, + string newLabelName, + string separator, + bool ignoreNotFound, + bool all, + string? joiner, + Location location) + { + _label = Preconditions.CheckNotNull(label); + _newLabelName = Preconditions.CheckNotNull(newLabelName); + _separator = Preconditions.CheckNotNull(separator); + _ignoreNotFound = ignoreNotFound; + _all = all; + _joiner = joiner; + _location = Preconditions.CheckNotNull(location); + } + + public TransformationStatus Transform(TransformWork work) + { + if (_all) + { + return ExposeAllLabels(work); + } + + string? value = work.GetLabel(_label); + if (value == null) + { + ValidationException.CheckCondition(_ignoreNotFound, "Cannot find label {0}", _label); + return TransformationStatus.Success(); + } + if (_label.Equals(_newLabelName)) + { + work.RemoveLabelWithValue(_label, value, wholeMessage: true); + } + work.AddLabel(_newLabelName, value, _separator, hidden: false); + return TransformationStatus.Success(); + } + + private TransformationStatus ExposeAllLabels(TransformWork work) + { + // Preserve insertion order and de-duplicate, like a LinkedHashSet. + var values = new List(); + var seen = new HashSet(); + foreach (var value in work.GetAllLabels(_label)) + { + if (seen.Add(value)) + { + values.Add(value); + } + } + + if (values.Count == 0) + { + ValidationException.CheckCondition(_ignoreNotFound, "Cannot find label {0}", _label); + return TransformationStatus.Success(); + } + + // If the label name is the same, we remove it and add it at the end, since the format + // of the message will be more consistent. + if (_label.Equals(_newLabelName)) + { + // Remove the old label since we want it with a different name/separator. + work.RemoveLabel(_label, wholeMessage: true); + } + if (_joiner != null) + { + work.AddLabel(_newLabelName, string.Join(_joiner, values), _separator, hidden: false); + } + else + { + foreach (var value in values) + { + work.AddLabel(_newLabelName, value, _separator, hidden: false); + } + } + + return TransformationStatus.Success(); + } + + public ITransformation Reverse() => + new ExplicitReversal(IntentionalNoop.Instance, this); + + public string Describe() => $"Exposing label {_label} as {_newLabelName}"; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Metadata/MapAuthor.cs b/src/Copybara.Core/Transform/Metadata/MapAuthor.cs new file mode 100644 index 000000000..6b880440e --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/MapAuthor.cs @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Transform; +using Starlark.Eval; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// Map authors between revision systems. +public class MapAuthor : ITransformation +{ + // Author only uses mail for comparison. + private readonly ImmutableDictionary _authorToAuthor; + private readonly ImmutableDictionary _mailToAuthor; + private readonly ImmutableDictionary _nameToAuthor; + private readonly bool _reversible; + private readonly bool _noopReverse; + private readonly bool _failIfNotFound; + private readonly bool _failIfNotFoundInReverse; + private readonly bool _mapAll; + private readonly Location _location; + + private MapAuthor( + Location location, + ImmutableDictionary authorToAuthor, + ImmutableDictionary mailToAuthor, + ImmutableDictionary nameToAuthor, + bool reversible, + bool noopReverse, + bool failIfNotFound, + bool failIfNotFoundInReverse, + bool mapAll) + { + _location = Preconditions.CheckNotNull(location); + _authorToAuthor = Preconditions.CheckNotNull(authorToAuthor); + _mailToAuthor = Preconditions.CheckNotNull(mailToAuthor); + _nameToAuthor = Preconditions.CheckNotNull(nameToAuthor); + _reversible = reversible; + _noopReverse = noopReverse; + _failIfNotFound = failIfNotFound; + _failIfNotFoundInReverse = failIfNotFoundInReverse; + _mapAll = mapAll; + } + + public static MapAuthor Create( + Location location, + IReadOnlyDictionary authorMap, + bool reversible, + bool noopReverse, + bool failIfNotFound, + bool failIfNotFoundInReverse, + bool mapAll) + { + var authorToAuthor = ImmutableDictionary.CreateBuilder(); + var mailToAuthor = ImmutableDictionary.CreateBuilder(); + var nameToAuthor = ImmutableDictionary.CreateBuilder(); + + foreach (var e in authorMap) + { + Author to = Author.Parse(e.Value); + try + { + authorToAuthor.Add(AuthorParser.Parse(e.Key).ToString(), to.ToString()); + } + catch (InvalidAuthorException) + { + if (e.Key.Contains('@')) + { + mailToAuthor.Add(e.Key, to); + } + else + { + nameToAuthor.Add(e.Key, to); + } + } + } + + return new MapAuthor( + location, + authorToAuthor.ToImmutable(), + mailToAuthor.ToImmutable(), + nameToAuthor.ToImmutable(), + reversible, + noopReverse, + failIfNotFound, + failIfNotFoundInReverse, + mapAll); + } + + public TransformationStatus Transform(TransformWork work) + { + work.SetAuthor(GetMappedAuthor(work.GetAuthor())); + + if (_mapAll) + { + foreach (var changeObj in work.GetChanges().GetCurrent()) + { + var current = (Change)changeObj; + current.SetMappedAuthor(GetMappedAuthor(current.GetAuthor())); + } + } + + return TransformationStatus.Success(); + } + + private Author GetMappedAuthor(Author originalAuthor) + { + if (_authorToAuthor.TryGetValue(originalAuthor.ToString(), out var newAuthor)) + { + try + { + return AuthorParser.Parse(newAuthor); + } + catch (InvalidAuthorException e) + { + throw new InvalidOperationException("Shouldn't happen. We validate before", e); + } + } + if (_mailToAuthor.TryGetValue(originalAuthor.Email, out var byMail)) + { + return byMail; + } + if (_nameToAuthor.TryGetValue(originalAuthor.Name, out var byName)) + { + return byName; + } + ValidationException.CheckCondition( + !_failIfNotFound, "Cannot find a mapping for author '{0}'", originalAuthor); + return originalAuthor; + } + + public ITransformation Reverse() + { + if (_noopReverse) + { + return new ExplicitReversal(IntentionalNoop.Instance, this); + } + if (!_reversible) + { + throw new NonReversibleValidationException( + "Author mapping doesn't have reversible enabled"); + } + if (!_mailToAuthor.IsEmpty) + { + throw new NonReversibleValidationException( + "author mapping is not reversible because it contains mail -> author mappings." + + " Only author -> author is reversible: " + + FormatMap(_nameToAuthor)); + } + if (!_nameToAuthor.IsEmpty) + { + throw new NonReversibleValidationException( + "author mapping is not reversible because it contains name -> author mappings." + + " Only author -> author is reversible: " + + FormatMap(_nameToAuthor)); + } + + var reverse = ImmutableDictionary.CreateBuilder(); + foreach (var kv in _authorToAuthor) + { + if (reverse.ContainsKey(kv.Value)) + { + throw new NonReversibleValidationException( + "non-reversible author map: value '" + kv.Value + "' mapped more than once"); + } + reverse.Add(kv.Value, kv.Key); + } + return new MapAuthor( + _location, + reverse.ToImmutable(), + ImmutableDictionary.Empty, + ImmutableDictionary.Empty, + _reversible, + _noopReverse, + _failIfNotFoundInReverse, + _failIfNotFound, + _mapAll); + } + + private static string FormatMap(ImmutableDictionary map) => + "{" + string.Join(", ", map.Select(kv => $"{kv.Key}={kv.Value}")) + "}"; + + public string Describe() => "Mapping authors"; + + public Location Location() => _location; + + public override string ToString() => + $"MapAuthor{{authorToAuthor={FormatMap(_authorToAuthor)}, mailToAuthor=" + + $"{FormatMap(_mailToAuthor)}, nameToAuthor={FormatMap(_nameToAuthor)}, " + + $"reversible={_reversible}, failIfNotFound={_failIfNotFound}, " + + $"failIfNotFoundInReverse={_failIfNotFoundInReverse}, location={_location}}}"; +} diff --git a/src/Copybara.Core/Transform/Metadata/MetadataModule.cs b/src/Copybara.Core/Transform/Metadata/MetadataModule.cs new file mode 100644 index 000000000..f32511724 --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/MetadataModule.cs @@ -0,0 +1,634 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Starlark.Annot; +using Starlark.Eval; +using Starlark.Syntax; +using StarlarkRt = Starlark.Eval.Starlark; +using Sequence = Starlark.Eval.Sequence; + +namespace Copybara.Transform.Metadata; + +/// +/// Metadata module for manipulating metadata of the changes. This is intended to be used by users, +/// for example: +/// +/// +/// metadata_transformations = [ +/// metadata.squash_notes( +/// prefix = 'Import of Foo project:\n', +/// ), +/// ] +/// +/// +[StarlarkBuiltin("metadata", Doc = "Core transformations for the change metadata")] +public class MetadataModule : IStarlarkValue +{ + [StarlarkMethod( + "squash_notes", + Doc = + "Generate a message that includes a constant prefix text and a list of changes" + + " included in the squash change.", + UseStarlarkThread = true)] + public ITransformation SquashNotes( + [Param( + Name = "prefix", + Named = true, + Doc = "A prefix to be printed before the list of commits.", + DefaultValue = "'Copybara import of the project:\\n\\n'")] + string prefix, + [Param( + Name = "max", + Named = true, + Doc = "Max number of commits to include in the message. For the rest a comment like (and" + + " x more) will be included. By default 100 commits are included.", + DefaultValue = "100")] + StarlarkInt max, + [Param( + Name = "compact", + Named = true, + Doc = "If compact is set, each change will be shown in just one line", + DefaultValue = "True")] + bool compact, + [Param( + Name = "show_ref", + Named = true, + Doc = "If each change reference should be present in the notes", + DefaultValue = "True")] + bool showRef, + [Param( + Name = "show_author", + Named = true, + Doc = "If each change author should be present in the notes", + DefaultValue = "True")] + bool showAuthor, + [Param( + Name = "show_description", + Named = true, + Doc = "If each change description should be present in the notes", + DefaultValue = "True")] + bool showDescription, + [Param( + Name = "oldest_first", + Named = true, + Doc = "If set to true, the list shows the oldest changes first. Otherwise it shows the" + + " changes in descending order.", + DefaultValue = "False")] + bool oldestFirst, + [Param( + Name = "use_merge", + Named = true, + Doc = "If true then merge changes are included in the squash notes", + DefaultValue = "True", + Positional = false)] + bool useMerge, + StarlarkThread thread) => + new MetadataSquashNotes( + CheckNotEmpty(prefix, "prefix"), + max.ToInt("max"), + compact, + showRef, + showAuthor, + showDescription, + oldestFirst, + useMerge, + thread.GetCallerLocation()); + + [StarlarkMethod( + "save_author", + Doc = + "For a given change, store a copy of the author as a label with the name" + + " ORIGINAL_AUTHOR.", + UseStarlarkThread = true)] + public ITransformation SaveAuthor( + [Param( + Name = "label", + Named = true, + Doc = "The label to use for storing the author", + DefaultValue = "'ORIGINAL_AUTHOR'")] + string label, + [Param( + Name = "separator", + Named = true, + Doc = "The separator to use between the label and the value", + DefaultValue = "\"=\"")] + string separator, + StarlarkThread thread) => + new SaveOriginalAuthor(label, separator, thread.GetCallerLocation()); + + [StarlarkMethod( + "map_author", + Doc = + "Map the author name and mail to another author. The mapping can be done by both name" + + " and mail or only using any of the two.", + UseStarlarkThread = true)] + public ITransformation MapAuthor( + [Param( + Name = "authors", + Named = true, + Doc = "The author mapping. Keys can be in the form of 'Your Name', 'some@mail' or 'Your" + + " Name '. The mapping applies heuristics to know which field to use in" + + " the mapping. The value has to be always in the form of 'Your Name '")] + Dict authors, + [Param( + Name = "reversible", + Named = true, + Doc = "If the transform is automatically reversible. Workflows using the reverse of this" + + " transform will be able to automatically map values to keys.", + DefaultValue = "False")] + bool reversible, + [Param( + Name = "noop_reverse", + Named = true, + Doc = "If true, the reversal of the transformation doesn't do anything.", + DefaultValue = "False")] + bool noopReverse, + [Param( + Name = "fail_if_not_found", + Named = true, + Doc = "Fail if a mapping cannot be found. Helps discovering early authors that should be" + + " in the map", + DefaultValue = "False")] + bool failIfNotFound, + [Param( + Name = "reverse_fail_if_not_found", + Named = true, + Doc = "Same as fail_if_not_found but when the transform is used in a inverse workflow.", + DefaultValue = "False")] + bool reverseFailIfNotFound, + [Param( + Name = "map_all_changes", + Named = true, + Doc = "If all changes being migrated should be mapped. Useful for getting a mapped" + + " metadata.squash_notes. By default we only map the current author.", + DefaultValue = "False")] + bool mapAll, + StarlarkThread thread) + { + Check( + reversible || !reverseFailIfNotFound, + "'reverse_fail_if_not_found' can only be true if 'reversible' is true"); + Check( + !noopReverse || !reverseFailIfNotFound, + "'reverse_fail_if_not_found' can only be true if 'noop_reverse' is not set"); + + return Metadata.MapAuthor.Create( + thread.GetCallerLocation(), + ConvertStringMap(authors, "authors"), + reversible, + noopReverse, + failIfNotFound, + reverseFailIfNotFound, + mapAll); + } + + [StarlarkMethod( + "use_last_change", + Doc = + "Use metadata (message or/and author) from the last change being migrated. Useful when" + + " using 'SQUASH' mode but user only cares about the last change.", + UseStarlarkThread = true)] + public ITransformation UseLastChange( + [Param( + Name = "author", + Named = true, + Doc = "Replace author with the last change author", + DefaultValue = "True", + Positional = false)] + bool useAuthor, + [Param( + Name = "message", + Named = true, + Doc = "Replace message with last change message.", + DefaultValue = "True", + Positional = false)] + bool useMsg, + [Param( + Name = "default_message", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Named = true, + Doc = "Replace message with last change message.", + DefaultValue = "None", + Positional = false)] + object defaultMsg, + [Param( + Name = "use_merge", + Named = true, + Doc = "If true then merge changes are taken into account for looking for the last" + + " change.", + DefaultValue = "True", + Positional = false)] + bool useMerge, + StarlarkThread thread) + { + Check(useAuthor || useMsg, "author or message should be enabled"); + string? defaultMessage = ConvertFromNoneable(defaultMsg, null); + Check( + defaultMessage == null || useMsg, "default_message can only be used if message = True "); + return new UseLastChange( + useAuthor, useMsg, defaultMessage, useMerge, thread.GetCallerLocation()); + } + + [StarlarkMethod( + "expose_label", + Doc = + "Certain labels are present in the internal metadata but are not exposed in the message" + + " by default. This transformations find a label in the internal metadata and exposes" + + " it in the message. If the label is already present in the message it will update it" + + " to use the new name and separator.", + UseStarlarkThread = true)] + public ITransformation ExposeLabel( + [Param(Name = "name", Doc = "The label to search", Named = true)] + string label, + [Param( + Name = "new_name", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Doc = "The name to use in the message", + Named = true, + DefaultValue = "None")] + object newName, + [Param( + Name = "separator", + Named = true, + Doc = "The separator to use when adding the label to the message", + DefaultValue = "\"=\"")] + string separator, + [Param( + Name = "ignore_label_not_found", + Named = true, + Doc = "If a label is not found, ignore the error and continue.", + DefaultValue = "True")] + bool ignoreIfLabelNotFound, + [Param( + Name = "all", + Named = true, + Doc = "By default Copybara tries to find the most relevant instance of the label. First" + + " looking into the message and then looking into the changes in order. If this" + + " field is true it exposes all the matches instead.", + DefaultValue = "False")] + bool all, + [Param( + Name = "concat_separator", + Named = true, + Doc = "If all is set, copybara will expose multiple values in one per line. If a" + + " separator is specified, it will concat the values instead.", + DefaultValue = "None")] + object joiner, + StarlarkThread thread) + { + Check( + LabelFinder.VALID_LABEL.IsMatch(label), "'name': Invalid label name'{0}'", label); + string newLabelName = ConvertFromNoneable(newName, label)!; + Check( + LabelFinder.VALID_LABEL.IsMatch(newLabelName), + "'new_name': Invalid label name '{0}'", + newLabelName); + + string? join = ConvertFromNoneable(joiner, null); + Check(join == null || all, "'joiner': Cannot be set unless all is True."); + return new ExposeLabelInMessage( + label, + newLabelName, + separator, + ignoreIfLabelNotFound, + all, + join, + thread.GetCallerLocation()); + } + + [StarlarkMethod( + "remove_label", + Doc = "Remove a label from the message", + UseStarlarkThread = true)] + public ITransformation RemoveLabel( + [Param(Name = "name", Doc = "The label name", Named = true)] + string label, + StarlarkThread thread) + { + Check( + LabelFinder.VALID_LABEL.IsMatch(label), "'name': Invalid label name'{0}'", label); + return new RemoveLabelInMessage(label, thread.GetCallerLocation()); + } + + [StarlarkMethod( + "restore_author", + Doc = + "For a given change, restore the author present in the ORIGINAL_AUTHOR label as the" + + " author of the change.", + UseStarlarkThread = true)] + public ITransformation RestoreAuthor( + [Param( + Name = "label", + Named = true, + Doc = "The label to use for restoring the author", + DefaultValue = "'ORIGINAL_AUTHOR'")] + string label, + [Param( + Name = "separator", + Named = true, + Doc = "The separator to use between the label and the value", + DefaultValue = "\"=\"")] + string separator, + [Param( + Name = "search_all_changes", + Named = true, + Doc = "By default Copybara only looks in the last current change for the author label." + + " This allows to do the search in all current changes (Only makes sense for" + + " SQUASH/CHANGE_REQUEST).", + DefaultValue = "False")] + bool searchAllChanges, + StarlarkThread thread) => + new RestoreOriginalAuthor(label, separator, searchAllChanges, thread.GetCallerLocation()); + + [StarlarkMethod( + "add_header", + Doc = + "Adds a header line to the commit message. Any variable present in the message in the" + + " form of ${LABEL_NAME} will be replaced by the corresponding label in the message." + + " Note that this requires that the label is already in the message or in any of the" + + " changes being imported. The label in the message takes priority over the ones in" + + " the list of original messages of changes imported.\n", + UseStarlarkThread = true)] + public ITransformation AddHeader( + [Param( + Name = "text", + Named = true, + Doc = "The header text to include in the message. For example '[Import of foo ${LABEL}]'." + + " This would construct a message resolving ${LABEL} to the corresponding label.")] + string header, + [Param( + Name = "ignore_label_not_found", + Named = true, + Doc = "If a label used in the template is not found, ignore the error and don't add the" + + " header. By default it will stop the migration and fail.", + DefaultValue = "False")] + bool ignoreIfLabelNotFound, + [Param( + Name = "new_line", + Named = true, + Doc = "If a new line should be added between the header and the original message. This" + + " allows to create messages like `HEADER: ORIGINAL_MESSAGE`", + DefaultValue = "True")] + bool newLine, + StarlarkThread thread) => + new TemplateMessage( + header, + ignoreIfLabelNotFound, + newLine, + replaceMessage: false, + thread.GetCallerLocation()); + + [StarlarkMethod( + "replace_message", + Doc = + "Replace the change message with a template text. Any variable present in the message in" + + " the form of ${LABEL_NAME} will be replaced by the corresponding label in the" + + " message. Note that this requires that the label is already in the message or in any" + + " of the changes being imported. The label in the message takes priority over the ones" + + " in the list of original messages of changes imported.\n", + UseStarlarkThread = true)] + public ITransformation ReplaceMessage( + [Param( + Name = "text", + Named = true, + Doc = "The template text to use for the message. For example '[Import of foo ${LABEL}]'." + + " This would construct a message resolving ${LABEL} to the corresponding label.")] + string template, + [Param( + Name = "ignore_label_not_found", + Named = true, + Doc = "If a label used in the template is not found, ignore the error and don't add the" + + " header. By default it will stop the migration and fail.", + DefaultValue = "False")] + bool ignoreIfLabelNotFound, + StarlarkThread thread) => + new TemplateMessage( + template, + ignoreIfLabelNotFound, + newLine: false, + replaceMessage: true, + thread.GetCallerLocation()); + + [StarlarkMethod( + "scrubber", + Doc = "Removes part of the change message using a regex", + UseStarlarkThread = true)] + public ITransformation Scrubber( + [Param( + Name = "regex", + Named = true, + Doc = "Any text matching the regex will be removed. Note that the regex is runs in" + + " multiline mode.")] + string regex, + [Param( + Name = "msg_if_no_match", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Named = true, + Doc = "If set, Copybara will use this text when the scrubbing regex doesn't match.", + DefaultValue = "None")] + object msgIfNoMatchObj, + [Param( + Name = "fail_if_no_match", + Named = true, + Doc = "If set, msg_if_no_match must be None and then fail if the scrubbing regex doesn't" + + " match. ", + DefaultValue = "False")] + bool failIfNoMatch, + [Param( + Name = "replacement", + Named = true, + Doc = "Text replacement for the matching substrings. References to regex group numbers" + + " can be used in the form of $1, $2, etc.", + DefaultValue = "''")] + string replacement, + StarlarkThread thread) + { + Regex pattern; + try + { + pattern = new Regex(regex, RegexOptions.Multiline); + } + catch (ArgumentException e) + { + throw StarlarkRt.Errorf("Invalid regex expression: {0}", e.Message); + } + string? msgIfNoMatch = ConvertFromNoneable(msgIfNoMatchObj, null); + Check( + !failIfNoMatch || msgIfNoMatch == null, + "If fail_if_no_match is true, msg_if_no_match should be None."); + return new Scrubber( + pattern, msgIfNoMatch, failIfNoMatch, replacement, thread.GetCallerLocation()); + } + + [StarlarkMethod( + "verify_match", + Doc = + "Verifies that a RegEx matches (or not matches) the change message. Does not transform" + + " anything, but will stop the workflow if it fails.", + UseStarlarkThread = true)] + public ITransformation VerifyMatch( + [Param( + Name = "regex", + Named = true, + Doc = "The regex pattern to verify. The re2j pattern will be applied in multiline mode," + + " i.e. '^' refers to the beginning of a file and '$' to its end.")] + string regex, + [Param( + Name = "verify_no_match", + Named = true, + Doc = "If true, the transformation will verify that the RegEx does not match.", + DefaultValue = "False")] + bool verifyNoMatch, + StarlarkThread thread) + { + Regex pattern; + try + { + pattern = new Regex(regex, RegexOptions.Multiline); + } + catch (ArgumentException e) + { + throw StarlarkRt.Errorf("Invalid regex expression: {0}", e.Message); + } + return new MetadataVerifyMatch(pattern, verifyNoMatch, thread.GetCallerLocation()); + } + + [StarlarkMethod( + "map_references", + Doc = + "Allows updating links to references in commit messages to match the destination's" + + " format. Note that this will only consider the 5000 latest commits.", + UseStarlarkThread = true)] + public ReferenceMigrator MapReferences( + [Param( + Name = "before", + Named = true, + Doc = "Template for origin references in the change message. Use a '${reference}' token" + + " to capture the actual references.")] + string originPattern, + [Param( + Name = "after", + Named = true, + Doc = "Format for destination references in the change message. Use a '${reference}'" + + " token to represent the destination reference.")] + string destinationFormat, + [Param( + Name = "regex_groups", + DefaultValue = "{}", + Named = true, + Doc = "Regexes for the ${reference} token's content. Requires one 'before_ref' entry" + + " matching the ${reference} token's content on the before side. Optionally accepts" + + " one 'after_ref' used for validation.")] + Dict groups, + [Param( + Name = "additional_import_labels", + Named = true, + AllowedTypes = new[] { typeof(Sequence) }, + DefaultValue = "[]", + Doc = "Meant to be used when migrating from another tool: Per default, copybara will only" + + " recognize the labels defined in the workflow's endpoints. The tool will use" + + " these additional labels to find labels created by other invocations and tools.")] + object labels, + StarlarkThread thread) + { + var groupsMap = ConvertStringMap(groups, "regex_groups"); + Check( + groupsMap.ContainsKey("before_ref") + && (groupsMap.Count != 2 || groupsMap.ContainsKey("after_ref")) + && groupsMap.Count <= 2, + "Invalid 'regex_groups' - Should only contain 'before_ref' and optionally 'after_ref'." + + " Was: {0}.", + string.Join(", ", groupsMap.Keys)); + Regex beforePattern; + Regex? afterPattern = null; + try + { + beforePattern = new Regex(groupsMap["before_ref"]); + } + catch (ArgumentException) + { + throw StarlarkRt.Errorf("Invalid before_ref regex '{0}'.", groupsMap["before_ref"]); + } + if (groupsMap.ContainsKey("after_ref")) + { + try + { + afterPattern = new Regex(groupsMap["after_ref"]); + } + catch (ArgumentException) + { + throw StarlarkRt.Errorf("Invalid after_ref regex '{0}'.", groupsMap["after_ref"]); + } + } + return ReferenceMigrator.Create( + originPattern, + destinationFormat, + beforePattern, + afterPattern, + ConvertStringList(labels, "labels").ToImmutableArray(), + thread.GetCallerLocation()); + } + + // ---- Helpers (inlined from SkylarkUtil until that class is ported) ---- + + private static void Check(bool condition, string format, params object?[] args) + { + if (!condition) + { + throw StarlarkRt.Errorf(format, args); + } + } + + private static string CheckNotEmpty(string? value, string name) + { + if (string.IsNullOrEmpty(value)) + { + throw StarlarkRt.Errorf("Invalid empty field '{0}'.", name); + } + return value; + } + + private static string? ConvertFromNoneable(object? value, string? defaultValue) => + StarlarkRt.IsNullOrNone(value) ? defaultValue : (string)value!; + + private static IReadOnlyDictionary ConvertStringMap(Dict dict, string name) + { + var result = new Dictionary(); + foreach (var entry in dict.Entries) + { + if (entry.Key is not string k) + { + throw StarlarkRt.Errorf( + "Expected string key for '{0}' but got: {1}", name, entry.Key); + } + if (entry.Value is not string v) + { + throw StarlarkRt.Errorf( + "Expected string value for '{0}' but got: {1}", name, entry.Value); + } + result[k] = v; + } + return result; + } + + private static IReadOnlyList ConvertStringList(object value, string name) + { + var seq = Starlark.Eval.Sequence.Cast(value, name); + return seq.ToList(); + } +} diff --git a/src/Copybara.Core/Transform/Metadata/MetadataSquashNotes.cs b/src/Copybara.Core/Transform/Metadata/MetadataSquashNotes.cs new file mode 100644 index 000000000..755b1538b --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/MetadataSquashNotes.cs @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.TemplateToken; +using Copybara.Transform; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// +/// Generates a message that includes a constant prefix text and a list of changes included in the +/// squash change. +/// +public class MetadataSquashNotes : ITransformation +{ + private readonly LabelTemplate _prefixTemplate; + private readonly int _max; + private readonly bool _compact; + private readonly bool _showAuthor; + private readonly bool _showDescription; + private readonly bool _showRef; + private readonly bool _oldestFirst; + private readonly bool _useMerge; + private readonly Location _location; + + public MetadataSquashNotes( + string prefix, + int max, + bool compact, + bool showRef, + bool showAuthor, + bool showDescription, + bool oldestFirst, + bool useMerge, + Location location) + { + _prefixTemplate = new LabelTemplate(prefix); + _max = max; + _compact = compact; + _showRef = showRef; + _showAuthor = showAuthor; + _showDescription = showDescription; + _oldestFirst = oldestFirst; + _useMerge = useMerge; + _location = location; + } + + public TransformationStatus Transform(TransformWork work) + { + StringBuilder sb; + try + { + sb = new StringBuilder(_prefixTemplate.Resolve(work.GetLabel)); + } + catch (LabelTemplate.LabelNotFoundException e) + { + throw new ValidationException( + $"Cannot find label '{e.Label}' in message:\n {work.GetMessage()}\nor any of the" + + " original commit messages"); + } + + if (_max == 0) + { + // Don't force changes to be computed if we don't want any change back. + work.SetMessage(sb.ToString()); + return TransformationStatus.Success(); + } + + int counter = 0; + var changesList = new List>( + work.GetChanges().GetCurrent().Cast>()); + if (_oldestFirst) + { + changesList.Reverse(); + } + if (!_useMerge) + { + changesList = changesList.Where(e => !e.IsMerge()).ToList(); + } + + for (int i = 0; i < changesList.Count; i++) + { + Change c = changesList[i]; + if (counter == _max) + { + break; + } + var summary = new List(); + if (_compact) + { + sb.Append(" - "); + if (_showRef) + { + summary.Add(c.Ref); + } + if (_showDescription) + { + summary.Add(CutIfLong(c.FirstLineMessage())); + } + if (_showAuthor) + { + summary.Add("by " + c.GetMappedAuthor()); + } + sb.Append(string.Join(" ", summary)); + sb.Append('\n'); + } + else + { + sb.Append("--\n"); + if (_showRef) + { + summary.Add(c.Ref); + } + else + { + summary.Add($"Change {i + 1} of {changesList.Count}"); + } + if (_showAuthor) + { + summary.Add("by " + c.GetAuthor()); + } + sb.Append(string.Join(" ", summary)); + if (_showDescription) + { + sb.Append(":\n\n"); + sb.Append(c.GetMessage()); + } + sb.Append('\n'); + } + counter++; + } + + if (changesList.Count > _max) + { + sb.Append(" (And ").Append(changesList.Count - _max).Append(" more changes)\n"); + } + work.SetMessage(sb.ToString()); + + return TransformationStatus.Success(); + } + + private static string CutIfLong(string msg) => + msg.Length < 60 ? msg : msg.Substring(0, 57) + "..."; + + public ITransformation Reverse() => + new ExplicitReversal(IntentionalNoop.Instance, this); + + public string Describe() => "squash_notes"; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Metadata/MetadataVerifyMatch.cs b/src/Copybara.Core/Transform/Metadata/MetadataVerifyMatch.cs new file mode 100644 index 000000000..83e3909b9 --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/MetadataVerifyMatch.cs @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Transform; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// +/// A checker that validates that the change description satisfies a Regex or that it doesn't if +/// verifyNoMatch is set. +/// +public class MetadataVerifyMatch : ITransformation +{ + private readonly Regex _pattern; + private readonly bool _verifyNoMatch; + private readonly Location _location; + + internal MetadataVerifyMatch(Regex pattern, bool verifyNoMatch, Location location) + { + _pattern = Preconditions.CheckNotNull(pattern); + _verifyNoMatch = verifyNoMatch; + _location = Preconditions.CheckNotNull(location); + } + + public TransformationStatus Transform(TransformWork work) + { + bool found = _pattern.IsMatch(work.GetMessage()); + ValidationException.CheckCondition( + found || _verifyNoMatch, + "Could not find '{0}' in the change message. Message was:\n{1}", + _pattern, + work.GetMessage()); + + ValidationException.CheckCondition( + !found || !_verifyNoMatch, + "'{0}' found in the change message. Message was:\n{1}", + _pattern, + work.GetMessage()); + return TransformationStatus.Success(); + } + + public ITransformation Reverse() => + new ExplicitReversal(IntentionalNoop.Instance, this); + + public string Describe() => + $"Verify message {(_verifyNoMatch ? "does not match" : "matches")} '{_pattern}'"; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Metadata/ReferenceMigrator.cs b/src/Copybara.Core/Transform/Metadata/ReferenceMigrator.cs new file mode 100644 index 000000000..445c50c78 --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/ReferenceMigrator.cs @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.TemplateToken; +using Copybara.Transform; +using Starlark.Eval; +using Starlark.Syntax; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Transform.Metadata; + +/// Adjusts textual references in change messages to match the destination. +public class ReferenceMigrator : ITransformation +{ + internal const int MaxChangesToVisit = 5000; + + private readonly RegexTemplateTokens _before; + private readonly RegexTemplateTokens _after; + private readonly ImmutableArray _additionalLabels; + private readonly Regex? _reversePattern; + private readonly Location _location; + + private readonly Dictionary _knownChanges = new(); + + internal ReferenceMigrator( + RegexTemplateTokens before, + RegexTemplateTokens after, + Regex? reversePattern, + ImmutableArray additionalLabels, + Location location) + { + _before = Preconditions.CheckNotNull(before); + _after = Preconditions.CheckNotNull(after); + _additionalLabels = additionalLabels; + _reversePattern = reversePattern; + _location = Preconditions.CheckNotNull(location); + } + + public static ReferenceMigrator Create( + string before, + string after, + Regex forward, + Regex? backward, + ImmutableArray additionalLabels, + Location location) + { + var patterns = new Dictionary { ["reference"] = forward }; + var beforeTokens = new RegexTemplateTokens(before, patterns, repeatedGroups: false, location); + beforeTokens.ValidateUnused(); + var afterTokens = new RegexTemplateTokens(after, patterns, repeatedGroups: false, location); + afterTokens.ValidateUnused(); + if (after.LastIndexOf("$1", StringComparison.Ordinal) != -1) + { + throw StarlarkRt.Errorf( + "Destination format '{0}' uses the reserved token '$1'.", after); + } + return new ReferenceMigrator(beforeTokens, afterTokens, backward, additionalLabels, location); + } + + public TransformationStatus Transform(TransformWork work) + { + ValidationException? thrown = null; + var replacer = _before.CallbackReplacer( + _after, + new Callback(this, work, () => thrown, ex => thrown ??= ex), + firstOnly: false, + multiline: false, + patternsToIgnore: null); + string replaced = replacer.Replace(work.GetMessage()); + if (thrown != null) + { + throw thrown; + } + if (!replaced.Equals(work.GetMessage())) + { + work.SetMessage(replaced); + } + return TransformationStatus.Success(); + } + + private sealed class Callback : RegexTemplateTokens.IAlterAfterTemplate + { + private readonly ReferenceMigrator _owner; + private readonly TransformWork _work; + private readonly Func _getThrown; + private readonly Action _setThrown; + + public Callback( + ReferenceMigrator owner, + TransformWork work, + Func getThrown, + Action setThrown) + { + _owner = owner; + _work = work; + _getThrown = getThrown; + _setThrown = setThrown; + } + + public string Alter(IReadOnlyDictionary groupValues, string template) + { + groupValues.TryGetValue(0, out var whole); + if (whole != null) + { + try + { + groupValues.TryGetValue(1, out var refValue); + string? destinationRef = _owner.FindChange( + refValue!, + _work.GetMigrationInfo().GetOriginLabel(), + _work.GetMigrationInfo().DestinationVisitable()); + if (destinationRef != null) + { + // This will not work for the case where the template was "foo\\$1", if this + // is an issue, a non-naive implementation might be required. + return Regex.Replace(template, "[$]1", destinationRef.Replace("$", "$$")); + } + return whole; + } + catch (ValidationException exception) + { + if (_getThrown() == null) + { + _setThrown(exception); + } + return whole; + } + } + return template; + } + } + + public ITransformation Reverse() => + new ExplicitReversal(IntentionalNoop.Instance, this); + + public string Describe() => "map_references: " + _before + " to " + _after; + + private string? FindChange( + string refBeingMigrated, + string originLabel, + IChangeVisitable? destinationReader) + { + int changesVisited = 0; + var originLabels = new List { originLabel }; + originLabels.AddRange(_additionalLabels); + ValidationException.CheckCondition( + destinationReader != null, "Destination does not support reading change history."); + if (_knownChanges.TryGetValue(refBeingMigrated, out var known)) + { + return known; + } + try + { + destinationReader!.VisitChangesWithAnyLabel( + null, + originLabels, + new LabelVisitor(this, refBeingMigrated, () => ++changesVisited)); + _knownChanges.TryGetValue(refBeingMigrated, out var retVal); + if (_reversePattern != null && retVal != null && !IsFullMatch(_reversePattern, retVal)) + { + throw new ValidationException( + $"Reference {retVal} does not match regex '{_reversePattern}'"); + } + return retVal; + } + catch (RepoException exception) + { + throw new ValidationException("Exception finding reference.", exception); + } + } + + private static bool IsFullMatch(Regex regex, string input) + { + var m = regex.Match(input); + return m.Success && m.Index == 0 && m.Length == input.Length; + } + + private sealed class LabelVisitor : IChangesLabelVisitor + { + private readonly ReferenceMigrator _owner; + private readonly string _refBeingMigrated; + private readonly Func _incrementVisited; + + public LabelVisitor( + ReferenceMigrator owner, string refBeingMigrated, Func incrementVisited) + { + _owner = owner; + _refBeingMigrated = refBeingMigrated; + _incrementVisited = incrementVisited; + } + + public VisitResult Visit( + Change input, IReadOnlyDictionary matchedLabels) + { + foreach (var labelValue in matchedLabels.Values) + { + if (!_owner._knownChanges.ContainsKey(labelValue)) + { + _owner._knownChanges[labelValue] = input.Ref; + } + if (labelValue.Equals(_refBeingMigrated)) + { + return VisitResult.Terminate; + } + } + return _incrementVisited() > MaxChangesToVisit + ? VisitResult.Terminate + : VisitResult.Continue; + } + } + + public override string ToString() => $"ReferenceMigrator{{before={_before}, after={_after}}}"; + + public override bool Equals(object? other) => + other is ReferenceMigrator o + && Equals(_before, o._before) + && Equals(_after, o._after); + + public Location Location() => _location; + + public override int GetHashCode() => HashCode.Combine(_before, _after); +} diff --git a/src/Copybara.Core/Transform/Metadata/RemoveLabelInMessage.cs b/src/Copybara.Core/Transform/Metadata/RemoveLabelInMessage.cs new file mode 100644 index 000000000..fa9e5ce80 --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/RemoveLabelInMessage.cs @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Transform; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// Removes a label from the change message. +public class RemoveLabelInMessage : ITransformation +{ + private readonly string _label; + private readonly Location _location; + + internal RemoveLabelInMessage(string label, Location location) + { + _label = Preconditions.CheckNotNull(label); + _location = Preconditions.CheckNotNull(location); + } + + public TransformationStatus Transform(TransformWork work) + { + string message = work.GetMessage(); + work.RemoveLabel(_label, wholeMessage: false); + // Lets try to find the message in all the text. + if (work.GetMessage().Equals(message)) + { + work.RemoveLabel(_label, wholeMessage: true); + } + return TransformationStatus.Success(); + } + + public ITransformation Reverse() => + new ExplicitReversal(IntentionalNoop.Instance, this); + + public string Describe() => "Removing label " + _label; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Metadata/RestoreOriginalAuthor.cs b/src/Copybara.Core/Transform/Metadata/RestoreOriginalAuthor.cs new file mode 100644 index 000000000..b5265a3f8 --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/RestoreOriginalAuthor.cs @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Revision; +using Starlark.Eval; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// Restores an original author stored in a label. +public class RestoreOriginalAuthor : ITransformation +{ + private readonly string _label; + private readonly string _separator; + private readonly bool _searchAllChanges; + private readonly Location _location; + + internal RestoreOriginalAuthor( + string label, string separator, bool searchAllChanges, Location location) + { + _label = label; + _separator = separator; + _searchAllChanges = searchAllChanges; + _location = Preconditions.CheckNotNull(location); + } + + public TransformationStatus Transform(TransformWork work) + { + Author? author = null; + // If multiple commits are included (for example on a squash for skipping a bad change), + // last author wins. + foreach (var changeObj in work.GetChanges().GetCurrent()) + { + var change = (Change)changeObj; + var labelValue = change.GetLabels().Get(_label); + if (labelValue.Length != 0) + { + try + { + author = Author.Parse(labelValue[labelValue.Length - 1]); + } + catch (EvalException e) + { + // Don't fail the migration because the label is wrong since it is very + // difficult for a user to recover from this. + work.GetConsole().Warn("Cannot restore original author: " + e.Message); + } + } + if (!_searchAllChanges) + { + break; + } + } + + if (author != null) + { + work.SetAuthor(author); + work.RemoveLabel(_label, wholeMessage: true); + } + return TransformationStatus.Success(); + } + + public ITransformation Reverse() => + new SaveOriginalAuthor(_label, _separator, _location); + + public string Describe() => "Restoring original author"; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Metadata/SaveOriginalAuthor.cs b/src/Copybara.Core/Transform/Metadata/SaveOriginalAuthor.cs new file mode 100644 index 000000000..83aaab9dd --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/SaveOriginalAuthor.cs @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// Saves the original author of the change in the message with a label. +public class SaveOriginalAuthor : ITransformation +{ + private readonly string _label; + private readonly string _separator; + private readonly Location _location; + + internal SaveOriginalAuthor(string label, string separator, Location location) + { + _label = Preconditions.CheckNotNull(label); + _separator = Preconditions.CheckNotNull(separator); + _location = Preconditions.CheckNotNull(location); + } + + public TransformationStatus Transform(TransformWork work) + { + work.AddOrReplaceLabel(_label, work.GetAuthor().ToString(), _separator); + return TransformationStatus.Success(); + } + + public ITransformation Reverse() => + new RestoreOriginalAuthor(_label, _separator, searchAllChanges: false, _location); + + public string Describe() => "Saving original author"; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Metadata/Scrubber.cs b/src/Copybara.Core/Transform/Metadata/Scrubber.cs new file mode 100644 index 000000000..4955945d7 --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/Scrubber.cs @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Transform; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// A transformer that removes matching substrings from the change description. +public class Scrubber : ITransformation +{ + private readonly Regex _pattern; + private readonly string _replacement; + private readonly Location _location; + private readonly string? _defaultPublicMsg; + private readonly bool _failIfNoMatch; + + internal Scrubber( + Regex pattern, + string? defaultPublicMsg, + bool failIfNoMatch, + string replacement, + Location location) + { + _pattern = Preconditions.CheckNotNull(pattern); + _defaultPublicMsg = defaultPublicMsg; + _failIfNoMatch = failIfNoMatch; + _replacement = Preconditions.CheckNotNull(replacement); + _location = Preconditions.CheckNotNull(location); + } + + public TransformationStatus Transform(TransformWork work) + { + try + { + string scrubbedMessage = _pattern.Replace(work.GetMessage(), _replacement); + if (!work.GetMessage().Equals(scrubbedMessage)) + { + work.GetConsole() + .VerboseFmt( + "Scrubbed change description '{0}' by '{1}'", + work.GetMessage(), + scrubbedMessage); + work.SetMessage(scrubbedMessage); + return TransformationStatus.Success(); + } + ValidationException.CheckCondition( + !_failIfNoMatch, + "Scrubber regex: '{0}' didn't match for description: '{1}'", + _pattern, + work.GetMessage()); + if (_defaultPublicMsg != null) + { + work.SetMessage(_defaultPublicMsg); + } + } + catch (ArgumentOutOfRangeException e) + { + throw new ValidationException( + $"Could not find matching group. Are you missing a group in your regex '{_pattern}'?", + e); + } + return TransformationStatus.Success(); + } + + public ITransformation Reverse() => + new ExplicitReversal(IntentionalNoop.Instance, this); + + public string Describe() => "Description scrubber"; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Metadata/TemplateMessage.cs b/src/Copybara.Core/Transform/Metadata/TemplateMessage.cs new file mode 100644 index 000000000..15b40d1a0 --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/TemplateMessage.cs @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.TemplateToken; +using Copybara.Transform; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// +/// Adds a header text on top of the change message. +/// +/// This transform allows referring to change labels both from the current message or the set +/// of commits being imported. +/// +public class TemplateMessage : ITransformation +{ + private readonly bool _ignoreIfLabelNotFound; + private readonly bool _newLine; + private readonly bool _replaceMessage; + private readonly LabelTemplate _labelTemplate; + private readonly Location _location; + + internal TemplateMessage( + string header, + bool ignoreIfLabelNotFound, + bool newLine, + bool replaceMessage, + Location location) + { + _ignoreIfLabelNotFound = ignoreIfLabelNotFound; + _newLine = newLine; + _replaceMessage = replaceMessage; + _labelTemplate = new LabelTemplate(header); + _location = Preconditions.CheckNotNull(location); + } + + public TransformationStatus Transform(TransformWork work) + { + string newMsg; + try + { + newMsg = _labelTemplate.Resolve(work.GetLabel); + } + catch (LabelTemplate.LabelNotFoundException e) + { + if (_ignoreIfLabelNotFound) + { + return TransformationStatus.Success(); + } + throw new ValidationException( + $"Cannot find label '{e.Label}' in message:\n {work.GetMessage()}\nor any of the" + + " original commit messages"); + } + + if (!_replaceMessage) + { + newMsg += (_newLine ? "\n" : "") + work.GetMessage(); + } + work.SetMessage(newMsg); + return TransformationStatus.Success(); + } + + public ITransformation Reverse() => + new ExplicitReversal(IntentionalNoop.Instance, this); + + public string Describe() => "Adding header to the message"; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Metadata/UseLastChange.cs b/src/Copybara.Core/Transform/Metadata/UseLastChange.cs new file mode 100644 index 000000000..3cfa2b9c7 --- /dev/null +++ b/src/Copybara.Core/Transform/Metadata/UseLastChange.cs @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Revision; +using Copybara.Transform; +using Starlark.Syntax; + +namespace Copybara.Transform.Metadata; + +/// +/// Use metadata (i.e. message/author) from the last change being migrated. Useful when using +/// 'SQUASH' mode but user only cares about the last change. +/// +public class UseLastChange : ITransformation +{ + private readonly bool _useMessage; + private readonly bool _useAuthor; + private readonly string? _defaultMessage; + private readonly bool _useMerge; + private readonly Location _location; + + internal UseLastChange( + bool useAuthor, + bool useMessage, + string? defaultMessage, + bool useMerge, + Location location) + { + _useAuthor = useAuthor; + _useMessage = useMessage; + _defaultMessage = defaultMessage; + _useMerge = useMerge; + _location = Preconditions.CheckNotNull(location); + } + + public TransformationStatus Transform(TransformWork work) + { + Change? lastChange = GetLastChange(work); + if (lastChange == null) + { + if (_useMessage && _defaultMessage != null) + { + work.SetMessage(_defaultMessage); + } + return TransformationStatus.Success(); + } + if (_useMessage) + { + work.SetMessage(lastChange.GetMessage()); + } + if (_useAuthor) + { + work.SetAuthor(lastChange.GetMappedAuthor()); + } + return TransformationStatus.Success(); + } + + private Change? GetLastChange(TransformWork work) + { + foreach (var changeObj in work.GetChanges().GetCurrent()) + { + var change = (Change)changeObj; + if (!_useMerge && change.IsMerge()) + { + continue; + } + return change; + } + return null; + } + + public ITransformation Reverse() => + new ExplicitReversal(IntentionalNoop.Instance, this); + + public string Describe() => "Use last change metadata"; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Remove.cs b/src/Copybara.Core/Transform/Remove.cs new file mode 100644 index 000000000..0a93cb2dc --- /dev/null +++ b/src/Copybara.Core/Transform/Remove.cs @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; +using Starlark.Syntax; + +namespace Copybara.Transform; + +/// +/// We might promote this to a Skylark transform. But because we already have origin_files, that +/// works better with reversible workflows, this is a bad idea except for explicit reversals of +/// core.copy. +/// +public class Remove : ITransformation +{ + private readonly Glob _glob; + private readonly Location _location; + + public Remove(Glob glob, Location location) + { + _glob = Preconditions.CheckNotNull(glob); + _location = location; + } + + public TransformationStatus Transform(TransformWork work) + { + // TODO(malcon): Fix ConfigValidator and move this logic there. + ValidationException.CheckCondition( + work.IsInsideExplicitTransform(), + "core.remove() is only mean to be used inside core.transform for reversing" + + " transformations like core.copy(). Please use origin_files exclude for" + + " filtering out files."); + + int numDeletes = + FileUtil.DeleteFilesRecursively(work.GetCheckoutDir(), _glob.RelativeTo(work.GetCheckoutDir())); + if (numDeletes == 0) + { + return TransformationStatus.Noop(_glob + " didn't delete any file"); + } + return TransformationStatus.Success(); + } + + public ITransformation Reverse() => + throw new NonReversibleValidationException("core.remove is not reversible"); + + public string Describe() => "Removing " + _glob; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Rename.cs b/src/Copybara.Core/Transform/Rename.cs new file mode 100644 index 000000000..228182002 --- /dev/null +++ b/src/Copybara.Core/Transform/Rename.cs @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Copybara.Util; +using Starlark.Syntax; + +namespace Copybara.Transform; + +/// Transformation for doing rename of files. +public class Rename : ITransformation +{ + private readonly string _before; + private readonly string _after; + private readonly Glob _paths; + private readonly bool _overwrite; + private readonly bool _suffix; + private readonly Location _location; + + public Rename( + string before, string after, Glob paths, bool overwrite, bool suffix, Location location) + { + _before = before; + _after = after; + _paths = paths; + _overwrite = overwrite; + _suffix = suffix; + _location = location; + } + + public TransformationStatus Transform(TransformWork work) + { + bool noop = true; + foreach (CheckoutPath p in work.List(_paths).Cast()) + { + string file = PathOps.Resolve(p.GetCheckoutDir(), p.GetPath()); + if (!File.Exists(file)) + { + continue; + } + string destination; + if (_suffix) + { + if (!file.EndsWith(_before, StringComparison.Ordinal)) + { + continue; + } + destination = PathOps.Normalize(ReplaceFirst(file, _before, _after)); + } + else + { + if (!PathEndsWith(file, _before)) + { + continue; + } + destination = PathOps.Normalize(ReplaceFirst(file, _before, _after)); + } + ValidationException.CheckCondition( + PathOps.StartsWith(destination, p.GetCheckoutDir()), + "Destination file for " + destination + " is out of the checkout directory"); + noop = false; + var destParent = PathOps.GetParent(destination); + if (destParent != null) + { + Directory.CreateDirectory(destParent); + } + if (_overwrite && File.Exists(destination)) + { + File.Delete(destination); + } + File.Move(file, destination); + } + if (noop) + { + return TransformationStatus.Noop($"Couldn't find any file to rename with '{_before}'"); + } + return TransformationStatus.Success(); + } + + // Mirrors String.replace(before, after): replaces all occurrences of the literal 'before'. + private static string ReplaceFirst(string value, string before, string after) => + value.Replace(before, after); + + // Mirrors java.nio.file.Path.endsWith: true if the path ends with the given path segments. + private static bool PathEndsWith(string path, string suffix) + { + string normPath = path.Replace('\\', '/').TrimEnd('/'); + string normSuffix = suffix.Replace('\\', '/').TrimEnd('/'); + if (normPath == normSuffix) + { + return true; + } + return normPath.EndsWith("/" + normSuffix, StringComparison.Ordinal); + } + + public ITransformation Reverse() + { + if (_overwrite) + { + throw new NonReversibleValidationException( + "core.rename() with overwrite set is not automatically reversible. Use" + + " core.transform to define an explicit reverse"); + } + + return new ExplicitReversal( + new Rename(_after, _before, _paths, _overwrite, _suffix, _location), this); + } + + public string Describe() => "Renaming " + _before; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/Replace.cs b/src/Copybara.Core/Transform/Replace.cs new file mode 100644 index 000000000..68c751b05 --- /dev/null +++ b/src/Copybara.Core/Transform/Replace.cs @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.TemplateToken; +using Copybara.TreeState; +using Copybara.Util; +using Starlark.Eval; +using Starlark.Syntax; +using StarlarkRt = Starlark.Eval.Starlark; +using FileState = Copybara.TreeState.TreeState.FileState; +using Replacer = Copybara.TemplateToken.RegexTemplateTokens.Replacer; + +namespace Copybara.Transform; + +/// +/// A source code transformation which replaces a regular expression with some other string. +/// +/// The replacement is defined as two strings with interpolations and a mapping of +/// interpolation names to regular expressions. +/// +/// This transformation is line-based and only replaces the first instance of the pattern on a +/// line. +/// +public sealed class Replace : ITransformation +{ + private readonly RegexTemplateTokens _before; + private readonly RegexTemplateTokens _after; + private readonly ImmutableDictionary _regexGroups; + private readonly bool _firstOnly; + private readonly bool _multiline; + private readonly bool _repeatedGroups; + private readonly Glob _paths; + private readonly ImmutableArray _patternsToIgnore; + private readonly WorkflowOptions _workflowOptions; + private readonly Location _location; + + private Replace( + RegexTemplateTokens before, + RegexTemplateTokens after, + IReadOnlyDictionary regexGroups, + bool firstOnly, + bool multiline, + bool repeatedGroups, + Glob paths, + IReadOnlyList patternsToIgnore, + WorkflowOptions workflowOptions, + Location location) + { + _before = Preconditions.CheckNotNull(before); + _after = Preconditions.CheckNotNull(after); + _regexGroups = regexGroups.ToImmutableDictionary(); + _firstOnly = firstOnly; + _multiline = multiline; + _repeatedGroups = repeatedGroups; + _paths = Preconditions.CheckNotNull(paths); + _patternsToIgnore = patternsToIgnore.ToImmutableArray(); + _workflowOptions = Preconditions.CheckNotNull(workflowOptions); + _location = Preconditions.CheckNotNull(location); + } + + public override string ToString() => + $"Replace{{before={_before}, after={_after}, regexGroups=[{string.Join(", ", _regexGroups.Keys)}]," + + $" firstOnly={_firstOnly}, multiline={_multiline}, path={_paths}," + + $" patternsToIgnore=[{string.Join(", ", _patternsToIgnore.Select(p => p.ToString()))}]," + + $" location={_location}}}"; + + public TransformationStatus Transform(TransformWork work) + { + work.GetConsole().VerboseFmt("Running Replace {0}", this); + if (Regex.IsMatch("", _before.GetBefore().ToString()) && !_firstOnly) + { + work.GetConsole().WarnFmt( + "Replace {0} matches the empty String, this is likely to cause unintended behavior," + + " unless it is a no-op.", + this); + } + string checkoutDir = work.GetCheckoutDir(); + + var files = work.GetTreeState().Find(_paths.RelativeTo(checkoutDir)).ToList(); + var batchReplace = new BatchReplace(CreateReplacer, _before.GetBefore().ToString()); + _workflowOptions.Parallelizer().Run(files, batchReplace); + var changed = batchReplace.GetChanged(); + bool matchedFile = batchReplace.IsMatchedFile(); + + work.GetTreeState().NotifyModify(changed); + if (changed.Count == 0) + { + return TransformationStatus.Noop( + "Transformation '" + ToString() + "' was a no-op because it didn't " + + (matchedFile ? "change any of the matching files" : "match any file")); + } + return TransformationStatus.Success(); + } + + public string Describe() => + // before should be almost always unique so it is good enough for identifying the transform. + "Replace " + _before; + + public ITransformation Reverse() + { + try + { + _after.ValidateUnused(); + } + catch (EvalException e) + { + throw new NonReversibleValidationException( + "The transformation is not automatically reversible. Add an explicit reversal field" + + " with core.transform: " + e.Message, + e.InnerException); + } + return new Replace( + _after, _before, _regexGroups, _firstOnly, _multiline, _repeatedGroups, + _paths, _patternsToIgnore, _workflowOptions, _location); + } + + public static Replace Create( + Location location, + string before, + string after, + IReadOnlyDictionary regexGroups, + Glob paths, + bool firstOnly, + bool multiline, + bool repeatedGroups, + IReadOnlyList patternsToIgnore, + WorkflowOptions workflowOptions) + { + var parsedGroups = ParsePatterns(regexGroups); + + var beforeTokens = new RegexTemplateTokens(before, parsedGroups, repeatedGroups, location); + var afterTokens = new RegexTemplateTokens(after, parsedGroups, repeatedGroups, location); + + beforeTokens.ValidateUnused(); + + var parsedIgnorePatterns = new List(); + foreach (string toIgnore in patternsToIgnore) + { + try + { + parsedIgnorePatterns.Add(new Regex(toIgnore)); + } + catch (ArgumentException) + { + throw StarlarkRt.Errorf("'patterns_to_ignore' includes invalid regex: {0}", toIgnore); + } + } + + // Don't validate non-used interpolations in after since they are only relevant for + // reversible transformations. + return new Replace( + beforeTokens, afterTokens, parsedGroups, firstOnly, multiline, repeatedGroups, paths, + parsedIgnorePatterns, workflowOptions, location); + } + + public static IReadOnlyDictionary ParsePatterns( + IReadOnlyDictionary regexGroups) + { + var parsedGroups = new Dictionary(); + foreach (var group in regexGroups) + { + try + { + parsedGroups[group.Key] = new Regex(group.Value); + } + catch (ArgumentException) + { + throw StarlarkRt.Errorf( + "'regex_groups' includes invalid regex for key {0}: {1}", + group.Key, group.Value); + } + } + return parsedGroups; + } + + private sealed class BatchReplace : LocalParallelizer.TransformFunc + { + private readonly Func _replacerSupplier; + private readonly List _changed = new(); + private bool _matchedFile; + private readonly bool _emptyBefore; + private readonly object _lock = new(); + + public BatchReplace(Func replacerSupplier, string before) + { + _replacerSupplier = Preconditions.CheckNotNull(replacerSupplier); + _emptyBefore = before.Length == 0; + } + + public List GetChanged() => _changed; + + public bool IsMatchedFile() => _matchedFile; + + public bool Run(IEnumerable elements) + { + Replacer replacer = _replacerSupplier(); + var changed = new List(); + bool matchedFile = false; + foreach (FileState file in elements) + { + var fileInfo = new FileInfo(file.GetPath()); + if (fileInfo.LinkTarget != null) + { + continue; + } + matchedFile = true; + byte[] bytes = File.ReadAllBytes(file.GetPath()); + if (bytes.Length > int.MaxValue >> 1) + { + throw new ValidationException( + $"Cannot read file '{file.GetPath()}' because it is too big for" + + " core.replace(). You can exclude running for this file by adding" + + " core.replace(..., paths = glob(['**'], exclude =" + + " ['big/file/path'])). another option, if the file is not needed, is" + + " to exclude it in origin_files."); + } + string originalFileContent = Encoding.UTF8.GetString(bytes); + + if (!replacer.IsFirstOnly() && _emptyBefore && originalFileContent.Length > 10_000) + { + throw new ValidationException( + "Error trying to replace empty string with text on a big file, this usually" + + " happens if you use the transform" + + " core.replace(before = '', after = 'some text') or, more commonly," + + " when a you have a transform like core.replace(before = 'some text'," + + " after = '') and is reversed in another workflow. The effect of this" + + " transform is not what you want, as it will replace every single" + + " character with 'some text'. In the case of the reverse, the fix is" + + " to either wrap the core.replace in: core.transform([core.replace" + + "(...)], reversal =[]) so that it doesn't do anything on the reversal" + + " or, even better, to use a reversible scrubber like" + + " core.replace(before = 'confidential text', after = 'some text that" + + " is safe to be public'): " + replacer.GetLocation()); + } + string transformed = replacer.Replace(originalFileContent); + if (!originalFileContent.Equals(transformed)) + { + changed.Add(file); + File.WriteAllBytes(file.GetPath(), Encoding.UTF8.GetBytes(transformed)); + } + } + lock (_lock) + { + _matchedFile |= matchedFile; + _changed.AddRange(changed); + } + // We cannot return null here. + return true; + } + } + + public Replacer CreateReplacer() => + _before.CreateReplacer(_after, _firstOnly, _multiline, _patternsToIgnore); + + public Glob GetPaths() => _paths; + + public Location Location() => _location; +} diff --git a/src/Copybara.Core/Transform/ReplaceMapper.cs b/src/Copybara.Core/Transform/ReplaceMapper.cs new file mode 100644 index 000000000..7001c8325 --- /dev/null +++ b/src/Copybara.Core/Transform/ReplaceMapper.cs @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using Copybara.Common; +using Replacer = Copybara.TemplateToken.RegexTemplateTokens.Replacer; + +namespace Copybara.Transform; + +public class ReplaceMapper : IReversibleFunction +{ + private readonly ImmutableArray _replaces; + private readonly bool _all; + + public ReplaceMapper(ImmutableArray replaces, bool all) + { + _replaces = replaces; + _all = all; + } + + public ReplaceMapper(IReadOnlyList replaces, bool all) + : this(replaces.ToImmutableArray(), all) + { + } + + // Cache of the (relatively expensive) Replacer instances keyed by their Replace, mirroring the + // ThreadLocal weak/soft LoadingCache in the Java original. + [ThreadStatic] + private static ConditionalWeakTable? _replaceCache; + + private static Replacer GetReplacer(Replace replace) + { + _replaceCache ??= new ConditionalWeakTable(); + return _replaceCache.GetValue(replace, r => r.CreateReplacer()); + } + + public IReversibleFunction ReverseMapping() + { + var builder = ImmutableArray.CreateBuilder(_replaces.Length); + foreach (Replace replace in _replaces) + { + builder.Add((Replace)replace.Reverse()); + } + return new ReplaceMapper(builder.ToImmutable(), _all); + } + + public string Apply(string s) + { + string replacement = s; + foreach (Replace replace in _replaces) + { + Replacer replacer = GetReplacer(replace); + replacement = replacer.Replace(replacement); + if (_all) + { + continue; + } + if (replacement.Equals(s)) + { + continue; + } + return replacement; + } + return replacement; + } +} diff --git a/src/Copybara.Core/Transform/Sequence.cs b/src/Copybara.Core/Transform/Sequence.cs new file mode 100644 index 000000000..ff004b2d1 --- /dev/null +++ b/src/Copybara.Core/Transform/Sequence.cs @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Profiler; +using Starlark.Eval; + +namespace Copybara.Transform; + +/// A transformation that runs a sequence of delegate transformations. +public class Sequence : ITransformation +{ + private readonly string? _name; + private readonly Profiler.Profiler _profiler; + private readonly WorkflowOptions _workflowOptions; + private readonly ImmutableArray _sequence; + private readonly NoopBehavior _noopBehavior; + + internal Sequence( + Profiler.Profiler profiler, + string? name, + WorkflowOptions workflowOptions, + ImmutableArray sequence, + NoopBehavior noopBehavior) + { + _profiler = Preconditions.CheckNotNull(profiler); + _name = name; + _workflowOptions = workflowOptions; + _sequence = sequence; + _noopBehavior = noopBehavior; + } + + public TransformationStatus Transform(TransformWork work) + { + var transformationList = GetTransformations(); + + bool someTransformWasSuccess = false; + + if (_name != null) + { + if (_workflowOptions.SkipTransforms.Contains(_name)) + { + work.GetConsole().WarnFmt("Skipping transform block {0}", _name); + return TransformationStatus.Success(); + } + work.GetConsole().ProgressFmt("Running transform block {0}", _name); + } + for (int i = 0; i < transformationList.Count; i++) + { + // Only check the cache in between consecutive Transforms. + if (i != 0) + { + work.ValidateTreeStateCache(); + } + + ITransformation transformation = transformationList[i]; + work.GetConsole().Progress( + GetTransformMessage(transformation, i, transformationList.Count)); + TransformationStatus status = RunOneTransform(work, transformation); + + if (status.IsNoop()) + { + if (_noopBehavior == NoopBehavior.FAIL_IF_ANY_NOOP) + { + status.ThrowException(work.GetConsole(), _workflowOptions.IgnoreNoop); + } + else if (_noopBehavior == NoopBehavior.NOOP_IF_ANY_NOOP) + { + if (_workflowOptions.IgnoreNoop) + { + status.Warn(work.GetConsole()); + } + else + { + return status; + } + } + else if (work.GetConsole().IsVerbose) + { + status.Warn(work.GetConsole()); + } + } + + someTransformWasSuccess |= status.IsSuccess(); + } + + if (_noopBehavior == NoopBehavior.NOOP_IF_ALL_NOOP && !someTransformWasSuccess) + { + return TransformationStatus.Noop( + $"{this} was a no-op because all wrapped transforms were no-ops"); + } + + return TransformationStatus.Success(); + } + + private string GetTransformMessage( + ITransformation transform, int currentTransformIndex, int transformListSize) + { + string transformMsg = transform.Describe(); + if (transformListSize > 1) + { + transformMsg = _name != null + ? string.Format( + "[{0,2}/{1}] Transform block {2} - {3}", + currentTransformIndex + 1, transformListSize, _name, transformMsg) + : string.Format( + "[{0,2}/{1}] Transform {2}", + currentTransformIndex + 1, transformListSize, transformMsg); + } + return transformMsg; + } + + private IReadOnlyList GetTransformations() + { + if (!_workflowOptions.JoinTransformations()) + { + return _sequence; + } + var result = new List(_sequence.Length); + ITransformation? prev = null; + foreach (ITransformation transformation in _sequence) + { + if (prev != null && prev.CanJoin(transformation)) + { + prev = prev.Join(transformation); + } + else + { + if (prev != null) + { + result.Add(prev); + } + prev = transformation; + } + } + if (prev != null) + { + result.Add(prev); + } + return result; + } + + private TransformationStatus RunOneTransform(TransformWork work, ITransformation transform) + { + using (_profiler.Start(transform.Describe().Replace('/', ' '))) + { + return transform.Transform(work); + } + } + + public ITransformation Reverse() + { + var list = ImmutableArray.CreateBuilder(_sequence.Length); + foreach (ITransformation element in _sequence) + { + list.Add(element.Reverse()); + } + var reversed = list.ToImmutable(); + return new Sequence( + _profiler, _name, _workflowOptions, + ImmutableArray.CreateRange(reversed.Reverse()), _noopBehavior); + } + + public IReadOnlyList GetSequence() => _sequence; + + /// returns a string like "Sequence foobar: [a, b, c]". + public override string ToString() => + string.Format( + "Sequence{0}: [{1}]", + _name != null ? " " + _name : "", + string.Join(", ", _sequence)); + + public string Describe() => "sequence"; + + /// + /// Create a sequence from a list of native and Skylark transforms. + /// + /// a description of the argument being converted, such as its name. + public static Sequence FromConfig( + Profiler.Profiler profiler, + string? name, + WorkflowOptions workflowOptions, + IEnumerable elements, + string description, + StarlarkThread.PrintHandler printHandler, + Func transformWrapper, + NoopBehavior noopBehavior) + { + var transformations = ImmutableArray.CreateBuilder(); + foreach (object? element in elements) + { + transformations.Add( + transformWrapper( + Transformations.ToTransformation(element, description, printHandler))); + } + return new Sequence( + profiler, name, workflowOptions, transformations.ToImmutable(), noopBehavior); + } + + /// + /// An enum to specify how a should handle a no-op occurring in one of its + /// child s. + /// + public enum NoopBehavior + { + /// + /// No matter how many of the wrapped Transformation no-op, this Sequence is always considered + /// to be a successful op. + /// + IGNORE_NOOP, + + /// + /// If at least 1 of the wrapped transformations is a no-op, this Sequence will also be + /// considered a no-op. The remainder of the wrapped transformations will not be run. + /// + NOOP_IF_ANY_NOOP, + + /// + /// This Sequence will run all of the wrapped transformations. If all of them are no-ops + /// (including the case where the transformation list is empty), this Sequence is considered + /// to be a no-op. + /// + NOOP_IF_ALL_NOOP, + + /// + /// If at least 1 of the wrapped transformation is a no-op, this Sequence will fail + /// immediately, even if another Sequence with is wrapping this one. + /// + FAIL_IF_ANY_NOOP, + } +} diff --git a/src/Copybara.Core/Transform/SkylarkConsole.cs b/src/Copybara.Core/Transform/SkylarkConsole.cs new file mode 100644 index 000000000..0563af91f --- /dev/null +++ b/src/Copybara.Core/Transform/SkylarkConsole.cs @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Util.Console; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Transform; + +/// +/// A console that can be used in skylark transformations to print info, warning or error messages. +/// +[StarlarkBuiltin( + "console", + Doc = + "A console that can be used in skylark transformations to print info, warning or" + + " error messages.")] +public class SkylarkConsole : Console, IStarlarkValue +{ + private int _errorCount; + private readonly Console _delegate; + + public SkylarkConsole(Console @delegate) + { + _delegate = @delegate; + } + + public void StartupMessage(string version) => + throw new NotSupportedException("Shouldn't be called from skylark"); + + [StarlarkMethod( + "error", + Doc = "Show an error in the log. Note that this will stop Copybara execution.")] + public void Error([Param(Name = "message", Doc = "message to log")] string message) + { + _delegate.Error(message); + _errorCount++; + } + + public bool IsVerbose => _delegate.IsVerbose; + + [StarlarkMethod("warn", Doc = "Show a warning in the console")] + public void Warn([Param(Name = "message", Doc = "message to log")] string message) => + _delegate.Warn(message); + + [StarlarkMethod( + "verbose", + Doc = "Show an info message in the console if verbose logging is enabled.")] + public void Verbose([Param(Name = "message", Doc = "message to log")] string message) => + _delegate.Verbose(message); + + [StarlarkMethod("info", Doc = "Show an info message in the console")] + public void Info([Param(Name = "message", Doc = "message to log")] string message) => + _delegate.Info(message); + + [StarlarkMethod("progress", Doc = "Show a progress message in the console")] + public void Progress([Param(Name = "message", Doc = "message to log")] string progress) => + _delegate.Progress(progress); + + public bool PromptConfirmation(string message) => + throw new NotSupportedException("Shouldn't be called from skylark"); + + public string Colorize(AnsiColor ansiColor, string message) => + _delegate.Colorize(ansiColor, message); + + public string Ask(string msg, string? defaultAnswer, Func validator) => + _delegate.Ask(msg, defaultAnswer, validator); + + /// Print a format string as error on the console. + public void ErrorFmt(string format, params object?[] args) + { + Error(ConsoleFormat.Printf(format, args)); + } + + /// Print a format string as warn on the console. + public void WarnFmt(string format, params object?[] args) + { + Warn(ConsoleFormat.Printf(format, args)); + } + + /// Print a format string as info on the console. + public void InfoFmt(string format, params object?[] args) + { + Info(ConsoleFormat.Printf(format, args)); + } + + /// Print a format string as progress on the console. + public void ProgressFmt(string format, params object?[] args) + { + Progress(ConsoleFormat.Printf(format, args)); + } + + public int GetErrorCount() => _errorCount; +} diff --git a/src/Copybara.Core/Transform/SkylarkTransformation.cs b/src/Copybara.Core/Transform/SkylarkTransformation.cs new file mode 100644 index 000000000..c7a31bc37 --- /dev/null +++ b/src/Copybara.Core/Transform/SkylarkTransformation.cs @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using Copybara.Common; +using Copybara.Exceptions; +using Starlark.Eval; +using Starlark.Syntax; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Transform; + +/// A transformation that uses a Skylark function to transform the code. +public class SkylarkTransformation : ITransformation +{ + private readonly IStarlarkCallable _function; + private readonly Dict _params; + private readonly StarlarkThread.PrintHandler _printHandler; + + public SkylarkTransformation( + IStarlarkCallable function, Dict @params, StarlarkThread.PrintHandler printHandler) + { + _function = Preconditions.CheckNotNull(function); + _params = Preconditions.CheckNotNull(@params); + _printHandler = Preconditions.CheckNotNull(printHandler); + } + + public TransformationStatus Transform(TransformWork work) + { + var skylarkConsole = new SkylarkConsole(work.GetConsole()); + TransformWork skylarkWork = work.WithConsole(skylarkConsole).WithParams(_params); + TransformationStatus status = TransformationStatus.Success(); + using (var mu = Mutability.Create("dynamic_transform")) + { + StarlarkThread thread = StarlarkThread.CreateTransient(mu, StarlarkSemantics.DEFAULT); + thread.SetPrintHandler(_printHandler); + try + { + object? result = StarlarkRt.Call( + thread, + _function, + new object?[] { skylarkWork }, + new Dictionary()); + result = ReferenceEquals(result, StarlarkRt.None) + ? TransformationStatus.Success() + : result; + ValidationException.CheckCondition( + result is TransformationStatus, + "Dynamic transforms functions should return nothing or objects of type {0}, but" + + " '{1}' returned: {2}", + TransformationStatus.StarlarkTypeName, + Describe(), + result!); + status = (TransformationStatus)result!; + } + catch (EvalException e) + { + switch (e.InnerException) + { + case EmptyChangeException ece: + throw ece; + case RepoException re: + throw new RepoException( + $"Error while executing the skylark transformation {Describe()}:" + + $" {e.Message}", + re); + default: + throw new ValidationException( + $"Error while executing the skylark transformation {Describe()}:" + + $" {e.Message}", + e); + } + } + finally + { + work.UpdateFrom(skylarkWork); + } + } + + ValidationException.CheckCondition( + skylarkConsole.GetErrorCount() == 0, + "{0} error(s) while executing {1}", + skylarkConsole.GetErrorCount(), + Describe()); + return status; + } + + public ITransformation Reverse() => new ExplicitReversal(IntentionalNoop.Instance, this); + + public string Describe() => _function.Name; + + public override string ToString() + { + string camelCaseName = LowerUnderscoreToUpperCamel(Describe()); + if (camelCaseName.EndsWith("Impl", StringComparison.Ordinal)) + { + camelCaseName = camelCaseName.Substring(0, camelCaseName.Length - 4); + } + var builder = new StringBuilder(camelCaseName).Append('{'); + bool first = true; + foreach (var e in _params.Entries) + { + if (!first) + { + builder.Append(", "); + } + first = false; + builder.Append(e.Key).Append('=').Append(e.Value); + } + return builder.Append('}').ToString(); + } + + public Location Location() => _function.Location; + + // Port of Guava CaseFormat.LOWER_UNDERSCORE.to(UPPER_CAMEL, ...). + private static string LowerUnderscoreToUpperCamel(string value) + { + var sb = new StringBuilder(value.Length); + bool upperNext = true; + foreach (char c in value) + { + if (c == '_') + { + upperNext = true; + continue; + } + sb.Append(upperNext ? char.ToUpperInvariant(c) : c); + upperNext = false; + } + return sb.ToString(); + } +} diff --git a/src/Copybara.Core/Transform/TodoReplace.cs b/src/Copybara.Core/Transform/TodoReplace.cs new file mode 100644 index 000000000..553de1a23 --- /dev/null +++ b/src/Copybara.Core/Transform/TodoReplace.cs @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.TreeState; +using Copybara.Util; +using Starlark.Syntax; +using Console = Copybara.Util.Console.Console; +using FileState = Copybara.TreeState.TreeState.FileState; + +namespace Copybara.Transform; + +/// Map Google style TODOs. +public class TodoReplace : ITransformation +{ + private static readonly Regex SingleUserPattern = + new(@"^([ \t]*)([^ \t]*)([ \t]*)$"); + + private readonly Regex _pattern; + private readonly Location _location; + private readonly Glob _glob; + private readonly ImmutableArray _todoTags; + private readonly LocalParallelizer _parallelizer; + private readonly Mode _mode; + private readonly ImmutableDictionary _mapping; + private readonly string? _defaultString; + private readonly Regex? _regexIgnorelist; + + public TodoReplace( + Location location, + Glob glob, + ImmutableArray todoTags, + Mode mode, + IReadOnlyDictionary mapping, + string? defaultString, + LocalParallelizer parallelizer, + Regex? regexIgnorelist) + { + _location = Preconditions.CheckNotNull(location); + _glob = Preconditions.CheckNotNull(glob); + _todoTags = todoTags; + _parallelizer = parallelizer; + Preconditions.CheckArgument(!todoTags.IsEmpty); + _mode = mode; + _mapping = mapping.ToImmutableDictionary(); + _defaultString = defaultString; + if (mode == Mode.USE_DEFAULT || mode == Mode.MAP_OR_DEFAULT) + { + Preconditions.CheckNotNull(defaultString); + } + _regexIgnorelist = regexIgnorelist; + _pattern = CreatePattern(todoTags); + } + + private static Regex CreatePattern(ImmutableArray todoTags) + { + string joined = string.Join("|", todoTags.Select(Regex.Escape)); + return new Regex("((?:" + joined + ") ?)\\((.*?)\\)"); + } + + public TransformationStatus Transform(TransformWork work) + { + work.GetTreeState().NotifyModify( + _parallelizer + .Run( + work.GetTreeState().Find(_glob.RelativeTo(work.GetCheckoutDir())).ToList(), + new BatchRun(this, work.GetConsole())) + .SelectMany(s => s)); + return TransformationStatus.Success(); + } + + private sealed class BatchRun : LocalParallelizer.TransformFunc> + { + private readonly TodoReplace _owner; + private readonly Console _console; + + public BatchRun(TodoReplace owner, Console console) + { + _owner = owner; + _console = console; + } + + public ISet Run(IEnumerable files) => _owner.Run(files, _console); + } + + private ISet Run(IEnumerable files, Console console) + { + var modifiedFiles = new HashSet(); + var batchPattern = new Regex(_pattern.ToString(), _pattern.Options); + foreach (FileState file in files) + { + var fileInfo = new FileInfo(file.GetPath()); + if (fileInfo.LinkTarget != null) + { + continue; + } + string content = Encoding.UTF8.GetString(File.ReadAllBytes(file.GetPath())); + var sb = new StringBuilder(); + bool modified = false; + int lastAppend = 0; + foreach (Match matcher in batchPattern.Matches(content)) + { + sb.Append(content, lastAppend, matcher.Index - lastAppend); + lastAppend = matcher.Index + matcher.Length; + if (matcher.Groups[2].Value.Trim().Length == 0) + { + sb.Append(matcher.Value); + continue; + } + var users = matcher.Groups[2].Value.Split(',').ToList(); + var mappedUsers = MapUsers(users, matcher.Value, file.GetPath(), console); + modified |= !users.SequenceEqual(mappedUsers); + string result = matcher.Groups[1].Value; + if (mappedUsers.Count != 0) + { + result += "(" + string.Join(",", mappedUsers) + ")"; + } + sb.Append(result); + } + sb.Append(content, lastAppend, content.Length - lastAppend); + + if (modified) + { + modifiedFiles.Add(file); + File.WriteAllBytes(file.GetPath(), Encoding.UTF8.GetBytes(sb.ToString())); + } + } + return modifiedFiles; + } + + private List MapUsers(List users, string rawText, string path, Console console) + { + var alreadyAdded = new HashSet(); + var result = new List(); + foreach (string rawUser in users) + { + Match matcher = SingleUserPattern.Match(rawUser); + // Throw VE if the pattern doesn't match and mode is MapOrFail. + if (!matcher.Success) + { + ValidationException.CheckCondition( + _mode != Mode.MAP_OR_FAIL, + "Unexpected '{0}' doesn't match expected format", rawUser); + console.WarnFmt("Skipping '{0}' that doesn't match expected format", rawUser); + continue; + } + string prefix = matcher.Groups[1].Value; + string originUser = matcher.Groups[2].Value; + string suffix = matcher.Groups[3].Value; + if (_regexIgnorelist != null && IsFullMatch(_regexIgnorelist, originUser)) + { + result.Add(prefix + originUser + suffix); + continue; + } + switch (_mode) + { + case Mode.MAP_OR_FAIL: + ValidationException.CheckCondition( + _mapping.ContainsKey(originUser), + "Cannot find a mapping '{0}' in '{1}' ({2})", originUser, rawText, path); + goto case Mode.MAP_OR_IGNORE; + case Mode.MAP_OR_IGNORE: + { + string destUser = _mapping.GetValueOrDefault(originUser, originUser); + if (alreadyAdded.Add(destUser)) + { + result.Add(prefix + destUser + suffix); + } + break; + } + case Mode.MAP_OR_DEFAULT: + { + string destUser = _mapping.GetValueOrDefault(originUser, _defaultString!); + if (alreadyAdded.Add(destUser)) + { + result.Add(prefix + destUser + suffix); + } + break; + } + case Mode.SCRUB_NAMES: + break; + case Mode.USE_DEFAULT: + if (alreadyAdded.Add(_defaultString!)) + { + result.Add(prefix + _defaultString + suffix); + } + break; + } + } + return result; + } + + private static bool IsFullMatch(Regex regex, string input) + { + Match m = regex.Match(input); + return m.Success && m.Index == 0 && m.Length == input.Length; + } + + public ITransformation Reverse() + { + if (_mode != Mode.MAP_OR_FAIL && _mode != Mode.MAP_OR_IGNORE) + { + throw new NonReversibleValidationException(_mode + " mode is not reversible"); + } + + var inverse = new Dictionary(); + foreach (var e in _mapping) + { + if (inverse.ContainsKey(e.Value)) + { + throw new NonReversibleValidationException( + "Non-reversible mapping: value already present: " + e.Value); + } + inverse[e.Value] = e.Key; + } + + return new TodoReplace( + _location, _glob, _todoTags, _mode, inverse, _defaultString, _parallelizer, + _regexIgnorelist); + } + + public string Describe() => "Replacing [" + string.Join(", ", _todoTags) + "]"; + + public Location Location() => _location; + + /// How to transforms TODOs in code. + public enum Mode + { + /// Try to use the mapping and if not found fail. + MAP_OR_FAIL, + + /// Try to use the mapping but ignore if no mapping found. + MAP_OR_IGNORE, + + /// Try to use the mapping and use the default if not found. + MAP_OR_DEFAULT, + + /// Scrub all names from TODOs. Transforms 'TODO(foo)' to 'TODO'. + SCRUB_NAMES, + + /// Replace any TODO(foo, bar) with TODO(default_string). + USE_DEFAULT, + } +} diff --git a/src/Copybara.Core/Transform/Transformations.cs b/src/Copybara.Core/Transform/Transformations.cs new file mode 100644 index 000000000..58f6a7cf7 --- /dev/null +++ b/src/Copybara.Core/Transform/Transformations.cs @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Transform; + +/// +/// This class consists exclusively of static methods that operate on or return +/// s. +/// +public static class Transformations +{ + /// + /// Cast a Starlark callable to a . If the input is already a + /// , it is returned unchanged. + /// + /// Many functions in Copybara's Starlark API require s as + /// input. In nearly all cases, the user may instead choose to provide an ordinary Starlark + /// function. This utility method converts those functions into objects with the necessary type. + /// + /// + /// the object to cast to a Transformation. + /// + /// the name of the field for which this object was provided by the user as a parameter, used for + /// error messages. + /// + /// + /// the to use for the thread which runs this Starlark + /// function. + /// + public static ITransformation ToTransformation( + object? element, string description, StarlarkThread.PrintHandler printHandler) + { + if (element is IStarlarkCallable callable) + { + return new SkylarkTransformation(callable, Dict.Empty(), printHandler); + } + if (element is ITransformation transformation) + { + return transformation; + } + throw StarlarkRt.Errorf( + "for '{0}' element, got {1}, want function or transformation", + description, StarlarkRt.Type(element)); + } +} diff --git a/src/Copybara.Core/Transform/VerifyDirIsEmptyVisitor.cs b/src/Copybara.Core/Transform/VerifyDirIsEmptyVisitor.cs new file mode 100644 index 000000000..1e818320d --- /dev/null +++ b/src/Copybara.Core/Transform/VerifyDirIsEmptyVisitor.cs @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; + +namespace Copybara.Transform; + +/// +/// A visitor which recursively verifies there are no files or symlinks in a directory tree. +/// +internal sealed class VerifyDirIsEmptyVisitor +{ + private readonly string _root; + private readonly IPathMatcher? _pathMatcher; + private readonly List _existingFiles = new(); + + public VerifyDirIsEmptyVisitor(string root, IPathMatcher? pathMatcher) + { + _root = Preconditions.CheckNotNull(root); + _pathMatcher = pathMatcher; + } + + public void Walk() + { + if (Directory.Exists(_root)) + { + foreach (var source in Directory.EnumerateFiles(_root, "*", SearchOption.AllDirectories)) + { + string relative = PathOps.Relativize(_root, source); + if (_pathMatcher == null || _pathMatcher.Matches(relative)) + { + _existingFiles.Add(relative); + } + } + } + if (_existingFiles.Count != 0) + { + _existingFiles.Sort(StringComparer.Ordinal); + throw new ValidationException( + $"Files already exist in {_root}: [{string.Join(", ", _existingFiles)}]"); + } + } +} diff --git a/src/Copybara.Core/Transform/VerifyMatch.cs b/src/Copybara.Core/Transform/VerifyMatch.cs new file mode 100644 index 000000000..749716d3d --- /dev/null +++ b/src/Copybara.Core/Transform/VerifyMatch.cs @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.TreeState; +using Copybara.Util; +using Starlark.Syntax; +using StarlarkRt = Starlark.Eval.Starlark; +using FileState = Copybara.TreeState.TreeState.FileState; + +namespace Copybara.Transform; + +/// +/// A source code pseudo-transformation which verifies that all specified files satisfy a RegEx. +/// Does not actually transform any code, but will throw errors on failure. Not applied in reversals. +/// +public sealed class VerifyMatch : ITransformation +{ + private readonly Regex _pattern; + private readonly bool _verifyNoMatch; + private readonly bool _alsoOnReversal; + private readonly Glob _fileMatcherBuilder; + private readonly LocalParallelizer _parallelizer; + private readonly Location _location; + private readonly string? _failureMessage; + + private VerifyMatch( + Regex pattern, + bool verifyNoMatch, + bool alsoOnReversal, + Glob fileMatcherBuilder, + string? failureMessage, + LocalParallelizer parallelizer, + Location location) + { + _pattern = Preconditions.CheckNotNull(pattern); + _verifyNoMatch = verifyNoMatch; + _alsoOnReversal = alsoOnReversal; + _fileMatcherBuilder = Preconditions.CheckNotNull(fileMatcherBuilder); + _failureMessage = failureMessage; + _parallelizer = parallelizer; + _location = Preconditions.CheckNotNull(location); + } + + public override string ToString() => + $"VerifyMatch{{Pattern={_pattern}, verifyNoMatch={_verifyNoMatch}," + + $" also_on_reversal={_alsoOnReversal}, path={_fileMatcherBuilder}}}"; + + public TransformationStatus Transform(TransformWork work) + { + string checkoutDir = work.GetCheckoutDir(); + var files = work.GetTreeState().Find(_fileMatcherBuilder.RelativeTo(checkoutDir)).ToList(); + + var errors = _parallelizer + .Run(files, new BatchRun(this, work.GetCheckoutDir())) + .SelectMany(e => e) + .ToList(); + + int size = 0; + foreach (string error in errors) + { + size++; + work.GetConsole().Error($"Error validating '{Describe()}': {error}"); + } + work.GetTreeState().NotifyNoChange(); + + ValidationException.CheckCondition( + size == 0, + "{0} file(s) failed the validation of {1}, located at {2}.", size, Describe(), _location); + + return TransformationStatus.Success(); + } + + private sealed class BatchRun : LocalParallelizer.TransformFunc> + { + private readonly VerifyMatch _owner; + private readonly string _checkoutDir; + + public BatchRun(VerifyMatch owner, string checkoutDir) + { + _owner = owner; + _checkoutDir = Preconditions.CheckNotNull(checkoutDir); + } + + public List Run(IEnumerable files) + { + var errors = new List(); + var batchPattern = new Regex(_owner._pattern.ToString(), _owner._pattern.Options); + foreach (FileState file in files) + { + var fileInfo = new FileInfo(file.GetPath()); + if (fileInfo.LinkTarget != null) + { + continue; + } + string originalFileContent = + Encoding.UTF8.GetString(File.ReadAllBytes(file.GetPath())); + Match matcher = batchPattern.Match(originalFileContent); + if (_owner._verifyNoMatch == matcher.Success) + { + string error = PathOps.Relativize(_checkoutDir, file.GetPath()); + if (_owner._verifyNoMatch) + { + int line = originalFileContent + .Substring(0, matcher.Index) + .Split('\n') + .Length; + error += string.Format( + " - Unexpected match found at line {0} - '{1}'.\n", line, matcher.Value); + } + else + { + error += " - Expected string was not present.\n"; + } + if (_owner._failureMessage != null) + { + error += _owner._failureMessage + "\n"; + } + errors.Add(error); + } + } + return errors; + } + } + + public string Describe() => $"verify_match '{_pattern}'"; + + public Location Location() => _location; + + public ITransformation Reverse() + { + if (_alsoOnReversal) + { + return new ExplicitReversal(this, this); + } + return new ExplicitReversal(IntentionalNoop.Instance, this); + } + + public static VerifyMatch Create( + Location location, + string regEx, + Glob paths, + bool verifyNoMatch, + bool alsoOnReversal, + string? failureMessage, + LocalParallelizer parallelizer) + { + Regex parsed; + try + { + parsed = new Regex(regEx, RegexOptions.Multiline); + } + catch (ArgumentException ex) + { + throw StarlarkRt.Errorf("Regex '{0}' is invalid: {1}", regEx, ex.Message); + } + return new VerifyMatch( + parsed, verifyNoMatch, alsoOnReversal, paths, failureMessage, parallelizer, location); + } +} diff --git a/src/Copybara.Core/TransformResult.cs b/src/Copybara.Core/TransformResult.cs new file mode 100644 index 000000000..81ba2ccb1 --- /dev/null +++ b/src/Copybara.Core/TransformResult.cs @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Approval; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Revision; +using Copybara.Util; +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// +/// Represents the final result of a transformation, including metadata and actual code to be +/// migrated. +/// +public sealed class TransformResult +{ + private readonly string _path; + private readonly Author _author; + private readonly DateTimeOffset _timestamp; + private readonly string _summary; + private readonly string? _baseline; + private readonly bool _askForConfirmation; + private readonly IRevision _currentRevision; + private readonly IRevision _requestedRevision; + private readonly string? _changeIdentity; + private readonly string _workflowName; + private readonly string? _rawSourceRef; + private readonly Changes _changes; + private readonly bool _setRevId; + private readonly ImmutableArray? _affectedFilesForSmartPrune; + private readonly Func> _labelFinder; + private readonly string _revIdLabel; + private readonly bool _confirmedInOrigin; + private readonly IApprovalsProvider _approvalsProvider; + private readonly IDestinationInfo? _destinationInfo; + + private static DateTimeOffset ReadTimestampOrCurrentTime(IRevision originRef) + { + var refTimestamp = originRef.ReadTimestamp(); + return refTimestamp ?? DateTimeOffset.Now; + } + + public TransformResult( + string path, + IRevision currentRevision, + Author author, + string summary, + IRevision requestedRevision, + string workflowName, + Changes changes, + string? rawSourceRef, + bool setRevId, + Func> labelFinder, + string revIdLabel) + : this( + path, + currentRevision, + author, + ReadTimestampOrCurrentTime(currentRevision), + summary, + baseline: null, + askForConfirmation: false, + requestedRevision, + changeIdentity: null, + workflowName, + changes, + rawSourceRef, + setRevId, + affectedFilesForSmartPrune: null, + labelFinder, + revIdLabel, + confirmedInOrigin: false, + new NoneApprovedProvider(), + destinationInfo: null) + { + } + + private TransformResult( + string path, + IRevision currentRevision, + Author author, + DateTimeOffset timestamp, + string summary, + string? baseline, + bool askForConfirmation, + IRevision requestedRevision, + string? changeIdentity, + string workflowName, + Changes changes, + string? rawSourceRef, + bool setRevId, + ImmutableArray? affectedFilesForSmartPrune, + Func> labelFinder, + string revIdLabel, + bool confirmedInOrigin, + IApprovalsProvider approvalsProvider, + IDestinationInfo? destinationInfo) + { + _path = Preconditions.CheckNotNull(path); + _currentRevision = Preconditions.CheckNotNull(currentRevision); + _author = Preconditions.CheckNotNull(author); + _timestamp = timestamp; + _summary = Preconditions.CheckNotNull(summary); + _baseline = baseline; + _askForConfirmation = askForConfirmation; + _requestedRevision = Preconditions.CheckNotNull(requestedRevision); + _changeIdentity = changeIdentity; + _workflowName = Preconditions.CheckNotNull(workflowName); + _changes = Preconditions.CheckNotNull(changes); + _rawSourceRef = rawSourceRef; + _setRevId = setRevId; + _affectedFilesForSmartPrune = affectedFilesForSmartPrune; + _labelFinder = Preconditions.CheckNotNull(labelFinder); + _revIdLabel = Preconditions.CheckNotNull(revIdLabel); + _confirmedInOrigin = confirmedInOrigin; + _approvalsProvider = approvalsProvider; + _destinationInfo = destinationInfo; + } + + private TransformResult Copy( + string? baseline = null, + bool? askForConfirmation = null, + string? changeIdentity = null, + Changes? changes = null, + string? summary = null, + bool? setRevId = null, + ImmutableArray? affectedFilesForSmartPrune = null, + Func>? labelFinder = null, + bool? confirmedInOrigin = null, + IApprovalsProvider? approvalsProvider = null, + IDestinationInfo? destinationInfo = null, + bool keepBaseline = true, + bool keepChangeIdentity = true, + bool keepAffected = true, + bool keepDestinationInfo = true) => + new( + _path, + _currentRevision, + _author, + _timestamp, + summary ?? _summary, + baseline ?? (keepBaseline ? _baseline : null), + askForConfirmation ?? _askForConfirmation, + _requestedRevision, + changeIdentity ?? (keepChangeIdentity ? _changeIdentity : null), + _workflowName, + changes ?? _changes, + _rawSourceRef, + setRevId ?? _setRevId, + affectedFilesForSmartPrune ?? (keepAffected ? _affectedFilesForSmartPrune : null), + labelFinder ?? _labelFinder, + _revIdLabel, + confirmedInOrigin ?? _confirmedInOrigin, + approvalsProvider ?? _approvalsProvider, + destinationInfo ?? (keepDestinationInfo ? _destinationInfo : null)); + + public TransformResult WithBaseline(string newBaseline) => + Copy(baseline: Preconditions.CheckNotNull(newBaseline)); + + public TransformResult WithSummary(string summary) => Copy(summary: summary); + + public TransformResult WithIdentity(string changeIdentity) => + Copy(changeIdentity: changeIdentity); + + public TransformResult WithApprovalsProvider(IApprovalsProvider approvalsProvider) => + Copy(approvalsProvider: approvalsProvider); + + public TransformResult WithAskForConfirmation(bool askForConfirmation) => + Copy(askForConfirmation: askForConfirmation); + + public TransformResult WithChanges(Changes changes) => Copy(changes: changes); + + public TransformResult WithSetRevId(bool setRevId) => Copy(setRevId: setRevId); + + public TransformResult WithAffectedFilesForSmartPrune( + IReadOnlyList affectedFilesForSmartPrune) => + Copy(affectedFilesForSmartPrune: + Preconditions.CheckNotNull(affectedFilesForSmartPrune).ToImmutableArray()); + + public TransformResult WithLabelFinder(Func> labelMapper) => + Copy(labelFinder: Preconditions.CheckNotNull(labelMapper)); + + public TransformResult WithDiffInOrigin(bool diffInOrigin) => + Copy(confirmedInOrigin: diffInOrigin); + + public TransformResult WithDestinationInfo(IDestinationInfo destinationInfo) => + Copy(destinationInfo: destinationInfo); + + /// Directory containing the tree of files to put in destination. + public string GetPath() => _path; + + /// The current revision being migrated. In ITERATIVE mode this would change per migration. + public IRevision GetCurrentRevision() => _currentRevision; + + /// The revision that the user asked to migrate to. + public IRevision GetRequestedRevision() => _requestedRevision; + + /// A stable identifier that represents an entity in the origin for this change. + public string? GetChangeIdentity() => _changeIdentity; + + /// Destination author to be used. + public Author GetAuthor() => _author; + + /// The moment when the code was submitted to the origin repository. + public DateTimeOffset GetTimestamp() => _timestamp; + + /// A description of the migrated changes to include in the destination's change description. + public string GetSummary() => _summary; + + /// Destination baseline to be used for updating the code in the destination. + public string? GetBaseline() => _baseline; + + /// If the destination should ask for confirmation. + public bool IsAskForConfirmation() => _askForConfirmation; + + /// If true, the destination will not ask for confirmation, instead showing the diff. + public bool IsConfirmedInOrigin() => _confirmedInOrigin; + + /// The workflow name for the migration. + public string GetWorkflowName() => _workflowName; + + /// Get all the labels from the message. + public IReadOnlyList FindAllLabels() => + ChangeMessage.ParseMessage(_summary).GetLabels(); + + /// Data about the set of changes that are being migrated. + public Changes GetChanges() => _changes; + + /// Reference as requested in the CLI if any. + public string? GetRawSourceRef() => _rawSourceRef; + + /// If RevId should be recorded in the destination. + public bool IsSetRevId() => _setRevId; + + /// The label to use for storing the current migration state. + public string GetRevIdLabel() => _revIdLabel; + + /// If not null, the subset of files that Workflow smart_prune detected as really changed. + public IReadOnlyList? GetAffectedFilesForSmartPrune() => + _affectedFilesForSmartPrune is { } arr ? arr : null; + + /// A function that returns all the label values that match a name. + public Func> GetLabelFinder() => _labelFinder; + + /// A function that retrieves the DestinationInfo object supplied by the destination. + public IDestinationInfo? GetDestinationInfo() => _destinationInfo; + + /// Get the approvals provider from the Origin. + public ApprovalsResult GetOriginApprovals( + IReadOnlyList changes, Console console) => + _approvalsProvider.ComputeApprovals(changes.ToImmutableArray(), _labelFinder, console); +} diff --git a/src/Copybara.Core/TransformWork.cs b/src/Copybara.Core/TransformWork.cs new file mode 100644 index 000000000..56f2ca8bd --- /dev/null +++ b/src/Copybara.Core/TransformWork.cs @@ -0,0 +1,677 @@ +/* + * Copyright (C) 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.TreeState; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// +/// Contains information related to an on-going process of repository transformation. +/// +/// This object is passed to the user defined functions in Skylark so that they can personalize +/// the commit message, change the author or run custom transformations. +/// +[StarlarkBuiltin( + "TransformWork", + Doc = + "Data about the set of changes that are being migrated. It includes information about" + + " changes like: the author to be used for commit, change message, etc. You receive a" + + " TransformWork object as an argument when defining a dynamic transform.")] +public sealed class TransformWork : CheckoutFileSystem, ISkylarkContext +{ + internal const string CopybaraContextReferenceLabel = "COPYBARA_CONTEXT_REFERENCE"; + internal const string ContextReferenceLabel = "CONTEXT_REFERENCE"; + internal const string CopybaraLastRev = "COPYBARA_LAST_REV"; + internal const string CopybaraCurrentRev = "COPYBARA_CURRENT_REV"; + internal const string CopybaraCurrentRevDateTime = "COPYBARA_CURRENT_REV_DATE_TIME"; + internal const string CopybaraCurrentMessage = "COPYBARA_CURRENT_MESSAGE"; + internal const string CopybaraAuthor = "COPYBARA_AUTHOR"; + internal const string CopybaraCurrentMessageTitle = "COPYBARA_CURRENT_MESSAGE_TITLE"; + internal const string CopybaraConfigPathLabel = "COPYBARA_CONFIG_PATH"; + internal const string CopybaraWorkflowNameLabel = "COPYBARA_WORKFLOW_NAME"; + + private Metadata _metadata; + private readonly Changes _changes; + private readonly Console _console; + private readonly MigrationInfo _migrationInfo; + private readonly IRevision _resolvedReference; + private readonly TreeState.TreeState _treeState; + private readonly bool _insideExplicitTransform; + private readonly IRevision? _lastRev; + private readonly IRevision? _currentRev; + private readonly Dict _skylarkTransformParams; + private readonly LazyResourceLoader _originApi; + private readonly LazyResourceLoader _destinationApi; + private readonly IResourceSupplier _destinationReader; + private readonly IDestinationInfo? _destinationInfo; + private readonly string _mode; + + public TransformWork( + string checkoutDir, + Metadata metadata, + Changes changes, + Console console, + MigrationInfo migrationInfo, + IRevision resolvedReference, + LazyResourceLoader originApi, + LazyResourceLoader destinationApi, + IResourceSupplier destinationReader, + string mode) + : this( + checkoutDir, + metadata, + changes, + console, + migrationInfo, + resolvedReference, + new TreeState.TreeState(checkoutDir), + insideExplicitTransform: false, + lastRev: null, + currentRev: null, + Dict.Empty(), + originApi, + destinationApi, + destinationReader, + destinationInfo: null, + mode) + { + } + + private TransformWork( + string checkoutDir, + Metadata metadata, + Changes changes, + Console console, + MigrationInfo migrationInfo, + IRevision resolvedReference, + TreeState.TreeState treeState, + bool insideExplicitTransform, + IRevision? lastRev, + IRevision? currentRev, + Dict skylarkTransformParams, + LazyResourceLoader originApi, + LazyResourceLoader destinationApi, + IResourceSupplier destinationReader, + IDestinationInfo? destinationInfo, + string mode) + : base(checkoutDir) + { + _metadata = Preconditions.CheckNotNull(metadata); + _changes = changes; + _console = console; + _migrationInfo = migrationInfo; + _resolvedReference = Preconditions.CheckNotNull(resolvedReference); + _treeState = treeState; + _insideExplicitTransform = insideExplicitTransform; + _lastRev = lastRev; + _currentRev = currentRev; + _skylarkTransformParams = skylarkTransformParams; + _originApi = Preconditions.CheckNotNull(originApi); + _destinationApi = Preconditions.CheckNotNull(destinationApi); + _destinationReader = Preconditions.CheckNotNull(destinationReader); + _destinationInfo = destinationInfo; + _mode = mode; + } + + [StarlarkMethod(name: "message", Doc = "Message to be used in the change", StructField = true)] + public string GetMessage() => _metadata.GetMessage(); + + [StarlarkMethod("mode", Doc = "The workflow mode", StructField = true)] + public string GetMode() => _mode; + + [StarlarkMethod("author", Doc = "Author to be used in the change", StructField = true)] + public Author GetAuthor() => _metadata.GetAuthor(); + + [StarlarkMethod( + "params", + Doc = "Parameters for the function if created with core.dynamic_transform", + StructField = true)] + public Dict GetParams() => _skylarkTransformParams; + + [StarlarkMethod("set_message", Doc = "Update the message to be used in the change")] + public void SetMessage([Param(Name = "message")] string message) => + _metadata = _metadata.WithMessage(message); + + public Metadata GetMetadata() => _metadata; + + public void AddHiddenLabels(ImmutableListMultimap hiddenLabels) => + _metadata = _metadata.WithHiddenLabels(hiddenLabels); + + [StarlarkMethod( + "run", + Doc = + "Run a glob or a transform. For example:
" + + "files = ctx.run(glob(['**.java']))
or
" + + "ctx.run(core.move(\"foo\", \"bar\"))
or
")] + public object Run( + [Param( + Name = "runnable", + Doc = "When `runnable` is a `glob`, returns a list of files in the workdir which it" + + " matches. When `runnable` is a `transformation`, runs it in the workdir.", + AllowedTypes = new[] { typeof(Glob), typeof(ITransformation) })] + object runnable) + { + if (runnable is Glob glob) + { + return List(glob); + } + if (runnable is ITransformation transformation) + { + // Can never trust the cache when inside a dynamic transform. + _treeState.ClearCache(); + return transformation.Transform(this); + } + + throw StarlarkRt.Errorf( + "Only globs or transforms can be run, but '{0}' is of type {1}", + runnable, runnable.GetType()); + } + + [StarlarkMethod("success", Doc = "The status returned by a successful Transformation")] + public TransformationStatus SuccessStatus() => TransformationStatus.Success(); + + [StarlarkMethod("noop", Doc = "The status returned by a no-op Transformation")] + public TransformationStatus NoopStatus([Param(Name = "message")] string message) => + TransformationStatus.Noop(message); + + [StarlarkMethod("add_label", Doc = "Add a label to the end of the description")] + public void AddLabel( + [Param(Name = "label", Doc = "The label to add")] string label, + [Param(Name = "value", Doc = "The new value for the label")] string value, + [Param(Name = "separator", Doc = "The separator to use for the label", DefaultValue = "\"=\"")] + string separator, + [Param( + Name = "hidden", + Doc = "Don't show the label in the message but only keep it internally", + Named = true, + Positional = false, + DefaultValue = "False")] + bool hidden) + { + if (hidden) + { + var builder = ImmutableListMultimap.CreateBuilder(); + builder.Put(label, value); + AddHiddenLabels(builder.Build()); + } + else + { + SetMessage(ChangeMessage.ParseMessage(GetMessage()) + .WithLabel(label, separator, value) + .ToString()); + } + } + + [StarlarkMethod( + "add_or_replace_label", + Doc = "Replace an existing label or add it to the end of the description")] + public void AddOrReplaceLabel( + [Param(Name = "label", Doc = "The label to add/replace")] string label, + [Param(Name = "value", Doc = "The new value for the label")] string value, + [Param(Name = "separator", Doc = "The separator to use for the label", DefaultValue = "\"=\"")] + string separator) => + SetMessage(ChangeMessage.ParseMessage(GetMessage()) + .WithNewOrReplacedLabel(label, separator, value) + .ToString()); + + [StarlarkMethod( + "add_text_before_labels", + Doc = "Add a text to the description before the labels paragraph")] + public void AddTextBeforeLabels([Param(Name = "text")] string text) + { + var message = ChangeMessage.ParseMessage(GetMessage()); + message = message.WithText(message.GetText() + '\n' + text); + SetMessage(message.ToString()); + } + + [StarlarkMethod("replace_label", Doc = "Replace a label if it exist in the message")] + public void ReplaceLabel( + [Param(Name = "label", Doc = "The label to replace")] string labelName, + [Param(Name = "value", Doc = "The new value for the label")] string value, + [Param(Name = "separator", Doc = "The separator to use for the label", DefaultValue = "\"=\"")] + string separator, + [Param( + Name = "whole_message", + Doc = "By default Copybara only looks in the last paragraph for labels. This flag makes" + + " it replace labels in the whole message.", + DefaultValue = "False")] + bool wholeMessage) => + SetMessage(ParseMessage(wholeMessage) + .WithReplacedLabel(labelName, separator, value) + .ToString()); + + [StarlarkMethod("remove_label", Doc = "Remove a label from the message if present")] + public void RemoveLabel( + [Param(Name = "label", Doc = "The label to delete")] string label, + [Param( + Name = "whole_message", + Doc = "By default Copybara only looks in the last paragraph for labels. This flag makes" + + " it replace labels in the whole message.", + DefaultValue = "False")] + bool wholeMessage) => + SetMessage(ParseMessage(wholeMessage).WithRemovedLabelByName(label).ToString()); + + public void RemoveLabelWithValue(string label, string value, bool wholeMessage) => + SetMessage(ParseMessage(wholeMessage).WithRemovedLabelByNameAndValue(label, value).ToString()); + + [StarlarkMethod("now_as_string", Doc = "Get current date as a string")] + public string FormatDate( + [Param(Name = "format", Doc = "The format to use.", DefaultValue = "\"yyyy-MM-dd\"")] + string format, + [Param(Name = "zone", Doc = "The timezone id to use. By default UTC", DefaultValue = "\"UTC\"")] + string zone) + { + TimeZoneInfo tz = zone == "UTC" ? TimeZoneInfo.Utc : TimeZoneInfo.FindSystemTimeZoneById(zone); + var now = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, tz); + return now.ToString(JavaToNetDateFormat(format), CultureInfo.InvariantCulture); + } + + private static string JavaToNetDateFormat(string format) => + // Java and .NET share the common y/M/d/H/m/s tokens used in Copybara configs. + format; + + private ChangeMessage ParseMessage(bool wholeMessage) => + wholeMessage + ? ChangeMessage.ParseAllAsLabels(GetMessage()) + : ChangeMessage.ParseMessage(GetMessage()); + + private const string FindLabelDetails = + "First it looks at the generated message (that is, labels that might have been added by" + + " previous transformations), then it looks in all the commit messages being imported" + + " and finally in the resolved reference passed in the CLI."; + + [StarlarkMethod( + "find_label", + Doc = "Tries to find a label. " + FindLabelDetails + + " Returns the first such label value found this way.", + AllowReturnNones = true)] + public string? GetLabel([Param(Name = "label", Doc = "The label to find")] string label) + { + var labelValues = FindLabelValues(label, all: false); + return labelValues.Count == 0 ? null : labelValues[labelValues.Count - 1]; + } + + [StarlarkMethod( + "find_all_labels", + Doc = "Tries to find all the values for a label. " + FindLabelDetails)] + public IReadOnlyList GetAllLabels( + [Param(Name = "label", Doc = "The label to find")] string label) => + FindLabelValues(label, all: true); + + [StarlarkMethod( + "origin_api", + Doc = "Returns an api handle for the origin repository.")] + public IEndpoint GetOriginApi() => _originApi.Load(_console); + + [StarlarkMethod( + "destination_api", + Doc = "Returns an api handle for the destination repository.")] + public IEndpoint GetDestinationApi() => _destinationApi.Load(_console); + + [StarlarkMethod( + "destination_reader", + Doc = "Returns a handle to read files from the destination, if supported by the destination.")] + public DestinationReader GetDestinationReader() => _destinationReader.Get(); + + [StarlarkMethod( + "destination_info", + Doc = "Returns an object to store additional configuration and data for the destination.", + AllowReturnNones = true)] + public IDestinationInfo? GetDestinationInfo() => _destinationInfo; + + private IReadOnlyList FindLabelValues(string label, bool all) + { + var coreLabels = GetCoreLabels(); + if (coreLabels.TryGetValue(label, out var coreValue)) + { + return coreValue; + } + var result = new List(); + var msgLabel = GetLabelInMessage(label); + if (msgLabel.Count != 0) + { + result.AddRange(msgLabel.Select(l => l.GetValue())); + if (!all) + { + return result; + } + } + var values = _metadata.GetHiddenLabels().Get(label); + if (values.Length != 0) + { + if (!all) + { + return new List { values[values.Length - 1] }; + } + result.AddRange(values); + } + + // Try to find the label in the current changes migrated. + var currentChanges = _changes.GetCurrent(); + foreach (var changeObj in currentChanges) + { + var change = (Change)changeObj; + var allForSkylark = change.GetLabelsAllForSkylark(); + if (allForSkylark.TryGetValue(label, out var val)) + { + result.AddRange(val); + if (!all) + { + return result; + } + } + var revVal = change.GetRevision().AssociatedLabel(label); + if (revVal.Count != 0) + { + result.AddRange(revVal); + if (!all) + { + return result; + } + } + } + + if (currentChanges.Count == 0 && _currentRev != null) + { + var currentRevLabel = _currentRev.AssociatedLabels().Get(label); + if (currentRevLabel.Length != 0) + { + result.AddRange(currentRevLabel); + if (!all) + { + return result; + } + } + } + + // Try to find the label in the resolved reference. + var resolvedRefLabel = _resolvedReference.AssociatedLabels().Get(label); + if (resolvedRefLabel.Length != 0) + { + result.AddRange(resolvedRefLabel); + if (!all) + { + return result; + } + } + + return result; + } + + private IReadOnlyList GetLabelInMessage(string name) => + ParseMessage(wholeMessage: true).GetLabels().Where(label => label.IsLabel(name)).ToList(); + + [StarlarkMethod("set_author", Doc = "Update the author to be used in the change")] + public void SetAuthor([Param(Name = "author")] Author author) => + _metadata = _metadata.WithAuthor(author); + + [StarlarkMethod("changes", Doc = "List of changes that will be migrated", StructField = true)] + public Changes GetChanges() => _changes; + + [StarlarkMethod( + "console", + Doc = "Get an instance of the console to report errors or warnings", + StructField = true)] + public Console GetConsole() => _console; + + [StarlarkMethod( + "fill_template", + Doc = "Replaces variables in templates with the values from this revision.")] + public string FillTemplate( + [Param(Name = "template", Doc = "The template to use", Named = true)] string template) => + LabelFinder.MapLabels(GetAllLabels, template); + + public MigrationInfo GetMigrationInfo() => _migrationInfo; + + public IRevision GetResolvedReference() => _resolvedReference; + + public IRevision? GetCurrentRev() => _currentRev; + + public bool IsInsideExplicitTransform() => _insideExplicitTransform; + + /// Create a clone of the transform work but use a different console. + public TransformWork WithConsole(Console newConsole) => + new( + GetCheckoutDir(), + _metadata, + _changes, + Preconditions.CheckNotNull(newConsole), + _migrationInfo, + _resolvedReference, + _treeState, + _insideExplicitTransform, + _lastRev, + _currentRev, + _skylarkTransformParams, + _originApi, + _destinationApi, + _destinationReader, + _destinationInfo, + _mode); + + /// Clear the TreeState cache, unless we can confirm that it is up-to-date. + public void ValidateTreeStateCache() => _treeState.MaybeClearCache(); + + public TransformWork WithParams(Dict @params) => + new( + GetCheckoutDir(), + _metadata, + _changes, + _console, + _migrationInfo, + _resolvedReference, + _treeState, + _insideExplicitTransform, + _lastRev, + _currentRev, + Preconditions.CheckNotNull(@params), + _originApi, + _destinationApi, + _destinationReader, + _destinationInfo, + _mode); + + public TransformWork WithChanges(Changes changes) => + new( + GetCheckoutDir(), + _metadata, + Preconditions.CheckNotNull(changes), + _console, + _migrationInfo, + _resolvedReference, + _treeState, + _insideExplicitTransform, + _lastRev, + _currentRev, + _skylarkTransformParams, + _originApi, + _destinationApi, + _destinationReader, + _destinationInfo, + _mode); + + public TransformWork WithLastRev(IRevision? previousRef) => + new( + GetCheckoutDir(), + _metadata, + _changes, + _console, + _migrationInfo, + _resolvedReference, + _treeState, + _insideExplicitTransform, + previousRef, + _currentRev, + _skylarkTransformParams, + _originApi, + _destinationApi, + _destinationReader, + _destinationInfo, + _mode); + + public TransformWork WithResolvedReference(IRevision resolvedReference) => + new( + GetCheckoutDir(), + _metadata, + _changes, + _console, + _migrationInfo, + Preconditions.CheckNotNull(resolvedReference), + _treeState, + _insideExplicitTransform, + _lastRev, + _currentRev, + _skylarkTransformParams, + _originApi, + _destinationApi, + _destinationReader, + _destinationInfo, + _mode); + + public TransformWork InsideExplicitTransform() => + new( + GetCheckoutDir(), + _metadata, + _changes, + _console, + _migrationInfo, + _resolvedReference, + _treeState, + insideExplicitTransform: true, + _lastRev, + _currentRev, + _skylarkTransformParams, + _originApi, + _destinationApi, + _destinationReader, + _destinationInfo, + _mode); + + public TransformWork WithCurrentRev(IRevision currentRev) => + new( + GetCheckoutDir(), + _metadata, + _changes, + _console, + _migrationInfo, + _resolvedReference, + _treeState, + _insideExplicitTransform, + _lastRev, + Preconditions.CheckNotNull(currentRev), + _skylarkTransformParams, + _originApi, + _destinationApi, + _destinationReader, + _destinationInfo, + _mode); + + public TransformWork WithDestinationInfo(IDestinationInfo? newDestinationInfo) => + new( + GetCheckoutDir(), + _metadata, + _changes, + _console, + _migrationInfo, + _resolvedReference, + _treeState, + _insideExplicitTransform, + _lastRev, + _currentRev, + _skylarkTransformParams, + _originApi, + _destinationApi, + _destinationReader, + newDestinationInfo, + _mode); + + /// Update mutable state from another worker data. + public void UpdateFrom(TransformWork skylarkWork) => _metadata = skylarkWork._metadata; + + public TreeState.TreeState GetTreeState() => _treeState; + + private IReadOnlyDictionary> GetCoreLabels() + { + var labels = new Dictionary>(); + string? ctxRef = !string.IsNullOrEmpty(_resolvedReference.ContextReference()) + ? _resolvedReference.ContextReference() + : _resolvedReference.FixedReference(); + labels[CopybaraContextReferenceLabel] = + ctxRef == null ? Array.Empty() : new[] { ctxRef }; + labels[ContextReferenceLabel] = labels[CopybaraContextReferenceLabel]; + + labels[CopybaraLastRev] = _lastRev == null + ? Array.Empty() + : new[] { System.Text.RegularExpressions.Regex.Replace(_lastRev.AsString(), " .*", "") }; + + labels[CopybaraCurrentRev] = _currentRev == null + ? Array.Empty() + : new[] { System.Text.RegularExpressions.Regex.Replace(_currentRev.AsString(), " .*", "") }; + + SetDateForCurrentRev(labels); + + labels[CopybaraCurrentMessage] = new[] { GetMessage() }; + labels[CopybaraAuthor] = new[] { GetAuthor().Name, GetAuthor().Email }; + labels[CopybaraCurrentMessageTitle] = + new[] { Change.ExtractFirstLine(_metadata.GetMessage()) }; + return labels; + } + + private void SetDateForCurrentRev(Dictionary> labels) + { + if (_currentRev == null) + { + labels[CopybaraCurrentRevDateTime] = Array.Empty(); + return; + } + DateTimeOffset? time = null; + try + { + time = _currentRev.ReadTimestamp(); + } + catch (RepoException e) + { + _console.Warn("Cannot access date for change " + _currentRev.AsString() + ": " + e.Message); + } + labels[CopybaraCurrentRevDateTime] = time != null + ? new[] { time.Value.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture) } + : Array.Empty(); + } + + public void OnFinish(object? result, object context) => + ValidationException.CheckCondition( + result == null || result.Equals(StarlarkRt.None), + "Transform work cannot return any result but returned: {0}", + result); + + /// Supplier of a resource that may throw validation/repo exceptions. + public interface IResourceSupplier + { + T Get(); + } +} diff --git a/src/Copybara.Core/Transformation.cs b/src/Copybara.Core/Transformation.cs new file mode 100644 index 000000000..712ef56ee --- /dev/null +++ b/src/Copybara.Core/Transformation.cs @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Starlark.Annot; +using Starlark.Eval; +using Starlark.Syntax; + +namespace Copybara; + +/// Interface implemented by all source code transformations. +[StarlarkBuiltin( + "transformation", + Doc = + "A single operation which modifies the source checked out from the origin, prior to writing" + + " it to the destination. Transformations can also be used to perform validations or" + + " checks.

Many common transformations are provided by the built-in" + + " libraries, such as core.

Custom" + + " transformations can be defined in Starlark code via core.dynamic_transform.")] +public interface ITransformation : IStarlarkValue +{ + /// Transforms the files inside the checkout dir specified by . + TransformationStatus Transform(TransformWork work); + + /// + /// Returns a transformation which runs this transformation in reverse. + /// + /// + /// if the transform is not reversible. + /// + ITransformation Reverse(); + + /// + /// Return a high level description of what the transform is doing. Note that this should not be + /// the method but something more user friendly. + /// + string Describe(); + + /// Starlark location of the transformation. + Location Location() => Starlark.Syntax.Location.BUILTIN; + + bool CanJoin(ITransformation transformation) => false; + + ITransformation Join(ITransformation next) => + throw new InvalidOperationException($"Unexpected join call for {this} and {next}"); +} diff --git a/src/Copybara.Core/TransformationStatus.cs b/src/Copybara.Core/TransformationStatus.cs new file mode 100644 index 000000000..65189e62d --- /dev/null +++ b/src/Copybara.Core/TransformationStatus.cs @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara; + +/// The status of a Transformation that was just run. Either a 'success' or a 'no-op'. +[StarlarkBuiltin( + TransformationStatus.StarlarkTypeName, + Doc = "The status of a Transformation that was just run. Either a 'success' or a 'no-op'.")] +public sealed class TransformationStatus : IStarlarkValue +{ + public const string StarlarkTypeName = "transformation_status"; + + private readonly bool _isSuccess; + private readonly string? _message; + + private TransformationStatus(bool success, string? message) + { + _isSuccess = success; + _message = message; + } + + public static TransformationStatus Success() => new(true, null); + + public static TransformationStatus Noop(string message) => new(false, message); + + [StarlarkMethod( + "is_success", + Doc = "Whether this status has the value SUCCESS.", + StructField = true)] + public bool IsSuccess() => _isSuccess; + + [StarlarkMethod("is_noop", Doc = "Whether this status has the value NO-OP.", StructField = true)] + public bool IsNoop() => !_isSuccess; + + public string GetMessage() + { + Preconditions.CheckState(IsNoop(), "Can only get message if the Transform was a no-op."); + return _message!; + } + + public void Warn(Console console) + { + Preconditions.CheckState(IsNoop(), "Can only warn if the Transform was a no-op."); + console.Warn("NOOP: " + _message); + } + + public void ThrowException(Console console, bool ignoreNoop) + { + Preconditions.CheckState(IsNoop(), "Can only throw if the Transform was a no-op."); + if (ignoreNoop) + { + Warn(console); + return; + } + throw new VoidOperationException( + $"{_message}. Use --ignore-noop if you want to ignore this error"); + } + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var that = (TransformationStatus)o; + return string.Equals(_message, that._message) && _isSuccess == that._isSuccess; + } + + public override int GetHashCode() => HashCode.Combine(_isSuccess, _message); + + public override string ToString() => + $"{StarlarkTypeName}(isSuccess={_isSuccess}, message={_message})"; +} diff --git a/src/Copybara.Core/TreeState/TreeState.cs b/src/Copybara.Core/TreeState/TreeState.cs new file mode 100644 index 000000000..233c18684 --- /dev/null +++ b/src/Copybara.Core/TreeState/TreeState.cs @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Util; + +namespace Copybara.TreeState; + +/// +/// An object that allows to do potentially cached filesystem lookups. +/// +/// In particular, if a transform does lookups (using find) and then notifies the affected +/// files, the next transform gets a cached version of the TreeState. +/// +public class TreeState +{ + /// + /// An object that contains a path found in the . + /// + /// Wrapped so that we can include things like the hash of the file in the future. + /// + public sealed class FileState : IEquatable + { + private readonly string _path; + + public FileState(string path) + { + _path = Preconditions.CheckNotNull(path); + } + + public string GetPath() => _path; + + public bool Equals(FileState? other) => other != null && _path == other._path; + + public override bool Equals(object? o) => o is FileState fs && Equals(fs); + + public override int GetHashCode() => _path.GetHashCode(); + + public override string ToString() => _path; + } + + private readonly string _checkoutDir; + private bool _isCached; + private bool _notified; + private Dictionary _files = new(); + + // Small LRU-ish cache of matches keyed by matcher, mirroring the Guava LoadingCache (max 10). + private readonly Dictionary> _cachedMatches = new(); + + public TreeState(string checkoutDir) + { + _checkoutDir = checkoutDir; + } + + /// Find a set of files in the checkout dir, using a . + public IEnumerable Find(IPathMatcher pathMatcher) + { + if (!_isCached) + { + _files = ReadFileSystem(); + _isCached = true; + } + if (!_cachedMatches.TryGetValue(pathMatcher, out var matches)) + { + matches = Filter(pathMatcher, _files.Values); + if (_cachedMatches.Count >= 10) + { + _cachedMatches.Clear(); + } + _cachedMatches[pathMatcher] = matches; + } + return matches; + } + + private static List Filter(IPathMatcher pathMatcher, IEnumerable files) + { + var result = new List(); + foreach (var file in files) + { + if (pathMatcher.Matches(file.GetPath())) + { + result.Add(file); + } + } + return result; + } + + private Dictionary ReadFileSystem() + { + var result = new Dictionary(); + if (!Directory.Exists(_checkoutDir)) + { + return result; + } + foreach (var file in Directory.EnumerateFiles(_checkoutDir, "*", SearchOption.AllDirectories)) + { + result[file] = new FileState(file); + } + return result; + } + + /// Notify the that have been modified. + public void NotifyModify(IEnumerable paths) + { + _notified = true; + foreach (var path in paths) + { + _files[path.GetPath()] = path; + } + } + + /// Not implemented for now. + public void NotifyAdd(IEnumerable path) => + throw new NotSupportedException("Not supported. Don't notify!"); + + /// Not implemented for now. + public void NotifyDelete(IEnumerable path) => + throw new NotSupportedException("Not supported. Don't notify!"); + + public void NotifyNoChange() => _notified = true; + + public bool IsCached() => _isCached; + + public void ClearCache() + { + _isCached = false; + _files = new Dictionary(); + _cachedMatches.Clear(); + _notified = false; + } + + /// + /// If any of the notify* methods were invoked, it will retain the cached version of the + /// TreeState. Otherwise it clears the cache. + /// + /// This method is called in between every pair of Transformations. Unless the previous + /// Transformation calls one of the notify* methods to indicate which files it has touched, we + /// must assume that the cache may be stale. + /// + public void MaybeClearCache() + { + if (!_notified) + { + ClearCache(); + } + _notified = false; + } +} diff --git a/src/Copybara.Core/TreeState/TreeStateUtil.cs b/src/Copybara.Core/TreeState/TreeStateUtil.cs new file mode 100644 index 000000000..fceca1a19 --- /dev/null +++ b/src/Copybara.Core/TreeState/TreeStateUtil.cs @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Util; + +namespace Copybara.TreeState; + +/// +/// Utilities for dealing with objects. Port of +/// com.google.copybara.treestate.TreeStateUtil. +/// +public static class TreeStateUtil +{ + /// Filter a collection of s using an . + public static List Filter( + IPathMatcher pathMatcher, IEnumerable files) => + files.Where(fileState => pathMatcher.Matches(fileState.GetPath())).ToList(); +} diff --git a/src/Copybara.Core/Trigger.cs b/src/Copybara.Core/Trigger.cs new file mode 100644 index 000000000..252c5ec66 --- /dev/null +++ b/src/Copybara.Core/Trigger.cs @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara; + +/// Starter of feedback migration executions. +[StarlarkBuiltin("trigger", Doc = "Starter of feedback migration executions.", Documented = false)] +public interface ITrigger : IStarlarkPrintableValue +{ + IEndpoint GetEndpoint(); + + void IStarlarkPrintableValue.Repr(Printer printer, StarlarkSemantics semantics) => + printer.Append(ToString() ?? ""); + + ImmutableListMultimap Describe(); + + /// Returns a key-value list describing the credentials the endpoint was instantiated with. + IReadOnlyList> DescribeCredentials() => + ImmutableArray>.Empty; +} diff --git a/src/Copybara.Core/Util/DiffFile.cs b/src/Copybara.Core/Util/DiffFile.cs new file mode 100644 index 000000000..3b2ce0acf --- /dev/null +++ b/src/Copybara.Core/Util/DiffFile.cs @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; + +namespace Copybara.Util; + +/// +/// A single changed file, as reported by git diff --name-status. Port of the nested +/// com.google.copybara.util.DiffUtil.DiffFile type (lifted to a top-level type in the C# +/// port). +/// +public sealed class DiffFile +{ + private readonly string _name; + private readonly Operation _operation; + + internal static readonly ImmutableDictionary OpByChar = + Enum.GetValues().ToImmutableDictionary(CharType); + + public DiffFile(string name, Operation operation) + { + _name = Preconditions.CheckNotNull(name); + _operation = operation; + } + + public string GetName() => _name; + + public Operation GetOperation() => _operation; + + /// Git Diff status letters. + public enum Operation + { + ADD, + DELETE, + MODIFIED, + COPY, + RENAME, + TYPE_CHANGE, + UNMERGED, + // X is omitted because it indicates a bug + } + + /// Returns the single-letter git status character for the operation. + internal static string CharType(Operation op) => op switch + { + Operation.ADD => "A", + Operation.DELETE => "D", + Operation.MODIFIED => "M", + Operation.COPY => "C", + Operation.RENAME => "R", + Operation.TYPE_CHANGE => "T", + Operation.UNMERGED => "U", + _ => throw new ArgumentOutOfRangeException(nameof(op), op, null), + }; + + public override string ToString() => + $"DiffFile{{name={_name}, operation={_operation}}}"; +} diff --git a/src/Copybara.Core/Util/DiffUtil.cs b/src/Copybara.Core/Util/DiffUtil.cs new file mode 100644 index 000000000..ab72384b7 --- /dev/null +++ b/src/Copybara.Core/Util/DiffUtil.cs @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Util.Console; + +namespace Copybara.Util; + +/// +/// Diff utilities that are repository-agnostic. Port of +/// com.google.copybara.util.DiffUtil. +/// +/// Diffing is implemented by shelling out to the git binary via +/// (git diff --no-index / git apply), mirroring the +/// upstream FoldersDiff behavior. This uses git as a repo-agnostic diff/patch engine +/// (--git-dir=/dev/null) so no repository is required. +/// +public static class DiffUtil +{ + private static readonly byte[] EmptyDiff = Array.Empty(); + + /// + /// Calculates the diff between two sibling directory trees. + /// + /// Returns the diff as an encoding-independent byte[]. + /// + public static byte[] Diff( + string one, string other, bool verbose, IReadOnlyDictionary environment) => + new FoldersDiff(verbose, environment).Run(GetParent(one), one, other); + + /// + /// Calculates the diff between two sibling directory trees while setting --ignore-cr-at-eol. + /// + /// Returns the diff as an encoding-independent byte[]. + /// + public static byte[] DiffWithIgnoreCrAtEol( + string one, string other, bool verbose, IReadOnlyDictionary environment) => + new FoldersDiff(verbose, environment) + .WithIgnoreCrAtEol() + .Run(GetParent(one), one, other); + + /// + /// Calculates the diff between two files with --ignore-cr-at-eol set. + /// + /// Returns the single file diff as an encoding-independent byte[]. + /// + public static byte[] DiffFileWithIgnoreCrAtEol( + string root, + string one, + string other, + bool verbose, + IReadOnlyDictionary environment) => + new FoldersDiff(verbose, environment) + .WithIgnoreCrAtEol() + .WithSingleFile() + .Run(root, one, other); + + /// Filter a diff output to only include diffs for original files that match a filter. + public static string FilterDiff(byte[] diff, Func pathFilter) + { + bool include = true; + var filteredDiff = new StringBuilder(); + foreach (var line in Encoding.UTF8.GetString(diff).Split('\n')) + { + if (line.StartsWith("diff ", StringComparison.Ordinal)) + { + var diffHeader = line.Split(' '); + // Given a diff in the format of: + // diff --git a/left/copybara/util/Test.java b/right/copybara/util/Test.java + // Returns "left/copybara/util/Test.java" + string path = diffHeader[2].Substring(2); + include = pathFilter(path); + } + if (include) + { + filteredDiff.Append(line).Append('\n'); + } + } + // Nothing to add + if (filteredDiff.Length == 0) + { + return ""; + } + return filteredDiff.ToString(); + } + + /// + /// Return the changed files without computing renames/copies. + /// + /// Each file name is relative to one/other paths. + /// + public static ImmutableArray DiffFiles( + string one, string other, bool verbose, IReadOnlyDictionary? environment) + { + string cmdResult = + Encoding.UTF8.GetString( + new FoldersDiff(verbose, environment) + .WithZOption() + .WithNameStatus() + .WithNoRenames() + .Run(GetParent(one), one, other)); + + var result = ImmutableArray.CreateBuilder(); + // Split on NUL. Consume the resulting tokens in (status, file) pairs. + var tokens = cmdResult.Split('\0'); + int i = 0; + while (i < tokens.Length) + { + string strOp = tokens[i++]; + if (string.IsNullOrEmpty(strOp)) + { + continue; + } + if (!DiffFile.OpByChar.TryGetValue(strOp, out var op)) + { + throw new InvalidOperationException( + $"Unknown type '{strOp}'. Text:\n{cmdResult}"); + } + Preconditions.CheckState(i < tokens.Length, "Missing file name after status '{0}'", strOp); + string file = tokens[i++]; + Preconditions.CheckState(file.Contains('/'), "Expected a path with a separator: {0}", file); + result.Add(new DiffFile(file.Substring(file.IndexOf('/') + 1), op)); + } + return result.ToImmutable(); + } + + /// + /// Apply the patches in reverse to the directory using git apply. At least one of either + /// or a nonempty should be supplied. + /// + /// an optional diff that will be streamed to the command through stdin. + /// a list of paths to patch files that will be supplied to the command. + public static void ReverseApplyPatches( + byte[]? patchBytes, + IReadOnlyList patchFiles, + string applyDirectory, + IReadOnlyDictionary environment) => + FoldersDiff.ReverseApplyPatches(patchBytes, patchFiles, applyDirectory, environment); + + /// Given a git compatible diff, returns the diff colorized if the console allows it. + public static string Colorize(Console.Console console, string diffText) + { + var sb = new StringBuilder(); + foreach (var line in diffText.Split('\n')) + { + sb.Append('\n'); + if (line.StartsWith("diff ", StringComparison.Ordinal)) + { + sb.Append(console.Colorize(AnsiColor.Cyan, line)); + } + else if (line.StartsWith("rename ", StringComparison.Ordinal)) + { + sb.Append(console.Colorize(AnsiColor.Yellow, line)); + } + else if (line.StartsWith("+", StringComparison.Ordinal)) + { + sb.Append(console.Colorize(AnsiColor.Green, line)); + } + else if (line.StartsWith("-", StringComparison.Ordinal)) + { + sb.Append(console.Colorize(AnsiColor.Red, line)); + } + else + { + sb.Append(line); + } + } + return sb.ToString(); + } + + private static string GetParent(string path) + { + string? parent = System.IO.Path.GetDirectoryName(path); + return parent ?? throw new ArgumentException($"Path '{path}' has no parent directory."); + } + + /// Resolves the git binary honoring GIT_EXEC_PATH, mirroring GitEnvironment. + private static string ResolveGitBinary(IReadOnlyDictionary environment) + { + if (environment.TryGetValue("GIT_EXEC_PATH", out var execPath)) + { + return System.IO.Path.Combine(execPath, "git"); + } + return "git"; + } + + /// Executes git diff between two folders. + private sealed class FoldersDiff + { + private static readonly Regex OutputErrorPattern = + new("^error:", RegexOptions.Multiline); + + private readonly bool _nameStatus; + private readonly bool _noRenames; + private readonly bool _zOption; + private readonly bool _ignoreCrAtEol; + private readonly bool _singleFile; + private readonly bool _verbose; + private readonly IReadOnlyDictionary _environment; + + internal FoldersDiff(bool verbose, IReadOnlyDictionary? environment) + : this(verbose, environment ?? new Dictionary(), + false, false, false, false, false) + { + } + + private FoldersDiff( + bool verbose, + IReadOnlyDictionary environment, + bool nameStatus, + bool noRenames, + bool zOption, + bool ignoreCrAtEol, + bool singleFile) + { + _verbose = verbose; + _environment = environment; + _nameStatus = nameStatus; + _noRenames = noRenames; + _zOption = zOption; + _ignoreCrAtEol = ignoreCrAtEol; + _singleFile = singleFile; + } + + internal FoldersDiff WithNameStatus() => + new(_verbose, _environment, true, _noRenames, _zOption, _ignoreCrAtEol, _singleFile); + + internal FoldersDiff WithNoRenames() => + new(_verbose, _environment, _nameStatus, true, _zOption, _ignoreCrAtEol, _singleFile); + + internal FoldersDiff WithZOption() => + new(_verbose, _environment, _nameStatus, _noRenames, true, _ignoreCrAtEol, _singleFile); + + internal FoldersDiff WithIgnoreCrAtEol() => + new(_verbose, _environment, _nameStatus, _noRenames, _zOption, true, _singleFile); + + internal FoldersDiff WithSingleFile() => + new(_verbose, _environment, _nameStatus, _noRenames, _zOption, _ignoreCrAtEol, true); + + internal byte[] Run(string root, string one, string other) + { + Preconditions.CheckArgument( + _singleFile || string.Equals(GetParent(one), GetParent(other)), + "Paths 'one' and 'other' must be sibling directories."); + + var @params = new List + { + ResolveGitBinary(_environment), + // We want to use `git apply`/`git diff` as a glorified diff command without any + // git repo involvement. Make sure git doesn't accidentally pick up some + // git repo from higher up the directory tree. + "--git-dir=/dev/null", + // override diff.noprefix for consistent diff output, must come after "git" + "-c", + "diff.noprefix=false", + "diff", + "--no-color", + "--no-index", + // Be careful, no test coverage for these: + "--no-ext-diff", + }; + if (_nameStatus) + { + @params.Add("--name-status"); + } + if (_noRenames) + { + @params.Add("--no-renames"); + } + if (_zOption) + { + @params.Add("-z"); + } + if (_ignoreCrAtEol) + { + @params.Add("--ignore-cr-at-eol"); + } + + @params.Add("--"); + @params.Add(Relativize(root, one)); + @params.Add(Relativize(root, other)); + + var cmd = new Command(@params.ToArray(), _environment, root); + try + { + new CommandRunner(cmd).WithVerbose(_verbose).Execute(); + return EmptyDiff; + } + catch (BadExitStatusWithOutputException e) + { + CommandOutput output = e.GetOutput(); + // git diff returns exit status 0 when contents are identical, or 1 when different. + string outputError = output.GetStderr(); + if (!string.IsNullOrEmpty(outputError) && OutputErrorPattern.IsMatch(outputError)) + { + throw new IOException( + $"Error executing 'git diff': {e.Message}. Stderr: \n{output.GetStderr()}", e); + } + return output.GetStdoutBytes(); + } + catch (CommandException e) + { + throw new IOException("Error executing 'git diff'", e); + } + } + + internal static void ReverseApplyPatches( + byte[]? patchBytes, + IReadOnlyList patchFiles, + string applyDirectory, + IReadOnlyDictionary environment) + { + var @params = new List + { + ResolveGitBinary(environment), + // Same rationale as in Run: keep git from picking up a surrounding repo. + "--git-dir=/dev/null", + "apply", + "--reverse", + "-p2", + "--allow-empty", + }; + @params.AddRange(patchFiles); + if (patchBytes != null) + { + @params.Add("-"); + } + + var cmd = new Command(@params.ToArray(), environment, applyDirectory); + try + { + var runner = new CommandRunner(cmd).WithVerbose(true); + if (patchBytes != null) + { + runner = runner.WithInput(patchBytes); + } + runner.Execute(); + } + catch (CommandException e) + { + throw new IOException("Error executing 'git apply'", e); + } + } + + private static string Relativize(string root, string path) => + System.IO.Path.GetRelativePath(root, path); + } +} diff --git a/src/Copybara.Core/Version/CorrectorVersionSelector.cs b/src/Copybara.Core/Version/CorrectorVersionSelector.cs new file mode 100644 index 000000000..cbb89f93b --- /dev/null +++ b/src/Copybara.Core/Version/CorrectorVersionSelector.cs @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Version; + +/// +/// A version selector that, given a requested version, tries to find a matching tag by using several +/// fuzzing heuristics. +/// +public class CorrectorVersionSelector : IVersionSelector +{ + private readonly Console _console; + + public CorrectorVersionSelector(Console console) + { + _console = console; + } + + public string? Select(IVersionList versionList, string? requestedRef, Console console) + { + if (requestedRef == null) + { + return null; + } + string cleanedVersion = StripVersion(requestedRef); + foreach (string tag in versionList.List()) + { + if (StripVersion(tag) == cleanedVersion) + { + _console.InfoFmt( + "Assuming version {0} references {1} ({2})", requestedRef, tag, cleanedVersion); + return tag; + } + } + return null; + } + + private static bool IsDigit(char c) => c >= '0' && c <= '9'; + + private static bool IsVersionPart(char c) => IsDigit(c) || c == '.'; + + private static string StripVersion(string version) + { + // Trim leading non-numeric characters. + int start = 0; + while (start < version.Length && !IsDigit(version[start])) + { + start++; + } + string strippedPrefix = version.Substring(start); + + // Normalize separators ",;-_" to '.'. + var normalizedBuilder = new StringBuilder(strippedPrefix.Length); + foreach (char c in strippedPrefix) + { + normalizedBuilder.Append(c is ',' or ';' or '-' or '_' ? '.' : c); + } + string normalizedSeparator = normalizedBuilder.ToString(); + + var strippedVersion = new StringBuilder(); + int index = 0; + while (index < normalizedSeparator.Length) + { + if (RegionMatchesIgnoreCase(normalizedSeparator, index, "RC")) + { + strippedVersion.Append("RC"); + index += 2; + continue; + } + if (RegionMatchesIgnoreCase(normalizedSeparator, index, "PL")) + { + strippedVersion.Append("PL"); + index += 2; + continue; + } + if (IsVersionPart(normalizedSeparator[index])) + { + strippedVersion.Append(normalizedSeparator[index]); + index++; + continue; + } + // fast-forward through strings that might contain but not start with RC/PL + while (index < normalizedSeparator.Length + && !IsVersionPart(normalizedSeparator[index])) + { + index++; + } + } + return strippedVersion.ToString(); + } + + private static bool RegionMatchesIgnoreCase(string s, int index, string other) + { + if (index + other.Length > s.Length) + { + return false; + } + return string.Compare( + s, index, other, 0, other.Length, StringComparison.OrdinalIgnoreCase) == 0; + } + + public override string ToString() => nameof(CorrectorVersionSelector); +} diff --git a/src/Copybara.Core/Version/CustomVersionSelector.cs b/src/Copybara.Core/Version/CustomVersionSelector.cs new file mode 100644 index 000000000..993e924c5 --- /dev/null +++ b/src/Copybara.Core/Version/CustomVersionSelector.cs @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using Copybara.Common; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Version; + +/// A version selector that uses a custom comparator to select the version. +public sealed class CustomVersionSelector : IVersionSelector +{ + private readonly IStarlarkCallable _comparator; + private readonly string? _filterByRegex; + + /// Creates a new CustomVersionSelector. + /// the custom comparator to use. + /// the regex to filter the potential version candidates by. + /// + /// if the comparator is not a StarlarkFunction or if the comparator does not take two string + /// arguments named 'left' and 'right'. + /// + public CustomVersionSelector(IStarlarkCallable comparator, string? filterByRegex) + { + _comparator = EnforceStarlarkCallable(comparator); + _filterByRegex = filterByRegex; + } + + /// + /// Selects the latest version from the version list that matches the filter regex and the custom + /// comparator. + /// + /// the list of versions to select from. + /// the reference of the requested version. + /// the console to use for logging. + /// + /// the latest version that matches the filter regex and the custom comparator, or null. + /// + /// + /// if the comparator returns a comparison result outside of [-1, 1] or if the comparator throws + /// an exception during execution. + /// + public string? Select(IVersionList versionList, string? requestedRef, Console console) + { + ImmutableArray filteredVersions = FilterByRegexMatch(versionList.List()); + + if (versionList.List().Count == 0) + { + return null; + } + + var comparer = Comparer.Create((left, right) => + { + int result = CallCustomComparator(left, right, console); + if (result < -1 || result > 1) + { + throw new InvalidOperationException( + string.Format( + "Attempted to call comparator '{0}' left={1}, right={2} and got a comparison" + + " result of {3}", + _comparator.Name, left, right, result)); + } + return result; + }); + + // OrderBy performs a stable sort, mirroring Guava's ImmutableList.sortedCopyOf. + var sortedVersions = filteredVersions.OrderBy(v => v, comparer).ToList(); + + return sortedVersions.Count == 0 ? null : sortedVersions[^1]; + } + + private ImmutableArray FilterByRegexMatch(IReadOnlySet versionList) + { + if (string.IsNullOrEmpty(_filterByRegex)) + { + return versionList.ToImmutableArray(); + } + var pattern = new Regex(_filterByRegex); + return versionList + .Where(s => IsFullMatch(pattern, s)) + .ToImmutableArray(); + } + + private static bool IsFullMatch(Regex pattern, string s) + { + // Mirror java.util.regex Matcher.matches(): the entire input must match. + Match m = pattern.Match(s); + return m.Success && m.Index == 0 && m.Length == s.Length; + } + + private int CallCustomComparator(string left, string right, Console console) + { + try + { + using Mutability mutability = Mutability.Create("custom_version_selector_comparator"); + object? result = StarlarkRt.Call( + StarlarkThread.CreateTransient(mutability, StarlarkSemantics.DEFAULT), + _comparator, + /* args= */ ImmutableArray.Empty, + new Dictionary { ["left"] = left, ["right"] = right }); + return ((StarlarkInt)result!).ToInt("user comparator"); + } + catch (EvalException e) + { + console.ErrorFmt( + "Failed to excecute custom comparator. The exception was {0}", e.Message); + // this is going to propagate an upstream exception since it is not in [-1, 1] + return -2; + } + } + + private static IStarlarkCallable EnforceStarlarkCallable(IStarlarkCallable comparator) + { + Preconditions.CheckArgument( + comparator is StarlarkFunction, + $"Comparator must be a StarlarkFunction but was {comparator.GetType().FullName}"); + IReadOnlyList parameterNames = ((StarlarkFunction)comparator).GetParameterNames(); + Preconditions.CheckArgument( + parameterNames.Count == 2 + && parameterNames.OrderBy(n => n, StringComparer.Ordinal) + .SequenceEqual(new[] { "left", "right" }), + "The comparator must take two strings arguments named 'left' and 'right'"); + + return comparator; + } +} diff --git a/src/Copybara.Core/Version/LatestVersionSelector.cs b/src/Copybara.Core/Version/LatestVersionSelector.cs new file mode 100644 index 000000000..2e2261922 --- /dev/null +++ b/src/Copybara.Core/Version/LatestVersionSelector.cs @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.TemplateToken; +using Starlark.Eval; +using Starlark.Syntax; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Version; + +/// +/// Given a and a regex template, finds the latest version that matches the +/// regex. +/// +public class LatestVersionSelector : IVersionSelector +{ + private readonly string _format; + private readonly SortedDictionary _groupTypes; + private readonly RegexTemplateTokens _template; + + /// + public LatestVersionSelector( + string format, + IReadOnlyDictionary groups, + SortedDictionary groupTypes, + Location location) + { + _format = format; + _groupTypes = Preconditions.CheckNotNull(groupTypes); + _template = new RegexTemplateTokens( + Preconditions.CheckNotNull(format), groups, true, location); + } + + /// Enum type for different version segments. + public enum VersionElementType + { + NUMERIC, + ALPHABETIC, + } + + public IReadOnlySet SearchPatterns() => + ImmutableHashSet.Create(new SearchPattern(_template.GetTokens())); + + public string? Select(IVersionList versionList, string? requestedRef, Console console) + { + IReadOnlySet refs = versionList.List(); + + ImmutableListMultimap groupIndexes = _template.GetGroupIndexes(); + List latest = new(); + string? latestRef = null; + foreach (string @ref in refs) + { + Match matcher = _template.GetBefore().Match(@ref); + if (!IsFullMatch(matcher, @ref)) + { + console.VerboseFmt( + "Ref '{0}' didn't match version_selector didn't match any version for '{1}'. This" + + " ref will be ignored, consider correcting the version_selector regular" + + " expression if this is not intended.", + @ref, _template.GetBefore().ToString()); + continue; + } + List objs = new(); + foreach (var groups in _groupTypes) + { + string var = VarName(groups.Value, groups.Key); + ImmutableArray indexes = groupIndexes.Get(var); + string val = matcher.Groups[indexes[^1]].Value; + objs.Add(Convert(groups.Value, val, groups.Key)); + } + if (IsAfter(latest, objs)) + { + latest = objs; + latestRef = @ref; + } + } + if (latestRef == null) + { + console.WarnFmt( + "version_selector didn't match any version for '{0}'", + _template.GetBefore().ToString()); + } + + return latestRef; + } + + private static bool IsFullMatch(Match match, string input) => + match.Success && match.Index == 0 && match.Length == input.Length; + + private static bool IsAfter(List old, List newer) + { + if (old.Count == 0) + { + return true; + } + Preconditions.CheckArgument(old.Count == newer.Count); + for (int i = 0; i < old.Count; i++) + { + int comp = CompareElement(old[i], newer[i]); + if (comp != 0) + { + return comp < 0; + } + } + return false; // Everything equal + } + + private static int CompareElement(IComparable o, IComparable n) => o.CompareTo(n); + + public IReadOnlyList GetUnmatchedGroups() + { + var usedGroups = _template.GetGroupIndexes().Keys.ToHashSet(); + return _groupTypes + .Select(e => VarName(e.Value, e.Key)) + .Where(s => !usedGroups.Contains(s)) + .ToImmutableArray(); + } + + private static string VarName(VersionElementType type, int idx) => + (type == VersionElementType.NUMERIC ? "n" : "s") + idx; + + /// + private static IComparable Convert(VersionElementType type, string val, int idx) + { + if (type == VersionElementType.ALPHABETIC) + { + return val; + } + + // NUMERIC. Handles case for ".[0-9]+" + if (val.StartsWith('.')) + { + val = val.Substring(1); + } + if (val.Length == 0) + { + return int.MinValue; + } + if (int.TryParse(val, out int parsed)) + { + return parsed; + } + throw new ValidationException( + string.Format( + "Invalid value for numeric group {0}: '{1}'. Use s{2} instead of n{2} as group name" + + " or extract the prefix part to the format string.", + VarName(type, idx), val, idx)); + } + + public override string ToString() => + string.Format("core.latest_version(format = '{0}')", _format); +} diff --git a/src/Copybara.Core/Version/OrderedVersionSelector.cs b/src/Copybara.Core/Version/OrderedVersionSelector.cs new file mode 100644 index 000000000..537265bbd --- /dev/null +++ b/src/Copybara.Core/Version/OrderedVersionSelector.cs @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Linq; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Version; + +/// +/// A selector of selectors that traverses all the selectors in order and returns the first result +/// that is found. +/// +public class OrderedVersionSelector : IVersionSelector +{ + private readonly ImmutableArray _selectors; + + public OrderedVersionSelector(ImmutableArray selectors) + { + _selectors = selectors; + } + + public string? Select(IVersionList versionList, string? requestedRef, Console console) + { + foreach (IVersionSelector selector in _selectors) + { + string? selection = selector.Select(versionList, requestedRef, console); + if (selection != null) + { + return selection; + } + } + return null; + } + + /// + /// Returns the union of all inner searchPatterns. + /// + /// Any searchPattern that is "none" is ignored (So composition of selectors that use the + /// requestedRef can be mixed with version selectors that use ). + /// + /// If any searchPattern is "all", it returns "all" (Version selector is interested in all + /// the versions). + /// + public IReadOnlySet SearchPatterns() + { + var result = _selectors + .SelectMany(s => s.SearchPatterns()) + .Where(p => !p.IsNone()) + .ToImmutableHashSet(); + + if (result.Any(p => p.IsAll())) + { + return SearchPattern.ALL; + } + return result; + } + + public override string ToString() => $"[{string.Join(", ", _selectors)}]"; +} diff --git a/src/Copybara.Core/Version/RequestedExactMatchSelector.cs b/src/Copybara.Core/Version/RequestedExactMatchSelector.cs new file mode 100644 index 000000000..40fabcfb2 --- /dev/null +++ b/src/Copybara.Core/Version/RequestedExactMatchSelector.cs @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Version; + +/// +/// Given a requested reference, it returns that reference if it is an exact match with one of the +/// versions from . +/// +public class RequestedExactMatchSelector : IVersionSelector +{ + public string? Select(IVersionList versionList, string? requestedRef, Console console) + { + if (requestedRef != null && versionList.List().Contains(requestedRef)) + { + return requestedRef; + } + return null; + } + + public override string ToString() => nameof(RequestedExactMatchSelector); +} diff --git a/src/Copybara.Core/Version/RequestedVersionSelector.cs b/src/Copybara.Core/Version/RequestedVersionSelector.cs new file mode 100644 index 000000000..50ea221fb --- /dev/null +++ b/src/Copybara.Core/Version/RequestedVersionSelector.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Version; + +/// Given a requested version, it returns it as the selected version. None otherwise. +public class RequestedVersionSelector : IVersionSelector +{ + public string? Select(IVersionList versionList, string? requestedRef, Console console) => + requestedRef; + + public override string ToString() => nameof(RequestedVersionSelector); +} diff --git a/src/Copybara.Core/Version/VersionList.cs b/src/Copybara.Core/Version/VersionList.cs new file mode 100644 index 000000000..2d6393950 --- /dev/null +++ b/src/Copybara.Core/Version/VersionList.cs @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Starlark.Eval; + +namespace Copybara.Version; + +/// List all or a subset of the versions of a repository. +public interface IVersionList : IStarlarkPrintableValue, IConfigItemDescription +{ + /// List the versions. + /// + /// + IReadOnlySet List(); + + void IStarlarkPrintableValue.Repr(Printer printer, StarlarkSemantics semantics) => + printer.Append(ToString()); +} + +/// A version list that comes from a set of Strings. +public sealed class SetVersionList : IVersionList +{ + private readonly ImmutableHashSet _versions; + + public SetVersionList(IReadOnlySet versions) + { + _versions = versions.ToImmutableHashSet(); + } + + public IReadOnlySet List() => _versions; +} diff --git a/src/Copybara.Core/Version/VersionResolver.cs b/src/Copybara.Core/Version/VersionResolver.cs new file mode 100644 index 000000000..14f51fe10 --- /dev/null +++ b/src/Copybara.Core/Version/VersionResolver.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Revision; +using Starlark.Eval; + +namespace Copybara.Version; + +/// Takes a ref and resolves it to the repository. +public interface IVersionResolver : IStarlarkValue, IConfigItemDescription +{ + /// Resolves the given to a revision. + /// + IRevision Resolve(string @ref, Func assemblyStrategy); +} diff --git a/src/Copybara.Core/Version/VersionSelector.cs b/src/Copybara.Core/Version/VersionSelector.cs new file mode 100644 index 000000000..c2939eb53 --- /dev/null +++ b/src/Copybara.Core/Version/VersionSelector.cs @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Linq; +using Copybara.TemplateToken; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Version; + +/// +/// Select a version from a list of versions, using custom logic (For example, semantic versioning, +/// etc.). +/// +[StarlarkBuiltin("VersionSelector", Doc = "Select a version from a list of versions")] +public interface IVersionSelector : IStarlarkPrintableValue +{ + /// + /// Selects a version from the given . Returns null if no + /// version was selected. + /// + /// + /// + string? Select(IVersionList versionList, string? requestedRef, Console console); + + /// + /// Give a hint on what the version selector is interested in so that an + /// can be more efficient in listing valid versions. + /// + /// A SearchPattern is composed of tokens that are either a literal or an interpolation. + /// The interpolation name is not important and might be ignored by the . + /// But if present, it can be used to report debugging information about a particular part of a + /// version found. + /// + /// Two edge cases: + /// + /// Empty list: Means that the selector doesn't use patterns (For example, because it uses + /// the CLI reference). + /// Single interpolation token: Means that it is interested in all the references. + /// Equivalent to '*'. + /// + /// + IReadOnlySet SearchPatterns() => SearchPattern.NONE; + + void IStarlarkPrintableValue.Repr(Printer printer, StarlarkSemantics semantics) => + printer.Append(ToString()); +} + +/// +/// A search pattern is a wrapper class of list that expresses the pattern of the +/// versions the is interested in. This allows having literals mixed +/// with interpolations (e.g. foo.*bar -> [foo, .*, bar]). +/// +public sealed class SearchPattern +{ + private readonly ImmutableArray _tokens; + + public static readonly IReadOnlySet ALL = + ImmutableHashSet.Create( + new SearchPattern(ImmutableArray.Create(Token.Interpolation("all")))); + + public static readonly IReadOnlySet NONE = ImmutableHashSet.Empty; + + public SearchPattern(IReadOnlyList tokens) + { + _tokens = tokens.ToImmutableArray(); + } + + public IReadOnlyList Tokens() => _tokens; + + /// Returns true if the search pattern is interested in all references. + public bool IsAll() => _tokens.All(t => t.GetTokenType() == TokenType.Interpolation); + + /// + /// Returns true if the search pattern doesn't use the references from the + /// as primary data for selecting the version. + /// + public bool IsNone() => _tokens.IsEmpty; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + return o is SearchPattern that && _tokens.SequenceEqual(that._tokens); + } + + public override int GetHashCode() + { + var hash = new HashCode(); + foreach (var token in _tokens) + { + hash.Add(token); + } + return hash.ToHashCode(); + } + + public override string ToString() => + $"SearchPattern{{tokens=[{string.Join(", ", _tokens)}]}}"; +} diff --git a/src/Copybara.Core/Workflow.cs b/src/Copybara.Core/Workflow.cs new file mode 100644 index 000000000..152e037af --- /dev/null +++ b/src/Copybara.Core/Workflow.cs @@ -0,0 +1,861 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using System.Text; +using Copybara.Action; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Config; +using Copybara.Effect; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.TemplateToken; +using Copybara.Util; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; +using ChangeMigrationFinishedEvent = Copybara.Monitor.IEventMonitor.ChangeMigrationFinishedEvent; +using EventMonitors = Copybara.Monitor.IEventMonitor.EventMonitors; + +namespace Copybara; + +/// +/// Non-generic entry points for . +/// +/// Starlark's core.workflow receives its origin and destination as untyped objects +/// (Java uses raw/wildcard types Origin<?> / Destination<?>). Because +/// .NET generics are reified — unlike Java's erasure — we cannot simply cast an +/// IOrigin<FolderRevision> to IOrigin<Revision>. Instead we reflect the +/// concrete revision type parameters out of the runtime origin/destination and instantiate the +/// correctly-typed . This preserves the Java behavior while remaining +/// type-safe at runtime. +/// +public static class Workflow +{ + internal const string COPYBARA_CONFIG_PATH_IDENTITY_VAR = "copybara_config_path"; + internal const string COPYBARA_WORKFLOW_NAME_IDENTITY_VAR = "copybara_workflow_name"; + internal const string COPYBARA_REFERENCE_IDENTITY_VAR = "copybara_reference"; + internal const string COPYBARA_REFERENCE_LABEL_VAR = "label:"; + + /// + /// Builds a whose type parameters are inferred from the runtime + /// types of and . All remaining + /// arguments are forwarded, in order, to the constructor. + /// + public static IMigration Create( + string name, + string? description, + object origin, + object destination, + Authoring.Authoring authoring, + ITransformation transformation, + string? lastRevisionFlag, + bool initHistoryFlag, + GeneralOptions generalOptions, + Glob originFiles, + Glob destinationFiles, + WorkflowMode mode, + WorkflowOptions workflowOptions, + ITransformation? reverseTransformForCheck, + Glob reversibleCheckIgnoreFiles, + bool askForConfirmation, + ConfigFile mainConfigFile, + Func> allConfigFiles, + bool dryRunModeField, + bool checkLastRevState, + ImmutableArray afterMigrationActions, + ImmutableArray afterAllMigrationActions, + ImmutableArray changeIdentity, + bool setRevId, + bool smartPrune, + MergeImportConfiguration? mergeImport, + AutoPatchfileConfiguration? autoPatchfileConfiguration, + ITransformation afterMergeTransformations, + bool migrateNoopChanges, + string? customRevId, + bool checkout, + ConsistencyFileConfiguration? consistencyFileConfig, + string? expectedFixedRef, + string? pinnedFixedRef, + ImmutableArray definitionStack, + ImmutableArray> definitionStackLocals) + { + Type originRevision = FindRevisionArg(origin.GetType(), typeof(IOrigin<>)) + ?? throw new ArgumentException( + $"origin of type '{origin.GetType()}' does not implement IOrigin<>"); + Type destinationRevision = FindRevisionArg(destination.GetType(), typeof(IDestination<>)) + ?? throw new ArgumentException( + $"destination of type '{destination.GetType()}' does not implement IDestination<>"); + + Type workflowType = typeof(Workflow<,>).MakeGenericType(originRevision, destinationRevision); + object? instance = Activator.CreateInstance( + workflowType, + name, + description, + origin, + destination, + authoring, + transformation, + lastRevisionFlag, + initHistoryFlag, + generalOptions, + originFiles, + destinationFiles, + mode, + workflowOptions, + reverseTransformForCheck, + reversibleCheckIgnoreFiles, + askForConfirmation, + mainConfigFile, + allConfigFiles, + dryRunModeField, + checkLastRevState, + afterMigrationActions, + afterAllMigrationActions, + changeIdentity, + setRevId, + smartPrune, + mergeImport, + autoPatchfileConfiguration, + afterMergeTransformations, + migrateNoopChanges, + customRevId, + checkout, + consistencyFileConfig, + expectedFixedRef, + pinnedFixedRef, + definitionStack, + definitionStackLocals); + return (IMigration)instance!; + } + + /// + /// Returns the revision type argument of the (e.g. + /// IOrigin<>) implemented by , or null if not found. + /// + private static Type? FindRevisionArg(Type concrete, Type openInterface) + { + foreach (Type i in concrete.GetInterfaces()) + { + if (i.IsGenericType && i.GetGenericTypeDefinition() == openInterface) + { + return i.GetGenericArguments()[0]; + } + } + return null; + } +} + +/// +/// Represents a particular migration operation that can occur for a project. Each project can have +/// multiple workflows. Each workflow has a particular origin and destination. +/// +/// Origin revision type. +/// Destination revision type. +public class Workflow : IMigration + where O : class, IRevision + where D : class, IRevision +{ + private readonly string _name; + private readonly string? _description; + private readonly IOrigin _origin; + private readonly IDestination _destination; + private readonly Authoring.Authoring _authoring; + private readonly ITransformation _transformation; + + private readonly string? _lastRevisionFlag; + private readonly bool _initHistoryFlag; + private readonly Console _console; + private readonly GeneralOptions _generalOptions; + private readonly Glob _originFiles; + private readonly Glob _destinationFiles; + private readonly WorkflowMode _mode; + private readonly WorkflowOptions _workflowOptions; + + private readonly ITransformation? _reverseTransformForCheck; + private readonly bool _verbose; + private readonly Glob _reversibleCheckIgnoreFiles; + private readonly bool _askForConfirmation; + private readonly bool _force; + private readonly ConfigFile _mainConfigFile; + private readonly Func> _allConfigFiles; + private readonly bool _effectiveDryRunMode; + private readonly bool _dryRunModeField; + private readonly ImmutableArray _afterMigrationActions; + private readonly ImmutableArray _changeIdentity; + private readonly bool _setRevId; + private readonly bool _smartPrune; + private readonly MergeImportConfiguration? _mergeImport; + private readonly AutoPatchfileConfiguration? _autoPatchfileConfiguration; + + // Package-visible in Java ('final Transformation afterMergeTransformations'). + internal readonly ITransformation AfterMergeTransformations; + private readonly bool _migrateNoopChanges; + private readonly bool _checkLastRevState; + private readonly ImmutableArray _afterAllMigrationActions; + private readonly string? _customRevId; + private readonly bool _checkout; + + private readonly ConsistencyFileConfiguration? _consistencyFileConfig; + private readonly string? _expectedFixedRef; + private readonly string? _pinnedFixedRef; + private readonly ImmutableArray _definitionStack; + private readonly ImmutableArray> _definitionStackLocals; + + public Workflow( + string name, + string? description, + IOrigin origin, + IDestination destination, + Authoring.Authoring authoring, + ITransformation transformation, + string? lastRevisionFlag, + bool initHistoryFlag, + GeneralOptions generalOptions, + Glob originFiles, + Glob destinationFiles, + WorkflowMode mode, + WorkflowOptions workflowOptions, + ITransformation? reverseTransformForCheck, + Glob reversibleCheckIgnoreFiles, + bool askForConfirmation, + ConfigFile mainConfigFile, + Func> allConfigFiles, + bool dryRunModeField, + bool checkLastRevState, + ImmutableArray afterMigrationActions, + ImmutableArray afterAllMigrationActions, + ImmutableArray changeIdentity, + bool setRevId, + bool smartPrune, + MergeImportConfiguration? mergeImport, + AutoPatchfileConfiguration? autoPatchfileConfiguration, + ITransformation afterMergeTransformations, + bool migrateNoopChanges, + string? customRevId, + bool checkout, + ConsistencyFileConfiguration? consistencyFileConfig, + string? expectedFixedRef, + string? pinnedFixedRef, + ImmutableArray definitionStack, + ImmutableArray> definitionStackLocals) + { + _name = Preconditions.CheckNotNull(name); + _description = description; + _origin = Preconditions.CheckNotNull(origin); + _destination = Preconditions.CheckNotNull(destination); + _authoring = Preconditions.CheckNotNull(authoring); + _transformation = Preconditions.CheckNotNull(transformation); + _lastRevisionFlag = lastRevisionFlag; + _initHistoryFlag = initHistoryFlag; + _console = Preconditions.CheckNotNull(generalOptions.GetConsole()); + _generalOptions = generalOptions; + _originFiles = Preconditions.CheckNotNull(originFiles); + _destinationFiles = Preconditions.CheckNotNull(destinationFiles); + _mode = mode; + _workflowOptions = Preconditions.CheckNotNull(workflowOptions); + _reverseTransformForCheck = reverseTransformForCheck; + _verbose = generalOptions.IsVerbose(); + _reversibleCheckIgnoreFiles = reversibleCheckIgnoreFiles; + _askForConfirmation = askForConfirmation; + _force = generalOptions.IsForced(); + _mainConfigFile = Preconditions.CheckNotNull(mainConfigFile); + _allConfigFiles = allConfigFiles; + _checkLastRevState = checkLastRevState; + _customRevId = customRevId; + _checkout = checkout; + _effectiveDryRunMode = dryRunModeField || generalOptions.DryRunMode; + _dryRunModeField = dryRunModeField; + _afterMigrationActions = afterMigrationActions; + _afterAllMigrationActions = afterAllMigrationActions; + _changeIdentity = changeIdentity; + _setRevId = setRevId; + _smartPrune = smartPrune; + _mergeImport = mergeImport; + _autoPatchfileConfiguration = autoPatchfileConfiguration; + AfterMergeTransformations = afterMergeTransformations; + _migrateNoopChanges = migrateNoopChanges; + _consistencyFileConfig = consistencyFileConfig; + _expectedFixedRef = expectedFixedRef; + _pinnedFixedRef = pinnedFixedRef; + _definitionStack = definitionStack; + _definitionStackLocals = definitionStackLocals; + } + + public string GetName() => _name; + + public string? GetDescription() => _description; + + /// The repository that represents the source of truth. + public IOrigin GetOrigin() => _origin; + + /// The destination repository to copy to. + public IDestination GetDestination() => _destination; + + /// The author mapping between an origin and a destination. + public Authoring.Authoring GetAuthoring() => _authoring; + + /// Transformation to run before writing them to the destination. + public ITransformation GetTransformation() => _transformation; + + public bool IsAskForConfirmation() => _askForConfirmation; + + /// + /// Includes only the fields that are part of the configuration: Console is not part of the + /// config, configName is in the parent, and lastRevisionFlag is a command-line flag. + /// + public override string ToString() => + $"Workflow{{name={_name}, origin={_origin}, destination={_destination}, authoring={_authoring}," + + $" transformation={_transformation}, originFiles={_originFiles}," + + $" destinationFiles={_destinationFiles}, mode={_mode}," + + $" reverseTransformForCheck={_reverseTransformForCheck}, askForConfirmation={_askForConfirmation}," + + $" checkLastRevState={_checkLastRevState}, afterMigrationActions=[{string.Join(", ", _afterMigrationActions)}]," + + $" changeIdentity=[{string.Join(", ", _changeIdentity)}], setRevId={_setRevId}}}"; + + public void Run(string workdir, IReadOnlyList sourceRefs) + { + if (sourceRefs.Count > 1) + { + throw new CommandLineException( + string.Format( + CultureInfo.InvariantCulture, + "Workflow does not support multiple source_ref arguments yet: {0}", + string.Join(", ", sourceRefs))); + } + + string? sourceRef = sourceRefs.Count == 1 ? sourceRefs[0] : null; + + ValidateFlags(); + using (Profiler().Start("run/" + _name)) + { + _console.Progress( + "Getting last revision: Resolving " + + (sourceRef == null ? "origin reference" : sourceRef)); + O resolvedRef = _generalOptions.RepoTask( + "origin.resolve_source_ref", () => _origin.Resolve(sourceRef!)); + + if (!string.IsNullOrEmpty(_expectedFixedRef) && !string.IsNullOrEmpty(_pinnedFixedRef)) + { + throw new CommandLineException( + "Using --expected-fixed-ref and --pinned-fixed-ref together is not supported."); + } + + if (!string.IsNullOrEmpty(_expectedFixedRef)) + { + ValidateExpectedFixedRef(resolvedRef); + } + + if (!string.IsNullOrEmpty(_pinnedFixedRef)) + { + resolvedRef = HandlePinnedFixedRef(resolvedRef, _pinnedFixedRef!); + } + + var allEffects = ImmutableArray.CreateBuilder(); + WorkflowRunHelper helper = NewRunHelper( + workdir, + resolvedRef, + sourceRef, + @event => + { + allEffects.AddRange(@event.DestinationEffects); + EventMonitors().DispatchEvent(m => m.OnChangeMigrationFinished(@event)); + }); + try + { + using (Profiler().Start(_mode.ToString().ToLowerInvariant())) + { + // Run-mode dispatch: mirrors Java's per-enum WorkflowMode.run(helper). + WorkflowModeRunner.Run(_mode, helper); + } + } + finally + { + if (!GetGeneralOptions().DryRunMode) + { + using (Profiler().Start("after_all_migration")) + { + ImmutableArray effects = allEffects.ToImmutable(); + IReadOnlyList resultEffects = RunHooks( + effects, + GetAfterAllMigrationActions(), + // Only do this once for all the actions + LazyResourceLoader.Memoized( + c => helper.GetOriginReader().GetFeedbackEndPoint(c!)), + // Only do this once for all the actions + LazyResourceLoader.Memoized( + c => helper.GetDestinationWriter().GetFeedbackEndPoint(c!)), + resolvedRef); + if (effects.Length != resultEffects.Count) + { + _console.Warn( + "Effects where created in after_all_migrations, but they are ignored."); + } + } + } + } + } + } + + private void ValidateExpectedFixedRef(O resolvedRef) + { + if (!string.IsNullOrEmpty(_expectedFixedRef) + && !string.IsNullOrEmpty(resolvedRef.FixedReference()) + && !resolvedRef.FixedReference()!.Equals(_expectedFixedRef, StringComparison.Ordinal)) + { + throw new ValidationException( + string.Format( + CultureInfo.InvariantCulture, + "Not migrating ref {0}, its fixed ref {1} did not match the expected fixed ref {2}.", + resolvedRef.ContextReference() ?? resolvedRef.AsString(), + resolvedRef.FixedReference(), + _expectedFixedRef)); + } + } + + private O HandlePinnedFixedRef(O resolvedMigrationRef, string pinnedFixedRef) + { + // Resolve the ref specified with --pinned-fixed-ref to ensure it is in the local repo cache, + // so `git merge-base` does not fail. + try + { + O resolvedPinnedRef = _generalOptions.RepoTask( + "origin.resolve_pinned_fixed_ref", () => _origin.Resolve(pinnedFixedRef)); + return _origin.ResolveAncestorRef( + resolvedPinnedRef.FixedReference()!, resolvedMigrationRef); + } + catch (ValidationException e) + { + throw new ValidationException( + string.Format( + CultureInfo.InvariantCulture, + "Could not enforce --pinned-fixed-ref. Cause: {0}", + e.Message), + e); + } + } + + /// + /// Validates if flags are compatible with this workflow. + /// + /// if flags are invalid for this workflow. + private void ValidateFlags() + { + _console.VerboseFmt( + "Using %s parallel threads for transformations", _workflowOptions.Threads); + ValidationException.CheckCondition( + !IsInitHistory() || _mode != WorkflowMode.ChangeRequest, + "%s is not compatible with %s", + WorkflowOptions.InitHistoryFlag, + WorkflowMode.ChangeRequest); + ValidationException.CheckCondition( + !IsCheckLastRevState() || _mode != WorkflowMode.ChangeRequest, + "%s is not compatible with %s", + WorkflowOptions.CheckLastRevStateFlag, + WorkflowMode.ChangeRequest); + ValidationException.CheckCondition( + !IsSmartPrune() || _mode == WorkflowMode.ChangeRequest, + "'smart_prune = True' is only supported for CHANGE_REQUEST mode."); + if (IsSetRevId()) + { + ValidationException.CheckCondition( + _mode != WorkflowMode.ChangeRequest || _customRevId == null, + "custom_rev_id is not allowed to be used in CHANGE_REQUEST mode if" + + " set_rev_id is set to true. custom_rev_id is used for looking" + + " for the baseline in the origin. No revId is stored in the destination."); + } + else + { + ValidationException.CheckCondition( + _mode == WorkflowMode.ChangeRequest || _mode == WorkflowMode.ChangeRequestFromSot, + "'set_rev_id = False' is only supported" + + " for CHANGE_REQUEST and CHANGE_REQUEST_FROM_SOT mode."); + } + } + + public virtual WorkflowRunHelper NewRunHelper( + string workdir, + O resolvedRef, + string? rawSourceRef, + Action migrationFinishedMonitor) + { + IOrigin.IReader reader = GetOrigin().NewReader(GetOriginFiles(), GetAuthoring()); + return new WorkflowRunHelper( + this, + workdir, + resolvedRef, + reader, + CreateWriter(resolvedRef), + rawSourceRef, + migrationFinishedMonitor); + } + + /// + /// Return the config files relative to their roots. For example a config file like 'admin/foo/bar' + /// with a root 'admin' would return 'foo/bar'. + /// + internal ISet ConfigPaths() + { + var result = new HashSet(); + foreach (var configFile in _allConfigFiles().Values) + { + result.Add(configFile.GetIdentifier()); + } + return result; + } + + public Info GetInfo() + { + return _generalOptions.RepoTask>( + "info", + () => + { + O? lastResolved = _generalOptions.RepoTask( + "origin.last_resolved", () => _origin.Resolve(reference: null!)); + + IOrigin.IReader oReader = _origin.NewReader(_originFiles, _authoring); + Change? lastResolvedChange = null; + try + { + lastResolvedChange = _generalOptions.RepoTask?>( + "origin.last_resolved", + () => lastResolved == null ? null : oReader.Change(lastResolved)); + } + catch (Exception e) when (e is RepoException or ValidationException) + { + // logger.atInfo().withCause(e): Error resolving change for lastResolved + } + + DestinationStatus? destinationStatus = _generalOptions.RepoTask( + "destination.previous_ref", () => GetDestinationStatus(lastResolved)); + + O? lastMigrated = _generalOptions.RepoTask( + "origin.last_migrated", + () => destinationStatus == null + ? null + : _origin.ResolveLastRev(destinationStatus.GetBaseline())); + + Change? lastMigratedChange = null; + try + { + lastMigratedChange = _generalOptions.RepoTask?>( + "origin.last_migrated", + () => lastMigrated == null ? null : oReader.Change(lastMigrated)); + } + catch (Exception e) when (e is RepoException or ValidationException) + { + // logger.atInfo().withCause(e): Error resolving change for lastMigrated + } + + IReadOnlyList> allChanges = _generalOptions.RepoTask>>( + "origin.changes", + () => + { + Origin.ChangesResponse changes = oReader.Changes(lastMigrated, lastResolved!); + if (!changes.IsEmpty()) + { + return changes.GetChanges(); + } + return ImmutableArray>.Empty; + }); + + WorkflowRunHelper helper = NewRunHelper( + // We shouldn't use this path for info + "shouldnt_be_used", + lastResolved!, + rawSourceRef: null, + // We don't create effects on info + _ => { }); + + var affectedChanges = new List>(); + foreach (var change in allChanges) + { + if (helper.GetMigratorForChange(change).ShouldSkipChange(change)) + { + continue; + } + affectedChanges.Add(change); + } + + MigrationReference migrationRef = MigrationReference.Create( + string.Format(CultureInfo.InvariantCulture, "workflow_{0}", _name), + lastMigrated, + lastMigratedChange, + affectedChanges, + lastResolvedChange); + + IReadOnlyList> originVersions = ImmutableArray>.Empty; + if (_workflowOptions.InfoIncludeVersions) + { + originVersions = oReader.GetVersions(); + } + + // TODO(port): reconcile - Info is the IMigration return type, but the + // workflow computes Info. Rebuild the info against the IRevision base type. + return Info.Create( + GetOriginDescription(), + GetDestinationDescription(), + migrationRef is { } mr + ? new[] { RebaseMigrationReference(mr) } + : Array.Empty>(), + originVersions.Select(c => (Change)(object)c).ToImmutableArray()); + }); + } + + private static MigrationReference RebaseMigrationReference(MigrationReference mr) => + // TODO(port): reconcile - unchecked cast reflecting Java's Info variance. + MigrationReference.Create( + mr.GetLabel(), + mr.LastMigrated, + (Change?)(object?)mr.LastMigratedChange, + mr.GetAvailableToMigrate().Select(c => (Change)(object)c), + (Change?)(object?)mr.LastResolvedChange); + + private DestinationStatus? GetDestinationStatus(O? revision) + { + if (GetLastRevisionFlag() != null) + { + return new DestinationStatus(GetLastRevisionFlag()!, ImmutableArray.Empty); + } + return CreateDryRunWriter(revision!) + .GetDestinationStatus(GetDestinationFiles(), GetRevIdLabel()); + } + + internal string GetRevIdLabel() => _customRevId ?? _origin.GetLabelName(); + + /// Create a writer that respects the effectiveDryRunMode value. + internal IDestination.IWriter CreateWriter(O revision) => + _destination.NewWriter(new WriterContext( + _name, + _workflowOptions.WorkflowIdentityUser, + _effectiveDryRunMode, + revision, + _destinationFiles.Roots())); + + /// Create a writer in dry-run mode. + internal IDestination.IWriter CreateDryRunWriter(O revision) => + _destination.NewWriter(new WriterContext( + _name, + _workflowOptions.WorkflowIdentityUser, + /*dryRun=*/ true, + revision, + _destinationFiles.Roots())); + + public ImmutableListMultimap GetOriginDescription() => _origin.Describe(_originFiles); + + public ImmutableListMultimap GetDestinationDescription() => + _destination.Describe(_destinationFiles); + + public IReadOnlyList> GetCredentialDescription() + { + var allCreds = ImmutableArray.CreateBuilder>(); + allCreds.AddRange(((IConfigItemDescription)_origin).DescribeCredentials("origin")); + allCreds.AddRange(((IConfigItemDescription)_destination).DescribeCredentials("destination")); + return allCreds.ToImmutable(); + } + + public Glob GetOriginFiles() => _originFiles; + + public Glob GetDestinationFiles() => _destinationFiles; + + public Console GetConsole() => _console; + + public WorkflowOptions GetWorkflowOptions() => _workflowOptions; + + public bool IsForce() => _force; + + public ITransformation? GetReverseTransformForCheck() => _reverseTransformForCheck; + + public bool IsVerbose() => _verbose; + + internal string? GetLastRevisionFlag() => _lastRevisionFlag; + + internal bool IsInitHistory() => _initHistoryFlag; + + public WorkflowMode GetMode() => _mode; + + public string GetModeString() => _mode.ToString(); + + internal bool IsCheckLastRevState() => _checkLastRevState; + + internal bool IsDryRunMode() => _effectiveDryRunMode; + + public bool IsDryRunModeField() => _dryRunModeField; + + public bool IsCheckout() => _checkout; + + /// + /// Migration identity tries to create a stable identifier for the migration that is stable between + /// Copybara invocations for the same reference. For example it will contain the copy.bara.sky + /// config file location relative to the root, the workflow name or the context reference used in + /// the request. + /// + /// This identifier can be used by destinations to reuse code reviews, etc. + /// + internal string GetMigrationIdentity(IRevision requestedRevision, TransformWork transformWork) + { + bool contextRefDefined = requestedRevision.ContextReference() != null; + // In iterative mode we want to use the revision, since we could have an export from + // git.origin(master) -> git.gerrit_destination. In that case we want to create one change + // per origin commit. + string ctxRef = contextRefDefined && _mode != WorkflowMode.Iterative + ? requestedRevision.ContextReference()! + : requestedRevision.AsString(); + if (_changeIdentity.IsEmpty) + { + return Identity.ComputeIdentity( + "ChangeIdentity", + ctxRef, + _name, + _mainConfigFile.GetIdentifier(), + _workflowOptions.WorkflowIdentityUser); + } + var sb = new StringBuilder(); + foreach (var token in _changeIdentity) + { + if (token.GetTokenType() == TokenType.Literal) + { + sb.Append(token.GetValue()); + } + else if (token.GetValue().Equals(Workflow.COPYBARA_CONFIG_PATH_IDENTITY_VAR, StringComparison.Ordinal)) + { + sb.Append(_mainConfigFile.GetIdentifier()); + } + else if (token.GetValue().Equals(Workflow.COPYBARA_WORKFLOW_NAME_IDENTITY_VAR, StringComparison.Ordinal)) + { + sb.Append(_name); + } + else if (token.GetValue().Equals(Workflow.COPYBARA_REFERENCE_IDENTITY_VAR, StringComparison.Ordinal)) + { + sb.Append(ctxRef); + } + else if (token.GetValue().StartsWith(Workflow.COPYBARA_REFERENCE_LABEL_VAR, StringComparison.Ordinal)) + { + string label = token.GetValue().Substring(Workflow.COPYBARA_REFERENCE_LABEL_VAR.Length); + string? labelValue = transformWork.GetLabel(label); + if (labelValue == null) + { + _console.Warn(string.Format( + CultureInfo.InvariantCulture, + "Couldn't find label '{0}'. Using the default identity algorithm", + label)); + return Identity.ComputeIdentity( + "ChangeIdentity", + ctxRef, + _name, + _mainConfigFile.GetIdentifier(), + _workflowOptions.WorkflowIdentityUser); + } + sb.Append(labelValue); + } + } + return Identity.HashIdentity( + new ToStringHelper("custom_identity").Add("text", sb.ToString()), + _workflowOptions.WorkflowIdentityUser); + } + + public ConfigFile GetMainConfigFile() => _mainConfigFile; + + public Profiler.Profiler Profiler() => _generalOptions.Profiler(); + + public EventMonitors EventMonitors() => _generalOptions.EventMonitors(); + + internal Func> GetAllConfigFiles() => _allConfigFiles; + + public GeneralOptions GetGeneralOptions() => _generalOptions; + + public IReadOnlyList GetAfterMigrationActions() => _afterMigrationActions; + + public IReadOnlyList GetAfterAllMigrationActions() => _afterAllMigrationActions; + + internal IReadOnlyList RunHooks( + IReadOnlyList effects, + IReadOnlyList actions, + LazyResourceLoader originEndpoint, + LazyResourceLoader destinationEndpoint, + IRevision resolvedRef) + { + var console = new Transform.SkylarkConsole(GetConsole()); + + var hookDestinationEffects = new List(); + foreach (var action in actions) + { + using (Profiler().Start(action.GetName())) + { + // logger.atInfo().log("Running after migration hook: %s", action.getName()); + // TODO(port): reconcile - FinishHookContext / Action.run(context) is not yet ported. + // The hook context construction and dispatch will be restored once feedback/ + // FinishHookContext lands. For now we skip running the hook to preserve compilation. + _ = action; + _ = originEndpoint; + _ = destinationEndpoint; + _ = resolvedRef; + _ = console; + } + } + var builder = ImmutableArray.CreateBuilder(); + builder.AddRange(effects); + builder.AddRange(hookDestinationEffects); + return builder.ToImmutable(); + } + + internal ImmutableArray GetChangeIdentity() => _changeIdentity; + + public bool IsSetRevId() => _setRevId; + + internal bool IsSmartPrune() => _smartPrune; + + public bool IsMergeImport() => _mergeImport != null; + + public MergeImportConfiguration? GetMergeImport() => _mergeImport; + + public bool IsConsistencyFileMergeImport() => + IsMergeImport() + && GetMergeImport()!.UseConsistencyFile() + && GetConsistencyFilePath() != null; + + // return whether the consistency file merge behavior has been toggled off + public bool DisableConsistencyMergeImport() => _workflowOptions.DisableConsistencyMergeImport; + + public string? GetConsistencyFilePath() => _consistencyFileConfig?.Path(); + + public ConsistencyFileConfiguration? GetConsistencyFileConfig() => _consistencyFileConfig; + + public AutoPatchfileConfiguration? GetAutoPatchfileConfiguration() => _autoPatchfileConfiguration; + + public bool IsMigrateNoopChanges() => _migrateNoopChanges; + + public string? CustomRevId() => _customRevId; + + public Glob GetReversibleCheckIgnoreFiles() => _reversibleCheckIgnoreFiles; + + public string? GetExpectedFixedRef() => _expectedFixedRef; + + public string? GetPinnedFixedRef() => _pinnedFixedRef; + + public ImmutableArray GetDefinitionStack() => _definitionStack; + + /// + /// Returns the Starlark call stack's captured local variables for each level. + /// + /// These are only filled if the capture-definition-stack-locals option is set on the + /// workflow. The outer list aligns to the definition stack's order. The inner map is a mapping of + /// local variable names to string representations of the variable values, for each level of the + /// stack. + /// + public ImmutableArray> GetDefinitionStackLocals() => + _definitionStackLocals; +} diff --git a/src/Copybara.Core/WorkflowMode.cs b/src/Copybara.Core/WorkflowMode.cs new file mode 100644 index 000000000..6fff9db92 --- /dev/null +++ b/src/Copybara.Core/WorkflowMode.cs @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara; + +/// +/// Workflow type to run between origin and destination. +/// +// In upstream Java, each enum constant carries a per-mode `run(WorkflowRunHelper)` implementation +// (SQUASH / ITERATIVE / CHANGE_REQUEST / CHANGE_REQUEST_FROM_SOT). C# enums cannot carry behavior; +// the run() dispatch and its helpers (FilterChanges, MaybeGetLastRev, IsHistorySupported, etc.) live +// in and are invoked from . +public enum WorkflowMode +{ + /// + /// Create a single commit in the destination with new tree state. + /// + Squash, + + /// + /// Import each origin change individually. + /// + Iterative, + + /// + /// Import an origin tree state diffed by a common parent in destination. This could be a GH + /// Pull Request, a Gerrit Change, etc. + /// + ChangeRequest, + + /// + /// Import from the Source-of-Truth. This mode is useful when, despite the pending change + /// being already in the SoT, the users want to review the code on a different system. + /// + ChangeRequestFromSot, +} diff --git a/src/Copybara.Core/WorkflowModeRunner.cs b/src/Copybara.Core/WorkflowModeRunner.cs new file mode 100644 index 000000000..06622e521 --- /dev/null +++ b/src/Copybara.Core/WorkflowModeRunner.cs @@ -0,0 +1,672 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using Copybara.Common; +using Copybara.Effect; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Util.Console; +using EffectType = Copybara.Effect.DestinationEffect.EffectType; + +namespace Copybara; + +/// +/// Per-mode run logic for . +/// +/// In upstream Java each WorkflowMode enum constant carries its own run +/// implementation. C# enums cannot carry behavior, so the dispatch and the shared helpers +/// (filterChanges, maybeGetLastRev, isHistorySupported, etc.) live here and are +/// dispatched from via . +/// +public static class WorkflowModeRunner +{ + /// Dispatches to the run logic for . + public static void Run(WorkflowMode mode, WorkflowRunHelper runHelper) + where O : class, IRevision + where D : class, IRevision + { + switch (mode) + { + case WorkflowMode.Squash: + RunSquash(runHelper); + break; + case WorkflowMode.Iterative: + RunIterative(runHelper); + break; + case WorkflowMode.ChangeRequest: + RunChangeRequestMode(runHelper); + break; + case WorkflowMode.ChangeRequestFromSot: + RunChangeRequestFromSot(runHelper); + break; + default: + throw new InvalidOperationException("Unexpected workflow mode: " + mode); + } + } + + /// Create a single commit in the destination with new tree state. + private static void RunSquash(WorkflowRunHelper runHelper) + where O : class, IRevision + where D : class, IRevision + { + IReadOnlyList> detectedChanges = ImmutableArray>.Empty; + IReadOnlyDictionary, Change> conditionalChanges = + ImmutableDictionary, Change>.Empty; + O? current = runHelper.GetResolvedRef(); + O? lastRev = null; + if (IsHistorySupported(runHelper)) + { + lastRev = MaybeGetLastRev(runHelper); + if (runHelper.WorkflowOptions().ImportSameVersion) + { + current = lastRev; // Import the last imported version. + } + else + { + Origin.ChangesResponse response = runHelper.GetChanges(lastRev, current!); + if (response.IsEmpty()) + { + ManageNoChangesDetectedForSquash( + runHelper, current!, lastRev, response.GetEmptyReason()); + } + else + { + detectedChanges = response.GetChanges(); + conditionalChanges = response.GetConditionalChanges(); + } + } + } + + var metadata = new Metadata( + runHelper.GetChangeMessage("Project import generated by Copybara.\n"), + // SQUASH workflows always use the default author if it was not forced. + runHelper.GetFinalAuthor(runHelper.GetAuthoring().GetDefaultAuthor()), + ImmutableListMultimap.Empty); + + runHelper.MaybeValidateRepoInLastRevState(metadata); + + // Don't replace helperForChanges with runHelper since origin_files could be potentially + // different in the helper for the current change. + ChangeMigrator helperForChanges = detectedChanges.Count == 0 + ? runHelper.GetDefaultMigrator() + : runHelper.GetMigratorForChange(detectedChanges[^1]); + + // Remove changes that don't affect origin_files + IReadOnlyList> changes = FilterChanges( + detectedChanges, conditionalChanges, helperForChanges); + if (changes.Count == 0 + && IsHistorySupported(runHelper) + && !runHelper.WorkflowOptions().ImportSameVersion) + { + ManageNoChangesDetectedForSquash( + runHelper, current!, lastRev, Origin.EmptyReason.NoChanges); + } + + // Try to use the latest change that affected the origin_files roots instead of the current + // revision, that could be an unrelated change. + current = changes.Count == 0 ? current : changes[^1].GetRevision(); + + if (runHelper.IsSquashWithoutHistory()) + { + changes = ImmutableArray>.Empty; + } + ValidationException.CheckCondition( + current != null, + "Could not process ref. If using --same-version flag, please either (1) check our" + + " METADATA file to confirm version is properly formatted or (2) also use the" + + " --last-rev flag to manually specify"); + helperForChanges.Migrate( + current!, + lastRev, + runHelper.GetConsole(), + metadata, + // Squash notes an Skylark API expect last commit to be the first one. + new Changes(changes.Reverse().Cast(), Array.Empty()), + /* destinationBaseline= */ null, + runHelper.GetResolvedRef(), + runHelper.GetOriginBaselineForMergeImport(lastRev)); + } + + /// Import each origin change individually. + private static void RunIterative(WorkflowRunHelper runHelper) + where O : class, IRevision + where D : class, IRevision + { + O? lastRev = runHelper.GetLastRev(); + Origin.ChangesResponse changesResponse = + runHelper.GetChanges(lastRev, runHelper.GetResolvedRef()); + if (changesResponse.IsEmpty()) + { + ValidationException.CheckCondition( + changesResponse.GetEmptyReason() != Origin.EmptyReason.UnrelatedRevisions, + "last imported revision %s is not ancestor of requested revision %s", + lastRev!, + runHelper.GetResolvedRef()); + throw new EmptyChangeException( + "No new changes to import for resolved ref: " + + runHelper.GetResolvedRef().AsString()); + } + int changeNumber = 1; + + var changes = changesResponse.GetChanges().ToImmutableArray(); + int limit = changes.Length; + if (runHelper.WorkflowOptions().IterativeLimitChanges < changes.Length) + { + limit = runHelper.WorkflowOptions().IterativeLimitChanges; + runHelper.GetConsole().Info(string.Format( + CultureInfo.InvariantCulture, + "Importing first {0} change(s) out of {1}", + limit, changes.Length)); + } + + runHelper.MaybeValidateRepoInLastRevState(metadata: null); + + bool useIterativeMergeImport = runHelper.GetGeneralOptions() + .IsTemporaryFeature("experimental_iterative_merge_import", false); + O? originBaseline = + useIterativeMergeImport ? runHelper.GetOriginBaselineForMergeImport(lastRev) : null; + + if (useIterativeMergeImport && runHelper.IsMergeImport()) + { + runHelper.GetConsole().Warn( + "Iterative Merge Import is experimental. Please monitor the generated changelists."); + } + + var migrated = new LinkedList(); + int migratedChanges = 0; + int index = 0; + while (index < changes.Length && migratedChanges < limit) + { + Change change = changes[index]; + string prefix = string.Format( + CultureInfo.InvariantCulture, + "Change {0} of {1} ({2}): ", + changeNumber, Math.Min(changes.Length, limit), change.GetRevision().AsString()); + + bool errors = false; + using (runHelper.Profiler().Start(change.GetRevision().AsString())) + { + var current = new object[] { change }; + ChangeMigrator migrator = runHelper.GetMigratorForChange(change); + if (migrator.SkipChange(change)) + { + index++; + continue; + } + IReadOnlyList result; + try + { + result = migrator.Migrate( + change.GetRevision(), + lastRev, + new PrefixConsole(prefix, runHelper.GetConsole()), + new Metadata( + runHelper.GetChangeMessage(change.GetMessage()), + runHelper.GetFinalAuthor(change.GetAuthor()), + ImmutableListMultimap.Empty), + new Changes(current, migrated), + /* destinationBaseline= */ null, + // Use the current change since we might want to create different reviews in + // the destination. + change.GetRevision(), + originBaseline); + migratedChanges++; + foreach (var effect in result) + { + if (effect.Type != EffectType.NOOP) + { + errors |= effect.Errors.Count != 0; + } + } + } + catch (EmptyChangeException e) + { + runHelper.GetConsole().WarnFmt( + "Migration of origin revision '%s' resulted in an empty change in the" + + " destination: %s", + change.GetRevision().AsString(), e.Message); + } + catch (Exception e) when (e is ValidationException or RepoException) + { + runHelper.GetConsole().ErrorFmt( + "Migration of origin revision '%s' failed with error: %s", + change.GetRevision().AsString(), e.Message); + throw; + } + } + migrated.AddFirst(change); + + bool hasNext = index + 1 < changes.Length; + if (errors && hasNext) + { + // Use the regular console to log prompt and final message, easier to spot. + if (!runHelper.GetConsole().PromptConfirmation("Continue importing next change?")) + { + string message = string.Format( + CultureInfo.InvariantCulture, + "Iterative workflow aborted by user after: {0}", + prefix); + runHelper.GetConsole().Warn(message); + throw new ChangeRejectedException(message); + } + } + if (useIterativeMergeImport) + { + lastRev = change.GetRevision(); + originBaseline = change.GetRevision(); + } + changeNumber++; + index++; + } + if (migratedChanges == 0) + { + throw new EmptyChangeException( + string.Format( + CultureInfo.InvariantCulture, + "Iterative workflow produced no changes in the destination for resolved ref: {0}", + runHelper.GetResolvedRef().AsString())); + } + // logger.atInfo().log("Imported %d change(s) out of %d", migratedChanges, changes.Length); + } + + /// + /// Import an origin tree state diffed by a common parent in destination. This could be a GH Pull + /// Request, a Gerrit Change, etc. + /// + private static void RunChangeRequestMode(WorkflowRunHelper runHelper) + where O : class, IRevision + where D : class, IRevision + { + ValidationException.CheckCondition( + runHelper.DestinationSupportsPreviousRef(), + "'%s' is incompatible with destinations that don't support history (For example" + + " folder.destination)", + WorkflowMode.ChangeRequest); + string originLabelName = runHelper.GetLabelNameWhenOrigin(); + Origin.Baseline? baseline = + string.IsNullOrEmpty(runHelper.WorkflowOptions().ChangeBaseline) + ? runHelper.GetOriginReader().FindBaseline(runHelper.GetResolvedRef(), originLabelName) + : new Origin.Baseline( + runHelper.WorkflowOptions().ChangeBaseline, /*originRevision=*/ null); + + RunChangeRequest( + runHelper, + baseline, + runHelper.WorkflowOptions().BaselineForMergeImport == null + ? (baseline + ?? throw new ValidationException( + "Cannot read origin revision from baseline, please specify the baseline" + + " using the --baseline-for-merge-import flag.")) + .GetOriginRevision() + : runHelper.OriginResolveLastRev(runHelper.WorkflowOptions().BaselineForMergeImport!)); + } + + /// Import from the Source-of-Truth. + private static void RunChangeRequestFromSot(WorkflowRunHelper runHelper) + where O : class, IRevision + where D : class, IRevision + { + IReadOnlyList originBaselines; + if (string.IsNullOrEmpty(runHelper.WorkflowOptions().ChangeBaseline)) + { + originBaselines = runHelper.GetOriginReader().FindBaselinesWithoutLabel( + runHelper.GetResolvedRef(), + runHelper.WorkflowOptions().ChangeRequestFromSotLimit); + } + else + { + originBaselines = ImmutableArray.Create( + runHelper.OriginResolveLastRev(runHelper.WorkflowOptions().ChangeBaseline)); + } + + Origin.Baseline? destinationBaseline = GetDestinationBaseline(runHelper, originBaselines); + + if (destinationBaseline == null) + { + ValidationException.CheckCondition( + originBaselines.Count != 0, + "Couldn't find any parent change for %s and origin_files = %s", + runHelper.GetResolvedRef().AsString(), runHelper.GetOriginFiles()); + + throw ValidationException.RetriableException(string.Format( + CultureInfo.InvariantCulture, + "Couldn't find a change in the destination with {0} label that matches a change from" + + " the origin. Make sure to sync the submitted changes from the origin ->" + + " destination first or use SQUASH mode or use {1}", + runHelper.GetOriginLabelName(), + WorkflowOptions.ChangeRequestFromSotLimitFlag)); + } + RunChangeRequest(runHelper, destinationBaseline, null); + } + + private static Origin.Baseline? GetDestinationBaseline( + WorkflowRunHelper runHelper, IReadOnlyList originRevision) + where O : class, IRevision + where D : class, IRevision + { + Origin.Baseline? result = GetDestinationBaselineOneAttempt(runHelper, originRevision); + if (result != null) + { + return result; + } + + foreach (int delay in runHelper.WorkflowOptions().ChangeRequestFromSotRetry) + { + runHelper.GetConsole().WarnFmt( + "Couldn't find a change in the destination with %s label and %s value." + + " Retrying in %s seconds...", + runHelper.GetOriginLabelName(), originRevision, delay); + try + { + Thread.Sleep(TimeSpan.FromSeconds(delay)); + } + catch (ThreadInterruptedException e) + { + throw new RepoException( + "Interrupted while waiting for CHANGE_REQUEST_FROM_SOT destination baseline to" + + " be available", + e); + } + result = GetDestinationBaselineOneAttempt(runHelper, originRevision); + if (result != null) + { + return result; + } + } + return null; + } + + private static Origin.Baseline? GetDestinationBaselineOneAttempt( + WorkflowRunHelper runHelper, IReadOnlyList originRevisions) + where O : class, IRevision + where D : class, IRevision + { + var result = new Origin.Baseline?[] { null }; + runHelper.GetDestinationWriter().VisitChangesWithAnyLabel( + /* start= */ null, + new[] { runHelper.GetOriginLabelName() }, + new ChangeRequestFromSotVisitor(runHelper, originRevisions, result)); + return result[0]; + } + + private sealed class ChangeRequestFromSotVisitor : IChangesLabelVisitor + where O : class, IRevision + { + private readonly object _runHelper; + private readonly IReadOnlyList _originRevisions; + private readonly Origin.Baseline?[] _result; + + public ChangeRequestFromSotVisitor( + object runHelper, IReadOnlyList originRevisions, Origin.Baseline?[] result) + { + _runHelper = runHelper; + _originRevisions = originRevisions; + _result = result; + } + + public VisitResult Visit( + Change change, IReadOnlyDictionary matchedLabels) + { + foreach (string value in matchedLabels.Values) + { + foreach (O originRevision in _originRevisions) + { + if (RevisionWithoutReviewInfo(originRevision.AsString()) + .Equals(RevisionWithoutReviewInfo(value), StringComparison.Ordinal)) + { + _result[0] = new Origin.Baseline( + change.GetRevision().AsString(), originRevision); + return VisitResult.Terminate; + } + } + } + return VisitResult.Continue; + } + } + + /// + /// Technically revisions can contain additional metadata in the String. For example: + /// 'aaaabbbbccccddddeeeeffff1111222233334444 PatchSet-1'. This method returns the identification + /// part. + /// + private static string RevisionWithoutReviewInfo(string r) => + System.Text.RegularExpressions.Regex.Replace(r, " .*", ""); + + private static void RunChangeRequest( + WorkflowRunHelper runHelper, + Origin.Baseline? baseline, + O? baselineForMergeImport) + where O : class, IRevision + where D : class, IRevision + { + ValidationException.CheckCondition( + baseline != null, + "Cannot find matching parent commit in the destination. Use '%s' flag to force a parent" + + " commit to use as baseline in the destination.", + WorkflowOptions.ChangeRequestParentFlag); + // logger.atInfo().log("Found baseline %s", baseline.getBaseline()); + + ChangeMigrator migrator = runHelper.GetDefaultMigrator(); + // If --change_request_parent was used, we don't have information about the origin changes + // included in the CHANGE_REQUEST so we assume the last change is the only change. + IReadOnlyList> changes; + if (baseline!.GetOriginRevision() == null) + { + changes = ImmutableArray.Create( + runHelper.GetOriginReader().Change(runHelper.GetResolvedRef())); + } + else + { + Origin.ChangesResponse changesResponse = runHelper.GetOriginReader().Changes( + baseline.GetOriginRevision(), runHelper.GetResolvedRef()); + if (changesResponse.IsEmpty()) + { + throw new EmptyChangeException(string.Format( + CultureInfo.InvariantCulture, + "Change '{0}' doesn't include any change for origin_files = {1}", + runHelper.GetResolvedRef(), runHelper.GetOriginFiles())); + } + changes = FilterChanges( + changesResponse.GetChanges(), changesResponse.GetConditionalChanges(), migrator); + if (changes.Count == 0) + { + string revs = changesResponse.GetChanges().Count == 0 + ? "Unknown" + : string.Join( + ", ", changesResponse.GetChanges().Select(c => c.GetRevision().AsString())); + migrator.FinishedMigrate( + ImmutableArray.Create( + new DestinationEffect( + EffectType.NOOP, + string.Format( + CultureInfo.InvariantCulture, + "Cannot migrate revisions [{0}]: {1}", + revs, "didn't affect any destination file"), + changesResponse.GetChanges().Cast().ToList(), + destinationRef: null))); + throw new EmptyChangeException(string.Format( + CultureInfo.InvariantCulture, + "Change '{0}' doesn't include any change for origin_files = {1}", + runHelper.GetResolvedRef(), runHelper.GetOriginFiles())); + } + } + + // --read-config-from-change is not implemented in CHANGE_REQUEST mode + migrator.Migrate( + runHelper.GetResolvedRef(), + /*lastRev=*/ null, + runHelper.GetConsole(), + // Use latest change as the message/author. If it contains multiple changes the user can + // always use metadata.squash_notes or similar. + new Metadata( + runHelper.GetChangeMessage(changes[^1].GetMessage()), + runHelper.GetFinalAuthor(changes[^1].GetAuthor()), + ImmutableListMultimap.Empty), + // Squash notes an Skylark API expect last commit to be the first one. + new Changes(changes.Reverse().Cast(), Array.Empty()), + baseline, + runHelper.GetResolvedRef(), + baselineForMergeImport); + } + + private static void RecordNoopDestinationEffect( + O current, O? lastRev, WorkflowRunHelper runHelper) + where O : class, IRevision + where D : class, IRevision + { + runHelper.GetDefaultMigrator().FinishedMigrate( + ImmutableArray.Create( + new DestinationEffect( + EffectType.NOOP, + string.Format( + CultureInfo.InvariantCulture, + "No changes to migrate {0}", + lastRev == null + ? string.Format( + CultureInfo.InvariantCulture, + "up to current revision '{0}'.", current.AsString()) + : string.Format( + CultureInfo.InvariantCulture, + "from '{0}' to current revision '{1}'", + lastRev.AsString(), current.AsString())), + /* originRefs= */ runHelper.GetChanges(lastRev, current).GetChanges() + .Cast().ToList(), + /* destinationRef= */ null))); + } + + private static void ManageNoChangesDetectedForSquash( + WorkflowRunHelper runHelper, O current, O? lastRev, Origin.EmptyReason emptyReason) + where O : class, IRevision + where D : class, IRevision + { + switch (emptyReason) + { + case Origin.EmptyReason.NoChanges: + { + string noChangesMsg = string.Format( + CultureInfo.InvariantCulture, + "No changes{0} up to {1} match any origin_files", + lastRev == null ? "" : " from " + lastRev.AsString(), current.AsString()); + if (!runHelper.IsForce()) + { + RecordNoopDestinationEffect(current, lastRev, runHelper); + throw new EmptyChangeException(string.Format( + CultureInfo.InvariantCulture, + "{0}. Use {1} if you really want to run the migration anyway (For example if" + + " the copy.bara.sky file has changed).", + noChangesMsg, GeneralOptions.Force)); + } + runHelper.GetConsole().WarnFmt( + "%s. Migrating anyway because of %s", noChangesMsg, GeneralOptions.Force); + break; + } + case Origin.EmptyReason.ToIsAncestor: + { + if (!runHelper.IsForce()) + { + throw new EmptyChangeException(string.Format( + CultureInfo.InvariantCulture, + "'{0}' has been already migrated. Use {1} if you really want to run the" + + " migration again (For example if the copy.bara.sky file has changed).", + current.AsString(), GeneralOptions.Force)); + } + runHelper.GetConsole().WarnFmt( + "'%s' has been already migrated. Migrating anyway because of %s", + lastRev!.AsString(), GeneralOptions.Force); + break; + } + case Origin.EmptyReason.UnrelatedRevisions: + { + ValidationException.CheckCondition( + runHelper.IsForce(), + string.Format( + CultureInfo.InvariantCulture, + "Last imported revision '{0}' is not an ancestor of the revision currently" + + " being migrated ('{1}'). Use {2} if you really want to migrate the" + + " reference.", + lastRev!, current.AsString(), GeneralOptions.Force)); + runHelper.GetConsole().WarnFmt( + "Last imported revision '%s' is not an ancestor of the revision currently being" + + " migrated ('%s')", + lastRev, current.AsString()); + break; + } + } + } + + public static bool IsHistorySupported(WorkflowRunHelper helper) + where O : class, IRevision + where D : class, IRevision => + helper.DestinationSupportsPreviousRef() && helper.GetOriginReader().SupportsHistory(); + + internal static IReadOnlyList> FilterChanges( + IReadOnlyList> detectedChanges, + IReadOnlyDictionary, Change> conditionalChanges, + ChangeMigrator changeMigrator) + where O : class, IRevision + where D : class, IRevision + { + var includedChanges = detectedChanges + .Where(e => !changeMigrator.SkipChange(e)) + .ToList(); + + // For all the changes that should be included based on skipChange, find the ones that should + // be added unconditionally. + var unconditionalChanges = includedChanges + .Where(e => !conditionalChanges.ContainsKey(e)) + .ToImmutableHashSet(); + + // Only include unconditional changes or conditional changes whose dependant change is + // included. + return includedChanges + .Where(e => unconditionalChanges.Contains(e) + || (conditionalChanges.TryGetValue(e, out var dep) && unconditionalChanges.Contains(dep))) + .ToImmutableArray(); + } + + /// Returns the last rev if possible. If --force is not enabled it will fail if not found. + public static O? MaybeGetLastRev(WorkflowRunHelper runHelper) + where O : class, IRevision + where D : class, IRevision + { + try + { + return runHelper.GetLastRev(); + } + catch (CannotResolveRevisionException e) + { + if (runHelper.IsForce()) + { + runHelper.GetConsole().WarnFmt( + "Cannot find last imported revision, but proceeding because of %s flag", + GeneralOptions.Force); + } + else + { + throw new ValidationException( + string.Format( + CultureInfo.InvariantCulture, + "Cannot find last imported revision. Use {0} if you really want to proceed" + + " with the migration use, or use '--last-rev' to override the revision.", + GeneralOptions.Force), + e); + } + return null; + } + } +} diff --git a/src/Copybara.Core/WorkflowOptions.cs b/src/Copybara.Core/WorkflowOptions.cs new file mode 100644 index 000000000..578d89a5b --- /dev/null +++ b/src/Copybara.Core/WorkflowOptions.cs @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Authoring; +using Copybara.Util; + +namespace Copybara; + +/// +/// Arguments for Workflow components. +/// +public class WorkflowOptions : IOption +{ + internal const string ChangeRequestParentFlag = "--change-request-parent"; + internal const string ChangeRequestParentFlagAlt = "--change_request_parent"; + + internal const string ReadConfigFromChangeFlag = "--read-config-from-change"; + internal const string ChangeRequestFromSotLimitFlag = "--change-request-from-sot-limit"; + public const string SkipTransformFlagName = "--skip-transforms"; + internal const string InitHistoryFlag = "--init-history"; + public const string CheckLastRevStateFlag = "--check-last-rev-state"; + + [Flag( + new[] { ChangeRequestParentFlag, ChangeRequestParentFlagAlt }, + "Commit revision to be used as parent when importing a commit using CHANGE_REQUEST" + + " workflow mode. This shouldn't be needed in general as Copybara is able to detect" + + " the parent commit message.")] + public string ChangeBaseline { get; set; } = ""; + + /// + /// Public so that it can be used programmatically. + /// + [Flag("--last-rev", "Last revision that was migrated to the destination")] + public string? LastRevision { get; set; } + + [Flag( + "--expected-fixed-ref", + "The fixed reference that we expect the migrate ref argument to resolve to. If they do" + + " not match, the tool will exit with a NOOP status.", + Hidden = true)] + public string? ExpectedFixedRef { get; set; } + + [Flag( + "--pinned-fixed-ref", + "The fixed reference that we pin the migration to. The reference passed in should resolve" + + " to this SHA1, or the commit referenced by this SHA1 should be an ancestor of the" + + " provided reference. If this is not the case, the tool will exit with a NOOP" + + " status.", + Hidden = true)] + public string? PinnedFixedRef { get; set; } + + [Flag( + "--same-version", + "Re-import the last version imported. This is useful for example to check that" + + " a refactor in a copy.bara.sky file doesn't introduce accidental changes.")] + public bool ImportSameVersion { get; set; } + + [Flag( + InitHistoryFlag, + "Import all the changes from the beginning of the history up to the resolved" + + " ref. For 'ITERATIVE' workflows this will import individual changes since the first " + + "one. For 'SQUASH' it will import the squashed change up to the resolved ref. " + + "WARNING: Use with care, this flag should be used only for the very first run of " + + "Copybara for a workflow.")] + public bool InitHistory { get; set; } + + [Flag( + "--iterative-limit-changes", + "Import just a number of changes instead of all the pending ones")] + public int IterativeLimitChanges { get; set; } = int.MaxValue; + + [Flag( + "--ignore-noop", + "Only warn about operations/transforms that didn't have any effect." + + " For example: A transform that didn't modify any file, non-existent origin" + + " directories, etc.")] + public bool IgnoreNoop { get; set; } + + [Flag( + "--info-include-versions", + "Include upstream versions in the info command output.")] + public bool InfoIncludeVersions { get; set; } = true; + + [Flag( + "--squash-skip-history", + "Avoid exposing the history of changes that are being migrated. This is" + + " useful when we want to migrate a new repository but we don't want to expose all" + + " the change history to metadata.squash_notes.")] + public bool SquashSkipHistory { get; set; } + + [Flag( + "--import-noop-changes", + "By default Copybara will only try to migrate changes that could affect the" + + " destination. Ignoring changes that only affect excluded files in origin_files. This" + + " flag disables that behavior and runs for all the changes.")] + public bool MigrateNoopChanges { get; set; } + + [Flag( + "--workflow-identity-user", + "Use a custom string as a user for computing change identity")] + public string? WorkflowIdentityUser { get; set; } + + [Flag( + CheckLastRevStateFlag, + "If enabled, Copybara will validate that the destination didn't change" + + " since last-rev import for destination_files. Note that this" + + " flag doesn't work for CHANGE_REQUEST mode.")] + public bool CheckLastRevState { get; set; } + + [Flag( + "--threads", + "Number of threads to use when running transformations that change lot of files")] + public int Threads { get; set; } = Environment.ProcessorCount; + + [Flag( + ChangeRequestFromSotLimitFlag, + "Number of origin baseline changes to use for trying to match one in the" + + " destination. It can be used if the are many parent changes in the origin that are a" + + " no-op in the destination")] + public int ChangeRequestFromSotLimit { get; set; } = 500; + + [Flag( + "--threads-min-size", + "Minimum size of the lists to process to run them in parallel")] + public int ThreadsMinSize { get; set; } = 100; + + [Flag( + "--notransformation-join", + "By default Copybara tries to join certain transformations in one so that it" + + " is more efficient. This disables the feature.")] + public bool NoTransformationJoin { get; set; } + + [Flag( + ReadConfigFromChangeFlag, + "For each imported origin change, load the workflow's origin_files, " + + "destination_files and transformations from the config version of that change. The " + + "rest of the fields (more importantly, " + + "origin and destination) cannot change and the version from the first config will be " + + "used.")] + internal bool ReadConfigFromChange { get; set; } + + [Flag( + ReadConfigFromChangeFlag + "-disable", + ReadConfigFromChangeFlag + + " is a arity 0 flag, this flag overrides it to override it being enabled.", + Arity = 1)] + internal bool DisableReadConfigFromChange { get; set; } + + [Flag( + "--read-config-from-head-paths", + "When " + ReadConfigFromChangeFlag + " flag is used, read the following" + + " path from head instead. This flag allows to unblock migrations due to config" + + " libraries bugs. The paths accept globs syntax (**, ?, etc.)", + Hidden = true)] + public Glob? ReadConfigFromChangePaths { get; set; } + + [Flag("--nosmart-prune", "Disable smart prunning")] + internal bool NoSmartPrune { get; set; } + + [Flag( + "--to-folder", + "Sometimes a user wants to test what the outcome would be for a workflow without changing" + + " the configuration or adding an auxiliary testing workflow. This flag allows to" + + " change an existing workflow to use folder.destination")] + internal bool ToFolder { get; set; } + + [Flag( + "--capture-definition-stack-locals", + "Captures local variables at each level of the Starlark definition stack.", + Hidden = true)] + public bool CaptureDefinitionStackLocals { get; set; } + + [Flag( + "--change-request-from-sot-retry", + "Number of retries and delay between retries when we cannot find the baseline" + + " in the destination for CHANGE_REQUEST_FROM_SOT. For example '10,30,60' will retry" + + " three times. The first retry will be delayed 10s, the second one 30s and the third" + + " one 60s")] + public List ChangeRequestFromSotRetry { get; set; } = new(); + + [Flag( + "--default-author", + "Use this author as default instead of the one in the config file." + + "Format should be 'Foo Bar '")] + internal string? DefaultAuthor { get; set; } + + [Flag( + "--force-message", + "Force the change description to this. Note that this only changes the message" + + " before the transformations happen, you can still use the transformations" + + " to alter it.")] + internal string? ForcedChangeMessage { get; set; } + + [Flag( + "--force-author", + "Force the author to this. Note that this only changes the author" + + " before the transformations happen, you can still use the transformations" + + " to alter it.")] + internal Author? ForcedAuthor { get; set; } + + [Flag( + "--diff-in-origin", + "When this flag is enabled, copybara will show different changes between last" + + " Revision and current revision in origin instead of in destination. NOTE: it Only" + + " works for SQUASH and ITERATIVE")] + public bool DiffInOrigin { get; set; } + + [Flag( + "--baseline-for-merge-import", + "Origin baseline to use for merge import. This overrides any inferred origin baseline")] + public string? BaselineForMergeImport { get; set; } + + [Flag( + "--threads-for-merge-import", + "Number of threads to use for executing the diff tool for the merge import mode.")] + internal int ThreadsForMergeImport { get; set; } = 40; + + [Flag( + "--debug-merge-import", + "Debug merge import for files matching the regex. You can use something like" + + " \".*/myfile.cc\" and it will show debugging information for files that matches" + + " that regex (e.g. foo/myfile.cc)", + Hidden = true, + Arity = 1)] + internal string? DebugMergeImport { get; set; } + + [Flag( + "--disable-consistency-merge-import", + "If merge import is set to use consistency in the config, disable it for this run. This" + + " uses an import baseline instead. A new consistency file will still be generated.", + Arity = 1)] + public bool DisableConsistencyMergeImport { get; set; } + + [Flag( + SkipTransformFlagName, + "List of transform names that should be skipped.")] + public List SkipTransforms { get; set; } = new(); + + public WorkflowOptions() + { + } + + /// Copy constructor. + public WorkflowOptions(WorkflowOptions other) + { + ChangeBaseline = other.ChangeBaseline; + LastRevision = other.LastRevision; + InitHistory = other.InitHistory; + IterativeLimitChanges = other.IterativeLimitChanges; + IgnoreNoop = other.IgnoreNoop; + SquashSkipHistory = other.SquashSkipHistory; + MigrateNoopChanges = other.MigrateNoopChanges; + WorkflowIdentityUser = other.WorkflowIdentityUser; + CheckLastRevState = other.CheckLastRevState; + Threads = other.Threads; + ChangeRequestFromSotLimit = other.ChangeRequestFromSotLimit; + ThreadsMinSize = other.ThreadsMinSize; + NoTransformationJoin = other.NoTransformationJoin; + ReadConfigFromChange = other.ReadConfigFromChange; + DisableReadConfigFromChange = other.DisableReadConfigFromChange; + ReadConfigFromChangePaths = other.ReadConfigFromChangePaths; + NoSmartPrune = other.NoSmartPrune; + ToFolder = other.ToFolder; + ChangeRequestFromSotRetry = other.ChangeRequestFromSotRetry; + DefaultAuthor = other.DefaultAuthor; + ForcedChangeMessage = other.ForcedChangeMessage; + ForcedAuthor = other.ForcedAuthor; + DiffInOrigin = other.DiffInOrigin; + ExpectedFixedRef = other.ExpectedFixedRef; + PinnedFixedRef = other.PinnedFixedRef; + } + + public WorkflowOptions(string changeBaseline, string? lastRevision, bool checkLastRevState) + { + ChangeBaseline = changeBaseline; + LastRevision = lastRevision; + CheckLastRevState = checkLastRevState; + } + + public bool CanUseSmartPrune() => !NoSmartPrune; + + /// + /// Returns the forced default author, or null if none was specified via the flag. + /// + public Author? GetDefaultAuthorFlag() + { + if (DefaultAuthor == null) + { + return null; + } + + return Author.Parse(DefaultAuthor); + } + + public bool IsReadConfigFromChange() => ReadConfigFromChange && !DisableReadConfigFromChange; + + private LocalParallelizer? _parallelizer; + + private int GetThreads() + { + // logger.atInfo().log("Using %d thread(s) for transformations", threads); + return Threads; + } + + public LocalParallelizer Parallelizer() + { + return _parallelizer ??= new LocalParallelizer(GetThreads(), ThreadsMinSize); + } + + public bool JoinTransformations() => !NoTransformationJoin; + + public string? GetLastRevision() => LastRevision; + + public bool IsInitHistory() => InitHistory; + + public string GetChangeBaseline() => ChangeBaseline; + + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is not WorkflowOptions that) + { + return false; + } + + return ChangeBaseline == that.ChangeBaseline + && LastRevision == that.LastRevision; + } + + public override int GetHashCode() => HashCode.Combine(ChangeBaseline, LastRevision); + + public WorkflowOptions WithInitHistory(bool initHistory) + { + var other = new WorkflowOptions(this) + { + InitHistory = initHistory, + }; + return other; + } +} diff --git a/src/Copybara.Core/WorkflowRunHelper.cs b/src/Copybara.Core/WorkflowRunHelper.cs new file mode 100644 index 000000000..36ec4ed05 --- /dev/null +++ b/src/Copybara.Core/WorkflowRunHelper.cs @@ -0,0 +1,1046 @@ +/* + * Copyright (C) 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Effect; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Util; +using Copybara.Util.Console; +using Console = Copybara.Util.Console.Console; +using ChangeMigrationFinishedEvent = Copybara.Monitor.IEventMonitor.ChangeMigrationFinishedEvent; +using ChangeMigrationStartedEvent = Copybara.Monitor.IEventMonitor.ChangeMigrationStartedEvent; +using EventMonitors = Copybara.Monitor.IEventMonitor.EventMonitors; +using EffectType = Copybara.Effect.DestinationEffect.EffectType; +using DestinationRef = Copybara.Effect.DestinationEffect.DestinationRef; + +namespace Copybara; + +/// Runs a single migration step for a , using its configuration. +/// Origin revision type. +/// Destination revision type. +public class WorkflowRunHelper + where O : class, IRevision + where D : class, IRevision +{ + private readonly Workflow _workflow; + private readonly string _workdir; + private readonly O _resolvedRef; + private readonly IOrigin.IReader _originReader; + protected readonly IDestination.IWriter Writer; + internal readonly string? RawSourceRef; + private readonly Action _migrationFinishedMonitor; + + public WorkflowRunHelper( + Workflow workflow, + string workdir, + O resolvedRef, + IOrigin.IReader originReader, + IDestination.IWriter destinationWriter, + string? rawSourceRef, + Action migrationFinishedMonitor) + { + _workflow = Preconditions.CheckNotNull(workflow); + _workdir = Preconditions.CheckNotNull(workdir); + _resolvedRef = Preconditions.CheckNotNull(resolvedRef); + _originReader = Preconditions.CheckNotNull(originReader); + Writer = Preconditions.CheckNotNull(destinationWriter); + RawSourceRef = rawSourceRef; + _migrationFinishedMonitor = Preconditions.CheckNotNull(migrationFinishedMonitor); + } + + public Action GetMigrationFinishedMonitor() => + _migrationFinishedMonitor; + + /// origin_files used for this workflow. + internal Glob GetOriginFiles() => _workflow.GetOriginFiles(); + + internal ChangeMigrator GetMigratorForChange(Change change) => + GetMigratorForChangeAndWriter(change, Writer); + + internal virtual ChangeMigrator GetMigratorForChangeAndWriter( + Change change, IDestination.IWriter writer) => + new(_workflow, _workdir, _originReader, writer, _resolvedRef, RawSourceRef, + _migrationFinishedMonitor); + + /// Get a default migrator for the current writer. + internal ChangeMigrator GetDefaultMigrator() => + new(_workflow, _workdir, _originReader, Writer, _resolvedRef, RawSourceRef, + _migrationFinishedMonitor); + + public Profiler.Profiler Profiler() => _workflow.Profiler(); + + protected string GetWorkdir() => _workdir; + + internal O GetResolvedRef() => _resolvedRef; + + /// Authoring configuration. + internal Authoring.Authoring GetAuthoring() => _workflow.GetAuthoring(); + + internal string GetChangeMessage(string message) => + WorkflowOptions().ForcedChangeMessage ?? message; + + public Author GetFinalAuthor(Author author) => + WorkflowOptions().ForcedAuthor ?? author; + + /// Console to use for printing messages. + internal Console GetConsole() => _workflow.GetConsole(); + + /// Options that change how workflows behave. + internal WorkflowOptions WorkflowOptions() => _workflow.GetWorkflowOptions(); + + internal GeneralOptions GetGeneralOptions() => _workflow.GetGeneralOptions(); + + internal O? GetOriginBaselineForMergeImport(O? lastRev) => + WorkflowOptions().BaselineForMergeImport == null + ? lastRev + : OriginResolveLastRev(WorkflowOptions().BaselineForMergeImport!); + + internal bool IsForce() => _workflow.IsForce(); + + internal bool IsMergeImport() => _workflow.IsMergeImport(); + + private bool IsInitHistory() => _workflow.IsInitHistory(); + + internal bool IsSquashWithoutHistory() => _workflow.GetWorkflowOptions().SquashSkipHistory; + + internal IDestination GetDestination() => _workflow.GetDestination(); + + internal IOrigin.IReader GetOriginReader() => _originReader; + + internal IDestination.IWriter GetDestinationWriter() => Writer; + + internal bool DestinationSupportsPreviousRef() => Writer.SupportsHistory(); + + internal void MaybeValidateRepoInLastRevState(Metadata? metadata) + { + if (!_workflow.IsCheckLastRevState() || IsForce()) + { + return; + } + + _workflow.GetGeneralOptions().IoRepoTask( + "validate_last_rev", + () => + { + O? lastRev = _workflow.GetGeneralOptions() + .RepoTask("get_last_rev", MaybeGetLastRev); + + if (lastRev == null) + { + // Not the job of this function to check for lastrev status. + return null; + } + Change change = _originReader.Change(lastRev); + var changes = new Changes(new object[] { change }, Array.Empty()); + // Create a new writer so that state is not shared with the regular writer. + ChangeMigrator migrator = GetMigratorForChangeAndWriter( + change, _workflow.CreateDryRunWriter(_resolvedRef)); + + try + { + _workflow.GetGeneralOptions().IoRepoTask( + "migrate", + () => + { + // We pass lastRev as the lastRev. This is not correct but we cannot know + // the previous rev of the last rev. This should only be used for + // generating messages, so users shouldn't care about the value (but they + // might care about its presence, so it cannot be null). + migrator.DoMigrate( + lastRev, + lastRev, + new PrefixConsole("Validating last migration: ", _workflow.GetConsole()), + metadata ?? new Metadata( + change.GetMessage(), + change.GetAuthor(), + ImmutableListMultimap.Empty), + changes, + /*destinationBaseline=*/ null, + lastRev, + null); + return null; + }); + throw new ValidationException( + "Migration of last-rev '" + + lastRev.AsString() + + "' didn't result in an empty change. This means that the result change" + + " of that migration was modified ouside of Copybara or that new changes" + + " happened later in the destination without using Copybara. Use --force" + + " if you really want to do the migration."); + } + catch (EmptyChangeException) + { + // EmptyChangeException ignored + } + return null; + }); + } + + internal Origin.ChangesResponse GetChanges(O? from, O to) + { + using (_workflow.Profiler().Start("get_changes")) + { + return _originReader.Changes(from, to); + } + } + + public string ImportAndTransformRevision( + Console console, + O lastRev, + O currentRev, + TransformWork.IResourceSupplier destinationReader) + { + ChangeMigrator migrator = GetDefaultMigrator(); + LazyResourceLoader originApi = + LazyResourceLoader.Memoized(c => GetOriginReader().GetFeedbackEndPoint(c!)); + LazyResourceLoader destinationApi = + LazyResourceLoader.Memoized(c => GetDestinationWriter().GetFeedbackEndPoint(c!)); + + return migrator.CheckoutBaselineAndTransform( + "premerge", + lastRev, + new Metadata( + "foo", new Author("foo", "foo@foo.com"), + ImmutableListMultimap.Empty), + currentRev, + console, + originApi, + destinationApi, + destinationReader); + } + + /// + /// Get last imported revision or fail if it cannot be found. + /// + /// if a last revision couldn't be found. + public O? GetLastRev() + { + O? lastRev = MaybeGetLastRev(); + if (lastRev == null && !IsInitHistory()) + { + throw new CannotResolveRevisionException(string.Format( + CultureInfo.InvariantCulture, + "Previous revision label {0} could not be found in {1} and --last-rev or" + + " --init-history flags were not passed", + GetOriginLabelName(), + _workflow.GetDestination())); + } + return lastRev; + } + + internal string GetOriginLabelName() => _workflow.GetRevIdLabel(); + + internal string GetLabelNameWhenOrigin() => + _workflow.CustomRevId() == null + ? _workflow.GetDestination().GetLabelNameWhenOrigin() + : _workflow.CustomRevId()!; + + /// + /// Returns the last revision that was imported from this origin to the destination. Returns + /// null if it cannot be determined. + /// + private O? MaybeGetLastRev() + { + if (_workflow.GetLastRevisionFlag() != null) + { + try + { + return OriginResolveLastRev(_workflow.GetLastRevisionFlag()!); + } + catch (RepoException e) + { + throw new CannotResolveRevisionException( + "Could not resolve --last-rev flag. Please make sure it exists in the origin: " + + _workflow.GetLastRevisionFlag(), + e); + } + } + DestinationStatus? status = Writer.GetDestinationStatus( + _workflow.GetDestinationFiles(), GetOriginLabelName()); + try + { + O? lastRev = status == null ? null : OriginResolveLastRev(status.GetBaseline()); + if (lastRev != null && _workflow.IsInitHistory()) + { + GetConsole().WarnFmt( + "Ignoring %s because a previous imported revision '%s' was found in the" + + " destination.", + Copybara.WorkflowOptions.InitHistoryFlag, lastRev.AsString()); + } + return lastRev; + } + catch (CannotResolveRevisionException e) + { + if (_workflow.IsInitHistory()) + { + // Expected to not find a revision if --init-history is provided + return null; + } + throw new CannotResolveRevisionException( + string.Format( + CultureInfo.InvariantCulture, + "Latest destination change has value '{0}' for label '{1}', but this does not" + + " resolve in the origin. This commonly happens if changes were merged" + + " outside the Source of Truth, several copybara workflows use the same" + + " label or if the origin history was re-written. Manually set the" + + " '--last-rev' flag to the export baseline to export a valid state to the" + + " destination.", + status?.GetBaseline(), + GetOriginLabelName()), + e); + } + } + + /// Resolve a string representation of a revision using the origin. + internal O OriginResolveLastRev(string revStr) => _workflow.GetOrigin().ResolveLastRev(revStr); + + public EventMonitors EventMonitors() => _workflow.EventMonitors(); +} + +/// +/// Migrate a change for a workflow. Can overwrite the reader, writer, transformations, etc. +/// +/// Port of the static nested WorkflowRunHelper.ChangeMigrator class. +/// +/// Origin revision type. +/// Destination revision type. +public class ChangeMigrator + where O : class, IRevision + where D : class, IRevision +{ + private readonly Workflow _headWorkflow; + private readonly string _workdir; + private readonly O _resolvedRef; + private readonly IOrigin.IReader _reader; + private readonly IDestination.IWriter _writer; + private readonly string? _rawSourceRef; + private readonly Action _migrationFinishedMonitor; + + internal ChangeMigrator( + Workflow workflow, + string workdir, + IOrigin.IReader reader, + IDestination.IWriter writer, + O resolvedRef, + string? rawSourceRef, + Action migrationFinishedMonitor) + { + _headWorkflow = Preconditions.CheckNotNull(workflow); + _workdir = Preconditions.CheckNotNull(workdir); + _resolvedRef = Preconditions.CheckNotNull(resolvedRef); + _reader = Preconditions.CheckNotNull(reader); + _writer = Preconditions.CheckNotNull(writer); + _rawSourceRef = rawSourceRef; + _migrationFinishedMonitor = Preconditions.CheckNotNull(migrationFinishedMonitor); + } + + protected virtual Workflow GetWorkflow() => _headWorkflow; + + /// + /// Return true if this change can be skipped because it would generate a noop in the destination. + /// + internal bool SkipChange(Change currentChange) + { + bool skipChange = ShouldSkipChange(currentChange); + if (skipChange) + { + GetWorkflow().GetConsole().VerboseFmt( + "Skipped change %s as it would create an empty result.", currentChange); + } + return skipChange; + } + + /// + /// Returns true iff the given change should be skipped based on the origin globs and flags + /// provided. + /// + internal bool ShouldSkipChange(Change currentChange) + { + if (GetWorkflow().IsMigrateNoopChanges()) + { + return false; + } + // We cannot know the files included. Try to migrate then. + if (currentChange.GetChangeFiles() == null) + { + return false; + } + IPathMatcher pathMatcher = GetOriginFiles().RelativeTo("/"); + foreach (string changedFile in currentChange.GetChangeFiles()!) + { + if (pathMatcher.Matches("/" + changedFile)) + { + return false; + } + } + // Heuristic for cases where the Copybara configuration is stored in the same folder as the + // origin code but excluded. The config root can be a subfolder of the files as seen by the + // origin. This might give some false positives but they would be noop migrations. + foreach (string changesFile in currentChange.GetChangeFiles()!) + { + foreach (string configPath in GetConfigFiles()) + { + if (changesFile.EndsWith(configPath, StringComparison.Ordinal)) + { + GetWorkflow().GetConsole().InfoFmt( + "Migrating %s because %s config file changed at that revision", + currentChange.GetRevision().AsString(), changesFile); + return false; + } + } + } + return true; + } + + protected ISet GetConfigFiles() => GetWorkflow().ConfigPaths(); + + internal Glob GetOriginFiles() => GetWorkflow().GetOriginFiles(); + + protected Glob GetDestinationFiles() => GetWorkflow().GetDestinationFiles(); + + protected ITransformation GetTransformation() => GetWorkflow().GetTransformation(); + + protected ITransformation? GetReverseTransformForCheck() => + GetWorkflow().GetReverseTransformForCheck(); + + protected Glob GetReversibleCheckIgnoreFiles() => GetWorkflow().GetReversibleCheckIgnoreFiles(); + + public Profiler.Profiler Profiler() => GetWorkflow().Profiler(); + + // provide the correct context reference when the --same-version flag is used + private O GetResolvedRefForTransform(O rev) + { + if (GetWorkflow().GetMode() == WorkflowMode.Squash + && GetWorkflow().GetWorkflowOptions().ImportSameVersion) + { + return rev; + } + return _resolvedRef; + } + + /// + /// Performs a full migration, including checking out files from the origin, deleting excluded + /// files, transforming the code, and writing to the destination. This writes to the destination + /// exactly once. + /// + public IReadOnlyList Migrate( + O rev, + O? lastRev, + Console processConsole, + Metadata metadata, + Changes changes, + Origin.Baseline? destinationBaseline, + O? changeIdentityRevision, + O? originBaselineForMergeImport) + { + IReadOnlyList effects = ImmutableArray.Empty; + Exception? lastException = null; + try + { + GetWorkflow().EventMonitors().DispatchEvent( + m => m.OnChangeMigrationStarted(new ChangeMigrationStartedEvent())); + effects = DoMigrate( + rev, + lastRev, + processConsole, + metadata, + changes, + destinationBaseline, + changeIdentityRevision, + originBaselineForMergeImport); + } + catch (RedundantChangeException e) + { + effects = ImmutableArray.Create( + new DestinationEffect( + EffectType.NOOP_AGAINST_PENDING_CHANGE, + string.Format( + CultureInfo.InvariantCulture, + "Cannot migrate revisions [{0}]: {1}", + CurrentRevsString(changes), + e.Message), + changes.GetCurrent().Cast().ToList(), + new DestinationRef(e.PendingRevision, "commit", url: null))); + lastException = e; + throw; + } + catch (EmptyChangeException empty) + { + effects = ImmutableArray.Create( + new DestinationEffect( + EffectType.NOOP, + string.Format( + CultureInfo.InvariantCulture, + "Cannot migrate revisions [{0}]: {1}", + CurrentRevsString(changes), + empty.Message), + changes.GetCurrent().Cast().ToList(), + destinationRef: null)); + lastException = empty; + throw; + } + catch (Exception e) when (e is not EmptyChangeException) + { + // Covers ValidationException, IOException, RepoException and RuntimeException. + bool userError = e is ValidationException; + effects = ImmutableArray.Create( + new DestinationEffect( + userError ? EffectType.ERROR : EffectType.TEMPORARY_ERROR, + "Errors happened during the migration", + changes.GetCurrent().Cast().ToList(), + destinationRef: null, + new[] { e.Message ?? e.ToString() })); + lastException = e; + throw; + } + finally + { + try + { + if (!GetWorkflow().GetGeneralOptions().DryRunMode) + { + try + { + using (Profiler().Start("after_migration")) + { + effects = GetWorkflow().RunHooks( + effects, + GetWorkflow().GetAfterMigrationActions(), + LazyResourceLoader.Memoized( + c => _reader.GetFeedbackEndPoint(c!)), + LazyResourceLoader.Memoized( + c => _writer.GetFeedbackEndPoint(c!)), + _resolvedRef); + } + } + catch (Exception e) when (e is ValidationException or RepoException) + { + if (lastException == null) + { + throw; + } + // lastException.addSuppressed(e) — no direct C# equivalent; the original + // exception is preserved by the surrounding throw in the catch blocks. + } + } + else if (GetWorkflow().GetAfterMigrationActions().Count != 0) + { + GetWorkflow().GetConsole().InfoFmt( + "Not calling 'after_migration' actions because of %s mode", + GeneralOptions.DryRunFlag); + } + } + finally + { + _migrationFinishedMonitor( + new ChangeMigrationFinishedEvent( + effects.ToImmutableArray(), + GetWorkflow().GetOriginDescription(), + GetWorkflow().GetDestinationDescription())); + } + } + return effects; + } + + private static string CurrentRevsString(Changes changes) => + changes.GetCurrent().Count == 0 + ? "Unknown" + : string.Join( + ", ", + changes.GetCurrent().Select(c => ((Change)c).GetRevision().AsString())); + + /// + /// Finish a migrate by noticing event monitor with the outcome effects. + /// + internal void FinishedMigrate(IReadOnlyList effects) + { + GetWorkflow().EventMonitors().DispatchEvent( + m => m.OnChangeMigrationStarted(new ChangeMigrationStartedEvent())); + _migrationFinishedMonitor( + new ChangeMigrationFinishedEvent( + effects.ToImmutableArray(), + GetWorkflow().GetOriginDescription(), + GetWorkflow().GetDestinationDescription())); + } + + private bool ShowDiffInOrigin(O rev, O? lastRev, Console processConsole) + { + if (!GetWorkflow().GetWorkflowOptions().DiffInOrigin + || GetWorkflow().GetMode() == WorkflowMode.ChangeRequest + || GetWorkflow().GetMode() == WorkflowMode.ChangeRequestFromSot + || lastRev == null) + { + return false; + } + string? diff = GetWorkflow().GetOrigin().ShowDiff(lastRev, rev); + if (diff == null) + { + throw new ValidationException( + "diff_in_origin is not supported by origin " + + ((IConfigItemDescription)GetWorkflow().GetOrigin()).GetTypeName()); + } + if (diff.Length == 0 && !GetWorkflow().GetGeneralOptions().IsForced()) + { + throw new EmptyChangeException("No difference at diff_in_origin"); + } + var sb = new System.Text.StringBuilder(); + foreach (string line in diff.Split('\n')) + { + sb.Append('\n'); + if (line.StartsWith("+", StringComparison.Ordinal)) + { + sb.Append(processConsole.Colorize(AnsiColor.Green, line)); + } + else if (line.StartsWith("-", StringComparison.Ordinal)) + { + sb.Append(processConsole.Colorize(AnsiColor.Red, line)); + } + else + { + sb.Append(line); + } + } + processConsole.Info(sb.ToString()); + if (!processConsole.PromptConfirmation( + string.Format( + CultureInfo.InvariantCulture, + "Continue to migrate with '{0}' to {1}?", + GetWorkflow().GetMode(), + ((IConfigItemDescription)GetWorkflow().GetDestination()).GetTypeName()))) + { + processConsole.Warn("Migration aborted by user."); + throw new ChangeRejectedException( + "User aborted execution: did not confirm diff in origin changes."); + } + return true; + } + + internal IReadOnlyList DoMigrate( + O rev, + O? lastRev, + Console processConsole, + Metadata metadata, + Changes changes, + Origin.Baseline? destinationBaseline, + O? changeIdentityRevision, + O? originBaselineForPrune) + { + string checkoutDir = System.IO.Path.Combine(_workdir, "checkout"); + using (Profiler().Start("prepare_workdir")) + { + processConsole.Progress("Cleaning working directory"); + if (Directory.Exists(_workdir)) + { + FileUtil.DeleteRecursively(_workdir); + } + Directory.CreateDirectory(checkoutDir); + } + processConsole.Progress("Checking out the change"); + bool isShowDiffInOrigin = ShowDiffInOrigin(rev, lastRev, processConsole); + + Checkout(rev, processConsole, checkoutDir, "origin.checkout"); + + string? originCopy = null; + Console console = GetWorkflow().GetConsole(); + if (GetReverseTransformForCheck() != null) + { + using (Profiler().Start("reverse_copy")) + { + console.Progress("Making a copy or the workdir for reverse checking"); + originCopy = System.IO.Path.Combine(_workdir, "origin"); + Directory.CreateDirectory(originCopy); + CopyForReverseCheck(checkoutDir, originCopy); + } + } + // Lazy loading to avoid running afoul of checks unless the instance is actually used. + LazyResourceLoader originApi = + LazyResourceLoader.Memoized(c => _reader.GetFeedbackEndPoint(c!)); + LazyResourceLoader destinationApi = + LazyResourceLoader.Memoized(c => _writer.GetFeedbackEndPoint(c!)); + var destinationReader = new FuncResourceSupplier( + () => _writer.GetDestinationReader( + console, (Origin.Baseline?)(object?)destinationBaseline, checkoutDir)); + + metadata = metadata.WithHiddenLabels(CliHiddenLabels()); + var transformWork = new TransformWork( + checkoutDir, + metadata, + changes, + console, + new MigrationInfo(GetWorkflow().GetRevIdLabel(), (IChangeVisitable?)_writer), + GetResolvedRefForTransform(rev), + originApi, + destinationApi, + destinationReader, + GetWorkflow().GetMode().ToString()) + .WithLastRev(lastRev) + .WithCurrentRev(rev) + .WithDestinationInfo(_writer.GetDestinationInfo()); + transformWork.AddLabel( + TransformWork.CopybaraConfigPathLabel, + GetWorkflow().GetMainConfigFile().GetIdentifier(), + "=", + hidden: true); + transformWork.AddLabel( + TransformWork.CopybaraWorkflowNameLabel, GetWorkflow().GetName(), "=", hidden: true); + + try + { + using (Profiler().Start("transforms")) + { + TransformationStatus status = GetTransformation().Transform(transformWork); + if (status.IsNoop()) + { + ShowInfoAboutNoop(console); + status.ThrowException(console, GetWorkflow().GetWorkflowOptions().IgnoreNoop); + } + } + } + catch (VoidOperationException) + { + // This happens if an inner sequence throws noop as an exception. + ShowInfoAboutNoop(console); + throw; + } + + if (GetReverseTransformForCheck() != null) + { + console.Progress("Checking that the transformations can be reverted"); + string reverse; + using (Profiler().Start("reverse_copy")) + { + reverse = System.IO.Path.Combine(_workdir, "reverse"); + Directory.CreateDirectory(reverse); + CopyForReverseCheck(checkoutDir, reverse); + } + + using (Profiler().Start("reverse_transform")) + { + TransformationStatus status = GetReverseTransformForCheck()!.Transform( + new TransformWork( + reverse, + transformWork.GetMetadata(), + changes, + console, + new MigrationInfo(originLabel: null, null), + GetResolvedRefForTransform(rev), + destinationApi, + originApi, + new FuncResourceSupplier( + () => DestinationReader.NotImplemented), + GetWorkflow().GetMode().ToString()) + .WithDestinationInfo(_writer.GetDestinationInfo())); + if (status.IsNoop()) + { + console.WarnFmt( + "No-op detected running the transformations in reverse. The most probably" + + " cause is that the transformations are not reversible."); + status.ThrowException(console, GetWorkflow().GetWorkflowOptions().IgnoreNoop); + } + } + + // TODO(port): reconcile - DiffUtil.diff/filterDiff and the reversible-diff comparison + // depend on the not-yet-ported com.google.copybara.util.DiffUtil. The forward/reverse + // transformations above have already run; the diff-based non-reversibility check is + // skipped until DiffUtil lands. + _ = originCopy; + } + + console.Progress("Checking that destination_files covers all files in transform result"); + // TODO(port): reconcile - ValidateDestinationFilesVisitor is not yet ported. + // new ValidateDestinationFilesVisitor(GetDestinationFiles(), checkoutDir).VerifyFilesToWrite(); + + var transformResult = new TransformResult( + checkoutDir, + rev, + transformWork.GetAuthor(), + transformWork.GetMessage(), + /* requestedRevision= */ GetResolvedRefForTransform(rev), + GetWorkflow().GetName(), + changes, + _rawSourceRef, + GetWorkflow().IsSetRevId(), + label => transformWork.GetAllLabels(label), + GetWorkflow().GetRevIdLabel()) + .WithDestinationInfo(transformWork.GetDestinationInfo()!); + + IReadOnlyList? mergeErrorPaths = null; + if (GetWorkflow().IsMergeImport()) + { + mergeErrorPaths = RunMergeImport( + console, + _writer, + destinationBaseline, + checkoutDir, + lastRev, + metadata, + originBaselineForPrune, + originApi, + destinationApi, + transformWork); + if (mergeErrorPaths == null) + { + console.Warn( + "Unable to determine a baseline; disabling merge import. This is expected" + + " if this is an initial import. Otherwise, you may have to provide a" + + " baseline using --baseline-for-merge-import, or using an existing" + + " consistency file."); + } + } + if (mergeErrorPaths == null) + { + mergeErrorPaths = ImmutableArray.Empty; + if (GetWorkflow().GetConsistencyFilePath() != null) + { + // TODO(port): reconcile - ConsistencyFile.generateNoDiff is not yet ported. + throw new NotSupportedException( + "TODO(port): consistency file generation (ConsistencyFile) not yet ported."); + } + } + if (destinationBaseline != null) + { + transformResult = transformResult.WithBaseline(destinationBaseline.GetBaseline()); + if (GetWorkflow().IsSmartPrune() && GetWorkflow().GetWorkflowOptions().CanUseSmartPrune()) + { + ValidationException.CheckCondition( + destinationBaseline.GetOriginRevision() != null, + "smart_prune is not compatible with %s flag for now", + WorkflowOptions.ChangeRequestParentFlag); + // TODO(port): reconcile - smart_prune diffing depends on DiffUtil.diffFiles / + // DiffFile which are not yet ported. + throw new NotSupportedException( + "TODO(port): smart_prune diffing (DiffUtil) not yet ported."); + } + } + transformResult = transformResult + .WithAskForConfirmation(GetWorkflow().IsAskForConfirmation()) + .WithDiffInOrigin(isShowDiffInOrigin) + .WithIdentity(GetWorkflow().GetMigrationIdentity(changeIdentityRevision!, transformWork)) + .WithApprovalsProvider(GetWorkflow().GetOrigin().GetApprovalsProvider()); + + IReadOnlyList result; + using (Profiler().Start( + "destination.write", + Profiler().TaskType(((IConfigItemDescription)GetWorkflow().GetDestination()).GetTypeName()))) + { + result = _writer.Write(transformResult, GetDestinationFiles(), processConsole); + } + Preconditions.CheckNotNull(result, "Destination returned a null result."); + Preconditions.CheckState( + result.Count != 0, "Destination {0} returned an empty set of effects", _writer); + + if (mergeErrorPaths.Count != 0) + { + var mergeErrorDestinationEffect = new DestinationEffect( + EffectType.CREATED, + string.Format( + CultureInfo.InvariantCulture, + "Found merge errors for paths: {0}", + string.Join(", ", mergeErrorPaths)), + Array.Empty(), + new DestinationRef("merge_error", "merge_error", null)); + + var builder = ImmutableArray.CreateBuilder(); + builder.AddRange(result); + builder.Add(mergeErrorDestinationEffect); + result = builder.ToImmutable(); + } + return result; + } + + private ImmutableListMultimap CliHiddenLabels() + { + var builder = ImmutableListMultimap.CreateBuilder(); + foreach (var e in GetWorkflow().GetGeneralOptions().CliLabels()) + { + builder.Put(GeneralOptions.CliFlagPrefix + e.Key.ToUpperInvariant(), e.Value); + } + return builder.Build(); + } + + /// + /// a list of paths that resulted in merge errors, or null if a baseline could not be determined. + /// + private IReadOnlyList? RunMergeImport( + Console console, + IDestination.IWriter writer, + Origin.Baseline? destinationBaseline, + string checkoutDir, + O? lastRev, + Metadata metadata, + O? originBaselineForPrune, + LazyResourceLoader originApi, + LazyResourceLoader destinationApi, + TransformWork transformWork) + { + // TODO(port): reconcile - merge import depends on a number of not-yet-ported utilities: + // MergeImportTool, MergeRunner, CommandLineDiffUtil, ApplyDestinationPatch, ConsistencyFile, + // AutoPatchUtil and DestinationReader.copyDestinationFilesToDirectory batch behaviors. The + // full structure is preserved in the Java source; port those utilities and restore this + // method's body during consolidation. + _ = (console, writer, destinationBaseline, checkoutDir, lastRev, metadata, + originBaselineForPrune, originApi, destinationApi, transformWork); + throw new NotSupportedException( + "TODO(port): merge_import (MergeImportTool and friends) not yet ported."); + } + + internal static Glob PatchlessDestinationFiles(Workflow workflow) + { + Glob destinationFiles = workflow.GetDestinationFiles(); + AutoPatchfileConfiguration? autoPatchfileConfiguration = + workflow.GetAutoPatchfileConfiguration(); + if (autoPatchfileConfiguration != null) + { + // TODO(port): reconcile - AutoPatchUtil.getAutopatchGlob is not yet ported. + throw new NotSupportedException( + "TODO(port): AutoPatchUtil.getAutopatchGlob not yet ported."); + } + if (workflow.GetMergeImport()!.UseConsistencyFile()) + { + destinationFiles = Glob.Difference( + destinationFiles, + Glob.CreateGlob(new[] { workflow.GetConsistencyFilePath()! })); + } + return destinationFiles; + } + + internal static Glob ConsistencyFileGlob(Workflow workflow) => + Glob.CreateGlob(new[] { workflow.GetConsistencyFilePath()! }); + + internal string CheckoutBaselineAndTransform( + string subdirName, + O? lastRev, + Metadata metadata, + O baseline, + Console console, + LazyResourceLoader originApi, + LazyResourceLoader destinationApi, + TransformWork.IResourceSupplier destinationReader) + { + string baselineWorkdir = System.IO.Path.Combine(_workdir, subdirName); + Directory.CreateDirectory(baselineWorkdir); + + var baselineConsole = new PrefixConsole("Migrating baseline for diff: ", console); + Checkout(baseline, baselineConsole, baselineWorkdir, "origin.baseline.checkout"); + + var baselineTransformWork = new TransformWork( + baselineWorkdir, + // We don't care about the message or author and this guarantees that it will work + // with the transformations + metadata, + // We don't care about the changes that are imported. + Changes.Empty, + baselineConsole, + new MigrationInfo(GetWorkflow().GetRevIdLabel(), (IChangeVisitable?)_writer), + _resolvedRef, + originApi, + destinationApi, + destinationReader, + GetWorkflow().GetMode().ToString()) + // Again, we don't care about this + .WithLastRev(lastRev) + .WithCurrentRev(baseline) + .WithDestinationInfo(_writer.GetDestinationInfo()); + using (Profiler().Start("baseline_transforms")) + { + TransformationStatus status = GetTransformation().Transform(baselineTransformWork); + if (status.IsNoop() + // no-op baseline transformations are OK for smart prune. + && !GetWorkflow().IsSmartPrune()) + { + console.WarnFmt("No-op detected in baseline transformations"); + ShowInfoAboutNoop(console); + status.ThrowException(console, GetWorkflow().GetWorkflowOptions().IgnoreNoop); + } + } + return baselineWorkdir; + } + + private void ShowInfoAboutNoop(Console console) + { + console.WarnFmt( + "No-op detected, this could happen for several reasons:\n\n" + + " - origin_files doesn't include the files. Current origin_files: %s\n\n" + + " - Previous transformations didn't do what you were expecting. You can" + + " inspect the work directory state (if run locally) at %s\n\n" + + " - Current version of the config doesn't work for an older (or newer)" + + " revision being migrated. This can be fixed by either wrapping the failing" + + " transformation with %s" + + " so that it is ignored or, if your origin supports it, using" + + " %s flag to sync the config version to the change being migrated.", + console.Colorize(AnsiColor.Yellow, GetWorkflow().GetOriginFiles().ToString()), + console.Colorize(AnsiColor.Yellow, _workdir), + console.Colorize( + AnsiColor.Yellow, + "core.transform([your_transformation], noop_behavior = \"IGNORE_NOOP\")"), + console.Colorize(AnsiColor.Yellow, "--read-config-from-change")); + } + + private void Checkout(O rev, Console processConsole, string checkoutDir, string profileDescription) + { + if (GetWorkflow().IsCheckout()) + { + using (Profiler().Start( + profileDescription, + Profiler().TaskType(((IConfigItemDescription)GetWorkflow().GetOrigin()).GetTypeName()))) + { + _reader.Checkout(rev, checkoutDir); + } + } + + // Remove excluded origin files. + IPathMatcher originFiles = GetOriginFiles().RelativeTo(checkoutDir); + processConsole.Progress("Removing excluded origin files"); + + int deleted = FileUtil.DeleteFilesRecursively( + checkoutDir, FileUtil.NotPathMatcher(originFiles)); + if (deleted != 0) + { + processConsole.InfoFmt( + "Removed %s files from workdir that do not match origin_files", deleted); + } + } + + private void CopyForReverseCheck(string from, string to) + { + try + { + FileUtil.CopyFilesRecursively(from, to, FileUtil.CopySymlinkStrategy.FailOutsideSymlinks); + } + catch (SymlinkException e) + { + throw new ValidationException( + "Failed to perform reversible check of transformations due to a symlink that " + + "points outside the checkout dir. Consider removing this symlink from your " + + "origin_files or, alternatively, set reversible_check = False in your " + + "workflow.", + e); + } + } + + private sealed class FuncResourceSupplier : TransformWork.IResourceSupplier + { + private readonly Func _func; + + public FuncResourceSupplier(Func func) => _func = func; + + public T Get() => _func(); + } +} diff --git a/src/Copybara.Core/WriterContext.cs b/src/Copybara.Core/WriterContext.cs new file mode 100644 index 000000000..9fa611667 --- /dev/null +++ b/src/Copybara.Core/WriterContext.cs @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Revision; + +namespace Copybara; + +/// Writer context which includes all the information for creating a writer. +public class WriterContext +{ + private readonly string _workflowName; + private readonly string _workflowIdentityUser; + private readonly bool _dryRun; + private readonly IRevision _originalRevision; + private readonly ImmutableHashSet _roots; + + public WriterContext( + string workflowName, + string? workflowIdentityUser, + bool dryRun, + IRevision originalRevision, + ImmutableHashSet roots) + { + _workflowName = Preconditions.CheckNotNull(workflowName); + _workflowIdentityUser = workflowIdentityUser ?? Environment.UserName; + _dryRun = dryRun; + _originalRevision = Preconditions.CheckNotNull(originalRevision); + _roots = Preconditions.CheckNotNull(roots); + } + + public IRevision GetOriginalRevision() => _originalRevision; + + public string GetWorkflowIdentityUser() => _workflowIdentityUser; + + public string GetWorkflowName() => _workflowName; + + public bool IsDryRun() => _dryRun; + + public ImmutableHashSet GetRoots() => _roots; +} diff --git a/src/Starlark/Eval/Module.cs b/src/Starlark/Eval/Module.cs index a87a00e14..568bd368c 100644 --- a/src/Starlark/Eval/Module.cs +++ b/src/Starlark/Eval/Module.cs @@ -61,6 +61,27 @@ public static Module WithPredeclaredAndData( public static Module Create() => new(ImmutableDictionary.Empty, null, StarlarkSemantics.DEFAULT); + /// + /// Returns the module (file) of the -th innermost enclosing Starlark + /// function on the call stack, or null if the number of active calls that are functions defined + /// in Starlark is less than or equal to . + /// + /// This method is a temporary workaround for Starlarkification and should not be used + /// anywhere else. + /// + public static Module? OfInnermostEnclosingStarlarkFunction(StarlarkThread thread, int depth) + { + StarlarkFunction? fn = thread.GetInnermostEnclosingStarlarkFunction(depth); + return fn?.Module; + } + + /// + /// Returns the module (file) of the innermost enclosing Starlark function on the call stack, or + /// null if none of the active calls are functions defined in Starlark. + /// + public static Module? OfInnermostEnclosingStarlarkFunction(StarlarkThread thread) => + OfInnermostEnclosingStarlarkFunction(thread, 0); + /// Returns the client data associated with this module. public object? ClientData => clientData; diff --git a/src/Starlark/Eval/StarlarkThread.cs b/src/Starlark/Eval/StarlarkThread.cs index 601bd14c3..af8d74a99 100644 --- a/src/Starlark/Eval/StarlarkThread.cs +++ b/src/Starlark/Eval/StarlarkThread.cs @@ -139,6 +139,28 @@ internal void SetErrorLocation(Location loc) /// Returns the location of the program counter in the enclosing call frame. public Location GetCallerLocation() => Toplevel ? Location.BUILTIN : FrameAt(1).Loc; + /// + /// Returns the -th innermost enclosing Starlark function on the call + /// stack, or null if the number of active calls that are Starlark-defined functions is less than + /// or equal to . + /// + internal StarlarkFunction? GetInnermostEnclosingStarlarkFunction(int depth) + { + Copybara.Common.Preconditions.CheckArgument(depth >= 0); + for (int i = callstack.Count - 1; i >= 0; i--) + { + if (callstack[i].Fn is StarlarkFunction fn) + { + if (depth == 0) + { + return fn; + } + depth--; + } + } + return null; + } + /// Returns the size of the callstack. internal int GetCallStackSize() => callstack.Count; From 3a6b09cf8b11b771991c060ea3832a48c5730f8d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 08:58:08 +0000 Subject: [PATCH 11/15] Port git plumbing core + http, remotefile/archive/hashing, format/buildozer, toml/json/xml/html/re2, credentials Wave B (6 agents + consolidation). Whole solution builds 0/0; 15 tests pass. - git/ plumbing core: GitRepository (git-CLI-backed via CommandRunner, faithful to upstream), GitRevision, GitRepoType, GitEnvironment, GitCredential, Refspec, FetchResult, MergeResult, IntegrateLabel, SameGitTree, exceptions. - http/ (System.Net.Http.HttpClient) incl. auth/multipart/json/endpoint. - remotefile/ + archive/ (zip/tar/gzip; xz/bz2 TODO) + hashing/. - format/ (buildifier) + buildozer/ (shell out via CommandRunner). - toml/ (hand-rolled parser) + json/ (System.Text.Json) + xml/ (System.Xml) + html/ + re2/ (System.Text.RegularExpressions). - credentials/ (issuer/secret model) + minimal checks/ stub. Consolidation fixes: qualified Copybara.Util.Command vs Buildozer.Command, dropped meaningless CheckNotNull on a non-nullable struct in TtlSecret. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- TODO.md | 27 +- src/Copybara.Core/Archive/ArchiveModule.cs | 167 + src/Copybara.Core/Archive/ArchiveUtil.cs | 112 + src/Copybara.Core/Buildozer/BuildozerBatch.cs | 140 + .../Buildozer/BuildozerCreate.cs | 166 + .../Buildozer/BuildozerDelete.cs | 84 + .../Buildozer/BuildozerModify.cs | 92 + .../Buildozer/BuildozerModule.cs | 356 +++ .../Buildozer/BuildozerOptions.cs | 203 ++ .../Buildozer/BuildozerPrintExecutor.cs | 61 + .../Buildozer/BuildozerTransformation.cs | 36 + src/Copybara.Core/Buildozer/Command.cs | 196 ++ src/Copybara.Core/Buildozer/Target.cs | 69 + .../Buildozer/TargetNotFoundException.cs | 26 + src/Copybara.Core/Checks/Checker.cs | 38 + src/Copybara.Core/Checks/CheckerException.cs | 35 + .../Credentials/ConstantCredentialIssuer.cs | 50 + src/Copybara.Core/Credentials/Credential.cs | 31 + .../Credentials/CredentialIssuer.cs | 33 + .../Credentials/CredentialIssuingException.cs | 33 + .../Credentials/CredentialModule.cs | 96 + .../Credentials/CredentialOptions.cs | 31 + .../CredentialRetrievalException.cs | 33 + .../Credentials/OpenCredential.cs | 38 + src/Copybara.Core/Credentials/StaticSecret.cs | 40 + .../Credentials/TomlKeySource.cs | 241 ++ src/Copybara.Core/Credentials/TtlSecret.cs | 54 + .../Credentials/UsernamePasswordIssuer.cs | 38 + src/Copybara.Core/Format/BuildifierFormat.cs | 154 + src/Copybara.Core/Format/BuildifierOptions.cs | 30 + src/Copybara.Core/Format/FormatModule.cs | 138 + .../Git/CannotIntegrateException.cs | 36 + src/Copybara.Core/Git/FetchResult.cs | 94 + src/Copybara.Core/Git/GitCredential.cs | 152 + src/Copybara.Core/Git/GitEnvironment.cs | 100 + src/Copybara.Core/Git/GitRepoType.cs | 151 + src/Copybara.Core/Git/GitRepository.cs | 2686 +++++++++++++++++ src/Copybara.Core/Git/GitRevision.cs | 401 +++ src/Copybara.Core/Git/IntegrateLabel.cs | 63 + .../Git/InvalidRefspecException.cs | 31 + src/Copybara.Core/Git/MergeResult.cs | 66 + .../Git/NonFastForwardRepositoryException.cs | 32 + .../Git/RebaseConflictException.cs | 31 + src/Copybara.Core/Git/Refspec.cs | 152 + src/Copybara.Core/Git/SameGitTree.cs | 82 + src/Copybara.Core/Hashing/HashingModule.cs | 93 + src/Copybara.Core/Html/HtmlElement.cs | 39 + src/Copybara.Core/Html/HtmlModule.cs | 194 ++ .../Http/Auth/AuthInterceptor.cs | 39 + .../Http/Auth/BearerInterceptor.cs | 43 + .../Http/Auth/MultimapConversions.cs | 40 + .../Http/Auth/UsernamePasswordInterceptor.cs | 48 + .../Http/Endpoint/HttpContentInterceptor.cs | 47 + .../Http/Endpoint/HttpEndpoint.cs | 241 ++ .../Http/Endpoint/HttpEndpointBody.cs | 35 + .../Http/Endpoint/HttpEndpointRequest.cs | 88 + .../Http/Endpoint/HttpEndpointResponse.cs | 73 + .../Http/Endpoint/HttpSecretInterceptor.cs | 58 + src/Copybara.Core/Http/HttpModule.cs | 320 ++ src/Copybara.Core/Http/HttpOptions.cs | 41 + src/Copybara.Core/Http/HttpTrigger.cs | 35 + .../Http/Json/HttpEndpointJsonContent.cs | 47 + src/Copybara.Core/Http/Json/StarlarkJson.cs | 87 + src/Copybara.Core/Http/Multipart/FilePart.cs | 53 + .../Http/Multipart/HttpEndpointFormPart.cs | 49 + .../HttpEndpointMultipartFormContent.cs | 59 + .../HttpEndpointUrlEncodedFormContent.cs | 54 + src/Copybara.Core/Http/Multipart/TextPart.cs | 55 + src/Copybara.Core/Json/GsonParserUtil.cs | 142 + src/Copybara.Core/Re2/Re2Module.cs | 51 + src/Copybara.Core/Re2/StarlarkMatcher.cs | 168 ++ src/Copybara.Core/Re2/StarlarkPattern.cs | 61 + .../RemoteFile/Extract/ExtractType.cs | 28 + .../RemoteFile/Extract/ExtractUtil.cs | 135 + .../RemoteFile/GclientHttpStreamFactory.cs | 82 + src/Copybara.Core/RemoteFile/GithubArchive.cs | 88 + .../RemoteFile/IHttpStreamFactory.cs | 42 + .../IRemoteArchiveVersionSelector.cs | 30 + .../RemoteFile/RemoteArchiveOrigin.cs | 368 +++ .../RemoteFile/RemoteArchiveRevision.cs | 55 + .../RemoteFile/RemoteArchiveVersion.cs | 34 + .../RemoteFile/RemoteFileModule.cs | 183 ++ .../RemoteFile/RemoteFileOptions.cs | 39 + .../RemoteFile/RemoteFileType.cs | 46 + .../RemoteFile/RemoteHttpFile.cs | 107 + src/Copybara.Core/Toml/TomlContent.cs | 146 + src/Copybara.Core/Toml/TomlModule.cs | 46 + src/Copybara.Core/Toml/TomlParser.cs | 755 +++++ src/Copybara.Core/Xml/XmlModule.cs | 92 + 89 files changed, 11449 insertions(+), 12 deletions(-) create mode 100644 src/Copybara.Core/Archive/ArchiveModule.cs create mode 100644 src/Copybara.Core/Archive/ArchiveUtil.cs create mode 100644 src/Copybara.Core/Buildozer/BuildozerBatch.cs create mode 100644 src/Copybara.Core/Buildozer/BuildozerCreate.cs create mode 100644 src/Copybara.Core/Buildozer/BuildozerDelete.cs create mode 100644 src/Copybara.Core/Buildozer/BuildozerModify.cs create mode 100644 src/Copybara.Core/Buildozer/BuildozerModule.cs create mode 100644 src/Copybara.Core/Buildozer/BuildozerOptions.cs create mode 100644 src/Copybara.Core/Buildozer/BuildozerPrintExecutor.cs create mode 100644 src/Copybara.Core/Buildozer/BuildozerTransformation.cs create mode 100644 src/Copybara.Core/Buildozer/Command.cs create mode 100644 src/Copybara.Core/Buildozer/Target.cs create mode 100644 src/Copybara.Core/Buildozer/TargetNotFoundException.cs create mode 100644 src/Copybara.Core/Checks/Checker.cs create mode 100644 src/Copybara.Core/Checks/CheckerException.cs create mode 100644 src/Copybara.Core/Credentials/ConstantCredentialIssuer.cs create mode 100644 src/Copybara.Core/Credentials/Credential.cs create mode 100644 src/Copybara.Core/Credentials/CredentialIssuer.cs create mode 100644 src/Copybara.Core/Credentials/CredentialIssuingException.cs create mode 100644 src/Copybara.Core/Credentials/CredentialModule.cs create mode 100644 src/Copybara.Core/Credentials/CredentialOptions.cs create mode 100644 src/Copybara.Core/Credentials/CredentialRetrievalException.cs create mode 100644 src/Copybara.Core/Credentials/OpenCredential.cs create mode 100644 src/Copybara.Core/Credentials/StaticSecret.cs create mode 100644 src/Copybara.Core/Credentials/TomlKeySource.cs create mode 100644 src/Copybara.Core/Credentials/TtlSecret.cs create mode 100644 src/Copybara.Core/Credentials/UsernamePasswordIssuer.cs create mode 100644 src/Copybara.Core/Format/BuildifierFormat.cs create mode 100644 src/Copybara.Core/Format/BuildifierOptions.cs create mode 100644 src/Copybara.Core/Format/FormatModule.cs create mode 100644 src/Copybara.Core/Git/CannotIntegrateException.cs create mode 100644 src/Copybara.Core/Git/FetchResult.cs create mode 100644 src/Copybara.Core/Git/GitCredential.cs create mode 100644 src/Copybara.Core/Git/GitEnvironment.cs create mode 100644 src/Copybara.Core/Git/GitRepoType.cs create mode 100644 src/Copybara.Core/Git/GitRepository.cs create mode 100644 src/Copybara.Core/Git/GitRevision.cs create mode 100644 src/Copybara.Core/Git/IntegrateLabel.cs create mode 100644 src/Copybara.Core/Git/InvalidRefspecException.cs create mode 100644 src/Copybara.Core/Git/MergeResult.cs create mode 100644 src/Copybara.Core/Git/NonFastForwardRepositoryException.cs create mode 100644 src/Copybara.Core/Git/RebaseConflictException.cs create mode 100644 src/Copybara.Core/Git/Refspec.cs create mode 100644 src/Copybara.Core/Git/SameGitTree.cs create mode 100644 src/Copybara.Core/Hashing/HashingModule.cs create mode 100644 src/Copybara.Core/Html/HtmlElement.cs create mode 100644 src/Copybara.Core/Html/HtmlModule.cs create mode 100644 src/Copybara.Core/Http/Auth/AuthInterceptor.cs create mode 100644 src/Copybara.Core/Http/Auth/BearerInterceptor.cs create mode 100644 src/Copybara.Core/Http/Auth/MultimapConversions.cs create mode 100644 src/Copybara.Core/Http/Auth/UsernamePasswordInterceptor.cs create mode 100644 src/Copybara.Core/Http/Endpoint/HttpContentInterceptor.cs create mode 100644 src/Copybara.Core/Http/Endpoint/HttpEndpoint.cs create mode 100644 src/Copybara.Core/Http/Endpoint/HttpEndpointBody.cs create mode 100644 src/Copybara.Core/Http/Endpoint/HttpEndpointRequest.cs create mode 100644 src/Copybara.Core/Http/Endpoint/HttpEndpointResponse.cs create mode 100644 src/Copybara.Core/Http/Endpoint/HttpSecretInterceptor.cs create mode 100644 src/Copybara.Core/Http/HttpModule.cs create mode 100644 src/Copybara.Core/Http/HttpOptions.cs create mode 100644 src/Copybara.Core/Http/HttpTrigger.cs create mode 100644 src/Copybara.Core/Http/Json/HttpEndpointJsonContent.cs create mode 100644 src/Copybara.Core/Http/Json/StarlarkJson.cs create mode 100644 src/Copybara.Core/Http/Multipart/FilePart.cs create mode 100644 src/Copybara.Core/Http/Multipart/HttpEndpointFormPart.cs create mode 100644 src/Copybara.Core/Http/Multipart/HttpEndpointMultipartFormContent.cs create mode 100644 src/Copybara.Core/Http/Multipart/HttpEndpointUrlEncodedFormContent.cs create mode 100644 src/Copybara.Core/Http/Multipart/TextPart.cs create mode 100644 src/Copybara.Core/Json/GsonParserUtil.cs create mode 100644 src/Copybara.Core/Re2/Re2Module.cs create mode 100644 src/Copybara.Core/Re2/StarlarkMatcher.cs create mode 100644 src/Copybara.Core/Re2/StarlarkPattern.cs create mode 100644 src/Copybara.Core/RemoteFile/Extract/ExtractType.cs create mode 100644 src/Copybara.Core/RemoteFile/Extract/ExtractUtil.cs create mode 100644 src/Copybara.Core/RemoteFile/GclientHttpStreamFactory.cs create mode 100644 src/Copybara.Core/RemoteFile/GithubArchive.cs create mode 100644 src/Copybara.Core/RemoteFile/IHttpStreamFactory.cs create mode 100644 src/Copybara.Core/RemoteFile/IRemoteArchiveVersionSelector.cs create mode 100644 src/Copybara.Core/RemoteFile/RemoteArchiveOrigin.cs create mode 100644 src/Copybara.Core/RemoteFile/RemoteArchiveRevision.cs create mode 100644 src/Copybara.Core/RemoteFile/RemoteArchiveVersion.cs create mode 100644 src/Copybara.Core/RemoteFile/RemoteFileModule.cs create mode 100644 src/Copybara.Core/RemoteFile/RemoteFileOptions.cs create mode 100644 src/Copybara.Core/RemoteFile/RemoteFileType.cs create mode 100644 src/Copybara.Core/RemoteFile/RemoteHttpFile.cs create mode 100644 src/Copybara.Core/Toml/TomlContent.cs create mode 100644 src/Copybara.Core/Toml/TomlModule.cs create mode 100644 src/Copybara.Core/Toml/TomlParser.cs create mode 100644 src/Copybara.Core/Xml/XmlModule.cs diff --git a/TODO.md b/TODO.md index a5c3ae280..92bfee3a6 100644 --- a/TODO.md +++ b/TODO.md @@ -99,29 +99,32 @@ builtins), `lib` (json, proto, etc.), `spelling`. `TodoReplace`, `SkylarkTransformation`, `transform/metadata/*`, `transform/debug/*`. - ⬜ `transform/patch/*` (needs DiffUtil/patch tooling). -## Phase 5 — Git support (`git/`) — uses LibGit2Sharp +## Phase 5 — Git support (`git/`) — uses LibGit2Sharp / git CLI -Largest single module (175 files). Port in slices: +Largest single module (~175 files). Port in slices: -- ⬜ Core plumbing: `GitRepository`, `GitRevision`, `GitReference`, - `GitEnvironment`, `GitCredential`, `RefspecConverter`, `GitOptions`. - Prefer LibGit2Sharp; keep a `git` CLI runner for gaps. +- ✅ Core plumbing: `GitRepository` (git-CLI-backed via `CommandRunner`, faithful + to upstream which shells out), `GitRevision`, `GitRepoType`, `GitEnvironment`, + `GitCredential`, `Refspec`, `FetchResult`, `MergeResult`, `IntegrateLabel`, + `SameGitTree`, exceptions. - ⬜ Origin/Destination: `GitOrigin`, `GitDestination`, `GitModule`, - `GitMirror`, `ChangeReader`, writer hooks. + `GitMirror`, `ChangeReader`, `GitVisitorUtil`, options, writer hooks. - ⬜ GitHub: `github/api` client (System.Text.Json + HttpClient), `GitHubOrigin`, `GitHubPrDestination`, `GitHubPrOrigin`, `github/util`. - ⬜ Gerrit: `gerritapi` client, `GerritOrigin`, `GerritDestination`. - ⬜ GitLab: `gitlab/api`, origin/destination. -- ⬜ `version/` resolvers. +- ✅ `version/` resolvers. ## Phase 6 — Other origins/destinations & modules - ✅ `folder/` (FolderOrigin/FolderDestination/FolderModule) — first ported origin/destination pair. -- ⬜ `remotefile/`, `archive/`, `hashing/`, `http/`, `format/` (buildifier), - `hg/` (Mercurial), `go/`, `rust/`, `python/`, `tsjs/`, `toml/`, `json/`, - `xml/`, `html/`, `re2/`, `buildozer/`, `checks/`, `approval/`, `feedback/`, - `action/`, `credentials/`, `treestate/`, `monitor/`, `regenerate/`, - `onboard/`, `configgen/`, `doc/` (reference doc generator). +- ✅ `remotefile/`, `archive/` (zip/tar/gzip; xz/bz2 = TODO), `hashing/`, + `http/` (HttpClient), `format/` (buildifier), `buildozer/`, `toml/`, `json/`, + `xml/`, `html/`, `re2/`, `credentials/`, `approval/`, `action/`, `treestate/`, + `monitor/`, `effect/`, `checks/` (minimal stub). +- ⬜ `hg/` (Mercurial), `go/`, `rust/`, `python/`, `tsjs/`, `feedback/`, + `checks/` (full), `regenerate/`, `onboard/`, `configgen/`, `doc/` (reference + doc generator), `transform/patch/`. ## Phase 7 — CLI (`src/Copybara.Cli`) diff --git a/src/Copybara.Core/Archive/ArchiveModule.cs b/src/Copybara.Core/Archive/ArchiveModule.cs new file mode 100644 index 000000000..532849908 --- /dev/null +++ b/src/Copybara.Core/Archive/ArchiveModule.cs @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Config; +using Copybara.Exceptions; +using Copybara.RemoteFile.Extract; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Archive; + +/// A module for handling archives in Starlark. +[StarlarkBuiltin("archive", Doc = "Functions to work with archives.")] +public class ArchiveModule : IStarlarkValue +{ + public ArchiveModule() + { + } + + [StarlarkMethod( + "create", + Doc = "Creates an archive, possibly compressed, from a list of files.")] + public void CreateArchive( + [Param(Name = "archive", Doc = "Expected path of the generated archive file.", Named = true)] + CheckoutPath archivePath, + [Param( + Name = "files", + Doc = + "An optional glob to describe the list of file paths that are to be included in the" + + " archive. If not specified, all files under the current working directory" + + " will be included. Note, the original file path in the filesystem will be" + + " preserved when archiving it.", + Named = true, + DefaultValue = "None", + AllowedTypes = new[] { typeof(Glob), typeof(NoneType) })] + object files) + { + ExtractType type = ResolveArchiveType(archivePath); + try + { + using Stream os = File.Create(archivePath.FullPath()); + ArchiveUtil.CreateArchive( + os, type, archivePath, SkylarkUtil.ConvertFromNoneable(files, null)); + } + catch (Exception e) when (e is IOException or ValidationException) + { + throw StarlarkRt.Errorf("There was an error creating the archive: {0}", e.ToString()); + } + } + + [StarlarkMethod( + "extract", + Doc = "Extract the contents of the archive to a path.")] + public void Extract( + [Param(Name = "archive", Named = true, Doc = "The path to the archive file.")] + CheckoutPath archivePath, + [Param( + Name = "type", + Named = true, + Doc = + "The archive type. Supported types: AUTO, JAR, ZIP, TAR, TAR_GZ, TAR_XZ, and" + + " TAR_BZ2. AUTO will try to infer the archive type automatically.", + DefaultValue = "\"AUTO\"")] + string typeStr, + [Param( + Name = "destination_folder", + Named = true, + Doc = + "The path to extract the archive to. This defaults to the directory where the" + + " archive is located.", + DefaultValue = "None", + AllowedTypes = new[] { typeof(CheckoutPath), typeof(NoneType) })] + object maybeDestination, + [Param( + Name = "paths", + Named = true, + Doc = "An optional glob that is used to filter the files extracted from the archive.", + DefaultValue = "None", + AllowedTypes = new[] { typeof(Glob), typeof(NoneType) })] + object paths) + { + ExtractType type = typeStr.Equals("AUTO") + ? ResolveArchiveType(archivePath) + : SkylarkUtil.StringToEnum("type", typeStr); + + CheckoutPath destination = + SkylarkUtil.ConvertFromNoneable(maybeDestination, archivePath.Resolve(".."))!; + + try + { + using Stream contents = File.OpenRead(archivePath.FullPath()); + ExtractUtil.ExtractArchive( + contents, + destination.FullPath(), + type, + SkylarkUtil.ConvertFromNoneable(paths, null)); + } + catch (Exception e) when (e is IOException or ValidationException) + { + throw StarlarkRt.Errorf("There was an error extracting the archive: {0}", e.ToString()); + } + } + + private static ExtractType ResolveArchiveType(CheckoutPath archivePath) + { + string filename = PathOps.GetFileName(archivePath.GetPath()); + string extension = GetFileExtension(filename); + switch (extension) + { + case "zip": + return ExtractType.ZIP; + case "jar": + return ExtractType.JAR; + case "tar": + return ExtractType.TAR; + case "tgz": + return ExtractType.TAR_GZ; + case "gz": + if (filename.EndsWith(".tar.gz")) + { + return ExtractType.TAR_GZ; + } + goto default; + case "xz": + if (filename.EndsWith(".tar.xz")) + { + return ExtractType.TAR_XZ; + } + goto default; + case "bz2": + if (filename.EndsWith(".tar.bz2")) + { + return ExtractType.TAR_BZ2; + } + goto default; + default: + throw StarlarkRt.Errorf( + "The archive type couldn't be inferred for the file: {0}", + archivePath.GetPath()); + } + } + + /// + /// Mirrors Guava's Files.getFileExtension: returns the substring after the last '.' in + /// the file name, or the empty string if there is no dot. + /// + private static string GetFileExtension(string fileName) + { + int dotIndex = fileName.LastIndexOf('.'); + return dotIndex == -1 ? "" : fileName[(dotIndex + 1)..]; + } +} diff --git a/src/Copybara.Core/Archive/ArchiveUtil.cs b/src/Copybara.Core/Archive/ArchiveUtil.cs new file mode 100644 index 000000000..5557e382b --- /dev/null +++ b/src/Copybara.Core/Archive/ArchiveUtil.cs @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Formats.Tar; +using System.IO.Compression; +using Copybara.Exceptions; +using Copybara.RemoteFile.Extract; +using Copybara.Util; + +namespace Copybara.Archive; + +/// +/// A utility class to generate a (compressed) archive at a target directory path. Accepts a +/// to filter out which files need to be archived. +/// +/// Upstream uses commons-compress. This port uses the .NET in-box archive writers: +/// for zip/jar, for tar (with +/// for tar.gz). XZ is not supported by the BCL and is left as TODO(port). +/// +public static class ArchiveUtil +{ + /// Internal utility to create an archive. + /// generic configured to write to the target archive file. + /// category of archive to generate based on target file extension. + /// copybara checkout path to the archive file. + /// glob to filter the set of files to be included in the archive. + /// + /// + public static void CreateArchive( + Stream os, ExtractType type, CheckoutPath archivePath, Glob? fileFilter) + { + switch (type) + { + case ExtractType.JAR: + case ExtractType.ZIP: + using (var zip = new ZipArchive(os, ZipArchiveMode.Create, leaveOpen: true)) + { + WriteFiles(archivePath, fileFilter, (relativePath, filePath) => + { + var entry = zip.CreateEntry(relativePath); + using var entryStream = entry.Open(); + using var source = File.OpenRead(filePath); + source.CopyTo(entryStream); + }); + } + break; + case ExtractType.TAR: + using (var tar = new TarWriter(os, leaveOpen: true)) + { + WriteFiles(archivePath, fileFilter, (relativePath, filePath) => + tar.WriteEntry(filePath, relativePath)); + } + break; + case ExtractType.TAR_GZ: + using (var gz = new GZipStream(os, CompressionMode.Compress, leaveOpen: true)) + using (var tar = new TarWriter(gz, leaveOpen: true)) + { + WriteFiles(archivePath, fileFilter, (relativePath, filePath) => + tar.WriteEntry(filePath, relativePath)); + } + break; + case ExtractType.TAR_XZ: + // TODO(port): XZ compression is not available in the .NET BCL. A third-party + // package (e.g. SharpCompress or XZ.NET) would be required to support TAR_XZ. + throw new ValidationException( + "TAR_XZ archives are not yet supported in the .NET port (no in-box XZ codec)."); + default: + throw new ValidationException( + $"Failed to get archive output stream for file type: {type}"); + } + } + + private static void WriteFiles( + CheckoutPath archivePath, Glob? fileFilter, Action writeEntry) + { + // Get the current working directory + string workdir = archivePath.GetCheckoutDir(); + + // Exclude the "archive file" itself from getting added to the archived bundle. + fileFilter ??= Glob.CreateGlob(ImmutableArray.Create("**")); + fileFilter = Glob.Difference( + fileFilter, Glob.CreateGlob(ImmutableArray.Create(archivePath.GetPath()))); + + var matcher = fileFilter.RelativeTo(workdir); + + foreach (string filePath in Directory.EnumerateFiles( + workdir, "*", SearchOption.AllDirectories)) + { + string full = PathOps.Normalize(Path.GetFullPath(filePath)); + if (!matcher.Matches(full)) + { + continue; + } + string relativePath = PathOps.Relativize(workdir, full); + writeEntry(relativePath, full); + } + } +} diff --git a/src/Copybara.Core/Buildozer/BuildozerBatch.cs b/src/Copybara.Core/Buildozer/BuildozerBatch.cs new file mode 100644 index 000000000..ba67328cf --- /dev/null +++ b/src/Copybara.Core/Buildozer/BuildozerBatch.cs @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Buildozer; +using Copybara.Common; +using BuildozerCommand = Copybara.Buildozer.BuildozerOptions.BuildozerCommand; + +namespace Copybara.Buildozer; + +/// A transformation that runs many buildozer transformation in batch. +public sealed class BuildozerBatch : IBuildozerTransformation +{ + private readonly BuildozerOptions _options; + private readonly WorkflowOptions _workflowOptions; + private readonly ImmutableArray _transformations; + + private BuildozerBatch( + BuildozerOptions options, + WorkflowOptions workflowOptions, + IEnumerable transformations) + { + _options = Preconditions.CheckNotNull(options); + _workflowOptions = Preconditions.CheckNotNull(workflowOptions); + _transformations = transformations.ToImmutableArray(); + } + + public TransformationStatus Transform(TransformWork work) + { + var commands = new List(); + foreach (IBuildozerTransformation transformation in _transformations) + { + transformation.BeforeRun(work); + commands.AddRange(transformation.GetCommands()); + } + try + { + _options.Run(work.GetConsole(), work.GetCheckoutDir(), commands); + return TransformationStatus.Success(); + } + catch (TargetNotFoundException e) + { + return TransformationStatus.Noop(e.Message); + } + } + + public ITransformation Reverse() => + throw new InvalidOperationException( + "Reverse should never be called for join transformations"); + + public string Describe() => + "buildozer batch of " + _transformations.Length + " buildozer transformations"; + + public bool CanJoin(ITransformation transformation) => IsBuildozer(transformation); + + internal static bool IsBuildozer(ITransformation transformation) => + transformation is IBuildozerTransformation; + + public ITransformation Join(ITransformation next) => + Join(_options, _workflowOptions, this, (IBuildozerTransformation)next); + + internal static BuildozerBatch Join( + BuildozerOptions buildozerOptions, + WorkflowOptions workflowOptions, + IBuildozerTransformation current, + IBuildozerTransformation next) + { + var transformationBuilder = ImmutableArray.CreateBuilder(); + if (current is BuildozerBatch currentBatch) + { + transformationBuilder.AddRange(currentBatch._transformations); + } + else + { + transformationBuilder.Add(current); + } + if (next is BuildozerBatch nextBatch) + { + transformationBuilder.AddRange(nextBatch._transformations); + } + else + { + transformationBuilder.Add(next); + } + return new BuildozerBatch( + buildozerOptions, workflowOptions, transformationBuilder.ToImmutable()); + } + + internal static BuildozerBatch JoinAll( + BuildozerOptions buildozerOptions, + WorkflowOptions workflowOptions, + IEnumerable transformations) + { + var transformationBuilder = ImmutableArray.CreateBuilder(); + foreach (IBuildozerTransformation transformation in transformations) + { + if (transformation is BuildozerBatch batch) + { + transformationBuilder.AddRange(batch._transformations); + } + else + { + transformationBuilder.Add(transformation); + } + } + return new BuildozerBatch( + buildozerOptions, workflowOptions, transformationBuilder.ToImmutable()); + } + + public void BeforeRun(TransformWork work) + { + foreach (IBuildozerTransformation transformation in _transformations) + { + transformation.BeforeRun(work); + } + } + + public IEnumerable GetCommands() + { + var result = new List(); + foreach (IBuildozerTransformation transformation in _transformations) + { + result.AddRange(transformation.GetCommands()); + } + return result; + } +} diff --git a/src/Copybara.Core/Buildozer/BuildozerCreate.cs b/src/Copybara.Core/Buildozer/BuildozerCreate.cs new file mode 100644 index 000000000..0b62d8100 --- /dev/null +++ b/src/Copybara.Core/Buildozer/BuildozerCreate.cs @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Buildozer; +using Copybara.Common; +using Copybara.Exceptions; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; +using BuildozerCommand = Copybara.Buildozer.BuildozerOptions.BuildozerCommand; + +namespace Copybara.Buildozer; + +/// +/// A transformation which creates a new build target and reverses to delete the same target. +/// +public sealed class BuildozerCreate : IBuildozerTransformation +{ + private readonly BuildozerOptions _options; + private readonly WorkflowOptions _workflowOptions; + private readonly Target _target; + private readonly string _ruleType; + private readonly RelativeTo _relativeTo; + private readonly ImmutableArray _commands; + + internal sealed class RelativeTo + { + internal readonly string Args; + + private static void ValidateTargetName(string targetName) + { + if (targetName.Contains(':')) + { + throw StarlarkRt.Errorf( + "unexpected : in target name (did you include the package by mistake?) - '{0}'", + targetName); + } + } + + internal RelativeTo(string before, string after) + { + if (before.Length != 0 && after.Length != 0) + { + throw new EvalException( + "cannot specify both 'before' and 'after' in the target create arguments"); + } + + if (before.Length != 0) + { + ValidateTargetName(before); + Args = "before " + before; + } + else if (after.Length != 0) + { + ValidateTargetName(after); + Args = "after " + after; + } + else + { + Args = ""; + } + } + + public override string ToString() => Args; + } + + internal BuildozerCreate( + BuildozerOptions options, + WorkflowOptions workflowOptions, + Target target, + string ruleType, + RelativeTo relativeTo, + IEnumerable commands) + { + _options = Preconditions.CheckNotNull(options); + _workflowOptions = Preconditions.CheckNotNull(workflowOptions); + _target = Preconditions.CheckNotNull(target); + _ruleType = Preconditions.CheckNotNull(ruleType); + _relativeTo = Preconditions.CheckNotNull(relativeTo); + _commands = commands.ToImmutableArray(); + } + + public TransformationStatus Transform(TransformWork work) + { + BeforeRun(work); + try + { + _options.Run(work.GetConsole(), work.GetCheckoutDir(), GetCommands()); + return TransformationStatus.Success(); + } + catch (TargetNotFoundException e) + { + // This should not happen for creation. If it happens, it is due to a file error. + throw new ValidationException(e.Message); + } + } + + public void BeforeRun(TransformWork work) + { + string buildFilePath = Path.Combine(work.GetCheckoutDir(), GetTargetBuildFile()); + if (!File.Exists(buildFilePath)) + { + // Alert the user that the package to contain this target doesn't have a BUILD file, since + // this may be a configuration error. + work.GetConsole().Info( + $"BUILD file to contain {_target} doesn't exist. Creating now."); + string? parent = Path.GetDirectoryName(buildFilePath); + if (parent != null) + { + Directory.CreateDirectory(parent); + } + File.WriteAllBytes(buildFilePath, Array.Empty()); + } + } + + private string GetTargetBuildFile() + { + string pkg = _target.GetPackage(); + // pkg can be empty (e.g. ":foo"), which should create targets in the workdir root, i.e. + // ./BUILD + return pkg + (pkg.Length == 0 ? "." : "") + "/BUILD"; + } + + public IEnumerable GetCommands() + { + var result = new List + { + new( + ImmutableArray.Create(GetTargetBuildFile()), + $"new {_ruleType} {_target.GetName()} {_relativeTo.Args}"), + }; + foreach (string command in _commands) + { + result.Add(new BuildozerCommand(ImmutableArray.Create(_target.ToString()), command)); + } + return result; + } + + public bool CanJoin(ITransformation transformation) => + BuildozerBatch.IsBuildozer(transformation); + + public ITransformation Join(ITransformation next) => + BuildozerBatch.Join(_options, _workflowOptions, this, (IBuildozerTransformation)next); + + public string Describe() => "buildozer.create " + _target; + + public ITransformation Reverse() => + new BuildozerDelete(_options, _workflowOptions, _target, this); + + public override string ToString() => + $"BuildozerCreate{{target={_target}, ruleType={_ruleType}, relativeTo={_relativeTo}," + + $" commands=[{string.Join(", ", _commands)}]}}"; +} diff --git a/src/Copybara.Core/Buildozer/BuildozerDelete.cs b/src/Copybara.Core/Buildozer/BuildozerDelete.cs new file mode 100644 index 000000000..f5c797ef4 --- /dev/null +++ b/src/Copybara.Core/Buildozer/BuildozerDelete.cs @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Buildozer; +using Copybara.Common; +using Copybara.Exceptions; +using BuildozerCommand = Copybara.Buildozer.BuildozerOptions.BuildozerCommand; + +namespace Copybara.Buildozer; + +/// +/// A transformation which deletes build target and reverses to create the same target. +/// +public sealed class BuildozerDelete : IBuildozerTransformation +{ + private readonly BuildozerOptions _options; + private readonly WorkflowOptions _workflowOptions; + private readonly Target _target; + private readonly BuildozerCreate? _recreateAs; + + internal BuildozerDelete( + BuildozerOptions options, + WorkflowOptions workflowOptions, + Target target, + BuildozerCreate? recreateAs) + { + _options = Preconditions.CheckNotNull(options); + _workflowOptions = Preconditions.CheckNotNull(workflowOptions); + _target = Preconditions.CheckNotNull(target); + _recreateAs = recreateAs; + } + + public TransformationStatus Transform(TransformWork work) + { + try + { + _options.Run(work.GetConsole(), work.GetCheckoutDir(), GetCommands()); + return TransformationStatus.Success(); + } + catch (TargetNotFoundException e) + { + return TransformationStatus.Noop(e.Message); + } + } + + public string Describe() => "buildozer.delete " + _target; + + public bool CanJoin(ITransformation transformation) => + BuildozerBatch.IsBuildozer(transformation); + + public ITransformation Join(ITransformation next) => + BuildozerBatch.Join(_options, _workflowOptions, this, (IBuildozerTransformation)next); + + public IEnumerable GetCommands() => + ImmutableArray.Create(new BuildozerCommand(_target.ToString(), "delete")); + + public ITransformation Reverse() + { + if (_recreateAs == null) + { + throw new NonReversibleValidationException( + "This buildozer.delete is not reversible. Please specify at least rule_type to make" + + " it reversible."); + } + return _recreateAs; + } + + public override string ToString() => + $"BuildozerDelete{{target={_target}, recreateAs={_recreateAs}}}"; +} diff --git a/src/Copybara.Core/Buildozer/BuildozerModify.cs b/src/Copybara.Core/Buildozer/BuildozerModify.cs new file mode 100644 index 000000000..96ed9b7b9 --- /dev/null +++ b/src/Copybara.Core/Buildozer/BuildozerModify.cs @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Buildozer; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util.Console; +using BuildozerCommand = Copybara.Buildozer.BuildozerOptions.BuildozerCommand; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Buildozer; + +/// A transformation which runs one or more commands against a single target. +public sealed class BuildozerModify : IBuildozerTransformation +{ + private readonly BuildozerOptions _options; + private readonly WorkflowOptions _workflowOptions; + private readonly IReadOnlyList _targets; + private readonly ImmutableArray _commands; + + internal BuildozerModify( + BuildozerOptions options, + WorkflowOptions workflowOptions, + IReadOnlyList targets, + IEnumerable commands) + { + _options = Preconditions.CheckNotNull(options); + _workflowOptions = Preconditions.CheckNotNull(workflowOptions); + _targets = Preconditions.CheckNotNull(targets); + _commands = commands.ToImmutableArray(); + } + + public TransformationStatus Transform(TransformWork work) + { + Console console = work.GetConsole(); + try + { + _options.Run(console, work.GetCheckoutDir(), GetCommands()); + return TransformationStatus.Success(); + } + catch (TargetNotFoundException e) + { + return TransformationStatus.Noop(e.Message); + } + } + + public ITransformation Reverse() + { + var reverseCommands = new List(); + for (int i = _commands.Length - 1; i >= 0; i--) + { + reverseCommands.Add(_commands[i].Reverse()); + } + return new BuildozerModify(_options, _workflowOptions, _targets, reverseCommands); + } + + public IEnumerable GetCommands() + { + var result = new List(); + foreach (Command command in _commands) + { + result.Add(new BuildozerCommand(Target.AsStringList(_targets), command.ToString())); + } + return result; + } + + public bool CanJoin(ITransformation transformation) => + BuildozerBatch.IsBuildozer(transformation); + + public ITransformation Join(ITransformation next) => + BuildozerBatch.Join(_options, _workflowOptions, this, (IBuildozerTransformation)next); + + public string Describe() => "buildozer.modify [" + string.Join(", ", _targets) + "]"; + + public override string ToString() => + $"BuildozerModify{{target=[{string.Join(", ", _targets)}]," + + $" commands=[{string.Join(", ", _commands)}]}}"; +} diff --git a/src/Copybara.Core/Buildozer/BuildozerModule.cs b/src/Copybara.Core/Buildozer/BuildozerModule.cs new file mode 100644 index 000000000..00d82cb9c --- /dev/null +++ b/src/Copybara.Core/Buildozer/BuildozerModule.cs @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections; +using System.Collections.Immutable; +using Copybara.Buildozer; +using Copybara.Common; +using Copybara.Config; +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; +using Sequence = Starlark.Eval.Sequence; + +namespace Copybara.Buildozer; + +/// Skylark module for Buildozer-related functionality. +[StarlarkBuiltin( + "buildozer", + Doc = + "Module for Buildozer-related functionality such as creating and modifying BUILD targets.")] +public sealed class BuildozerModule : IStarlarkValue +{ + private readonly BuildozerOptions _buildozerOptions; + private readonly WorkflowOptions _workflowOptions; + private readonly BuildozerPrintExecutor _buildozerPrintExecutor; + + public BuildozerModule( + WorkflowOptions workflowOptions, + BuildozerOptions buildozerOptions, + GeneralOptions generalOptions) + { + _workflowOptions = Preconditions.CheckNotNull(workflowOptions); + _buildozerOptions = Preconditions.CheckNotNull(buildozerOptions); + _buildozerPrintExecutor = + BuildozerPrintExecutor.Create( + buildozerOptions, Preconditions.CheckNotNull(generalOptions).GetConsole()); + } + + private static ImmutableArray GetTargetList(object arg) + { + if (arg is string s) + { + return ImmutableArray.Create(Target.FromConfig(s)); + } + + var builder = ImmutableArray.CreateBuilder(); + foreach (string target in SkylarkUtil.ConvertStringList(arg, "target")) + { + builder.Add(Target.FromConfig(target)); + } + return builder.ToImmutable(); + } + + private static ImmutableArray CoerceCommandList(IEnumerable commands) + { + var wrappedCommands = ImmutableArray.CreateBuilder(); + foreach (object? command in commands) + { + if (command is string s) + { + wrappedCommands.Add(Command.FromConfig(s, null)); + } + else if (command is Command c) + { + wrappedCommands.Add(c); + } + else + { + throw StarlarkRt.Errorf( + "Expected a string or buildozer.cmd, but got: {0}", command); + } + } + return wrappedCommands.ToImmutable(); + } + + [StarlarkMethod( + "create", + Doc = + "A transformation which creates a new build target and populates its " + + "attributes. This transform can reverse automatically to delete the target.")] + public BuildozerCreate Create( + [Param( + Name = "target", + Doc = + "Target to create, including the package, e.g. 'foo:bar'. The package can be " + + "'.' for the root BUILD file.", + Named = true)] + string target, + [Param( + Name = "rule_type", + Doc = "Type of this rule, for instance, java_library.", + Named = true)] + string ruleType, + [Param( + Name = "commands", + Doc = + "Commands to populate attributes of the target after creating it. Elements can" + + " be strings such as 'add deps :foo' or objects returned by buildozer.cmd.", + DefaultValue = "[]", + AllowedTypes = new[] { typeof(Sequence) }, + Named = true)] + object commands, + [Param( + Name = "before", + Doc = + "When supplied, causes this target to be created *before* the target named by" + + " 'before'", + Positional = false, + DefaultValue = "''", + Named = true)] + string before, + [Param( + Name = "after", + Doc = + "When supplied, causes this target to be created *after* the target named by" + + " 'after'", + Positional = false, + DefaultValue = "''", + Named = true)] + string after) + { + var commandStrings = new List(); + foreach (Command command in CoerceCommandList((IEnumerable)commands)) + { + commandStrings.Add(command.ToString()); + } + return new BuildozerCreate( + _buildozerOptions, + _workflowOptions, + Target.FromConfig(target), + ruleType, + new BuildozerCreate.RelativeTo(before, after), + commandStrings); + } + + private static void MustOmitRecreateParam(object expected, object actual, string paramName) + { + if (!expected.Equals(actual)) + { + throw StarlarkRt.Errorf( + "Parameter '{0}' is only used for reversible buildozer.delete transforms, but this" + + " buildozer.delete is not reversible. Specify 'rule_type' argument to make it" + + " reversible.", + paramName); + } + } + + [StarlarkMethod( + "delete", + Doc = + "A transformation which is the opposite of creating a build target. When run normally," + + " it deletes a build target. When reversed, it creates and prepares one.")] + public BuildozerDelete Delete( + [Param( + Name = "target", + Doc = "Target to delete, including the package, e.g. 'foo:bar'", + Named = true)] + string targetString, + [Param( + Name = "rule_type", + Doc = + "Type of this rule, for instance, java_library. Supplying this will cause this" + + " transformation to be reversible.", + DefaultValue = "''", + Named = true)] + string ruleType, + [Param( + Name = "recreate_commands", + Doc = + "Commands to populate attributes of the target after creating it. Elements can" + + " be strings such as 'add deps :foo' or objects returned by buildozer.cmd.", + Positional = false, + DefaultValue = "[]", + AllowedTypes = new[] { typeof(Sequence) }, + Named = true)] + object recreateCommands, + [Param( + Name = "before", + Doc = + "When supplied with rule_type and the transformation is reversed, causes this" + + " target to be created *before* the target named by 'before'", + Positional = false, + DefaultValue = "''", + Named = true)] + string before, + [Param( + Name = "after", + Doc = + "When supplied with rule_type and the transformation is reversed, causes this" + + " target to be created *after* the target named by 'after'", + Positional = false, + DefaultValue = "''", + Named = true)] + string after) + { + var commandStrings = new List(); + foreach (Command command in CoerceCommandList((IEnumerable)recreateCommands)) + { + commandStrings.Add(command.ToString()); + } + BuildozerCreate? recreateAs; + Target target = Target.FromConfig(targetString); + if (ruleType.Length == 0) + { + recreateAs = null; + MustOmitRecreateParam( + (object)ImmutableArray.Empty, ToListForCompare(recreateCommands), + "recreate_commands"); + MustOmitRecreateParam("", before, "before"); + MustOmitRecreateParam("", after, "after"); + } + else + { + recreateAs = + new BuildozerCreate( + _buildozerOptions, + _workflowOptions, + target, + ruleType, + new BuildozerCreate.RelativeTo(before, after), + commandStrings); + } + return new BuildozerDelete(_buildozerOptions, _workflowOptions, target, recreateAs); + } + + // Emulates Java's check that the (empty) default list argument was left untouched. + private static object ToListForCompare(object recreateCommands) + { + int count = 0; + foreach (object? unused in (IEnumerable)recreateCommands) + { + count++; + } + return count == 0 ? (object)ImmutableArray.Empty : recreateCommands; + } + + [StarlarkMethod( + "modify", + Doc = + "A transformation which runs one or more Buildozer commands against a single" + + " target expression. See http://go/buildozer for details on supported commands and" + + " target expression formats.")] + public BuildozerModify Modify( + [Param( + Name = "target", + AllowedTypes = new[] { typeof(string), typeof(Sequence) }, + Doc = "Specifies the target(s) against which to apply the commands. Can be a list.", + Named = true)] + object target, + [Param( + Name = "commands", + Doc = + "Commands to apply to the target(s) specified. Elements can" + + " be strings such as 'add deps :foo' or objects returned by buildozer.cmd.", + AllowedTypes = new[] { typeof(Sequence) }, + Named = true)] + object commands) + { + var commandList = CoerceCommandList((IEnumerable)commands); + if (commandList.Length == 0) + { + throw StarlarkRt.Errorf("at least one element required in 'commands' argument"); + } + return new BuildozerModify( + _buildozerOptions, _workflowOptions, GetTargetList(target), commandList); + } + + [StarlarkMethod( + "cmd", + Doc = + "Creates a Buildozer command. You can specify the reversal with the 'reverse' " + + "argument.")] + public Command Cmd( + [Param( + Name = "forward", + Doc = "Specifies the Buildozer command, e.g. 'replace deps :foo :bar'", + Named = true)] + string forward, + [Param( + Name = "reverse", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Doc = + "The reverse of the command. This is only required if the given command cannot be" + + " reversed automatically and the reversal of this command is required by" + + " some workflow or Copybara check. The following commands are automatically" + + " reversible:
  • add
  • remove (when used to remove element" + + " from list i.e. 'remove srcs foo.cc'
  • replace
", + DefaultValue = "None", + Named = true)] + object? reverse) + { + return Command.FromConfig(forward, SkylarkUtil.ConvertOptionalString(reverse)); + } + + [StarlarkMethod( + "print", + Doc = + "Executes a buildozer print command and returns the output. This is designed to be used" + + " in the context of a transform")] + public string Print( + [Param( + Name = "ctx", + Doc = "The TransformWork object", + AllowedTypes = new[] { typeof(TransformWork) }, + Named = true)] + TransformWork ctx, + [Param(Name = "attr", Doc = "The attribute from the target rule to print.", Named = true)] + string attr, + [Param(Name = "target", Doc = "The target to print from.", Named = true)] + string target) + { + return _buildozerPrintExecutor.Run(ctx.GetCheckoutDir(), attr, target); + } + + [StarlarkMethod( + "batch", + Doc = "Combines a list of buildozer transforms into a single batch transformation.")] + public BuildozerBatch Batch( + [Param( + Name = "transforms", + Doc = "The list of buildozer transforms to combine.", + AllowedTypes = new[] { typeof(Sequence) }, + Named = true)] + object transforms) + { + var builder = ImmutableArray.CreateBuilder(); + foreach (object? transform in (IEnumerable)transforms) + { + if (transform is IBuildozerTransformation t) + { + builder.Add(t); + } + else + { + throw StarlarkRt.Errorf( + "Expected a buildozer transform, but got: {0}", + transform?.GetType().Name ?? "null"); + } + } + return BuildozerBatch.JoinAll(_buildozerOptions, _workflowOptions, builder.ToImmutable()); + } +} diff --git a/src/Copybara.Core/Buildozer/BuildozerOptions.cs b/src/Copybara.Core/Buildozer/BuildozerOptions.cs new file mode 100644 index 000000000..c27e67e2e --- /dev/null +++ b/src/Copybara.Core/Buildozer/BuildozerOptions.cs @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Format; +using Copybara.Util; +using Copybara.Util.Console; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Command = Copybara.Util.Command; +using CommandException = Copybara.Util.CommandException; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Buildozer; + +/// Specifies how Buildozer is executed. +public sealed class BuildozerOptions : IOption +{ + private static readonly ILogger Logger = NullLogger.Instance; + + private static readonly Regex TargetNotFound = new( + @".*error while executing commands \[.+\] on target (?.* not found)", + RegexOptions.Compiled); + + private readonly GeneralOptions _generalOptions; + private readonly BuildifierOptions _buildifierOptions; + private readonly WorkflowOptions _workflowOptions; + + public BuildozerOptions( + GeneralOptions generalOptions, + BuildifierOptions buildifierOptions, + WorkflowOptions workflowOptions) + { + _generalOptions = Preconditions.CheckNotNull(generalOptions); + _buildifierOptions = Preconditions.CheckNotNull(buildifierOptions); + _workflowOptions = workflowOptions; + } + + [Flag( + "--buildozer-bin", + "Binary to use for buildozer (Default is /usr/bin/buildozer)", + Hidden = true)] + public string BuildozerBin { get; set; } = "/usr/bin/buildozer"; + + private void LogError(Console console, CommandOutput output) + { + Consoles.ErrorLogLines(console, "buildozer stdout: ", output.GetStdout()); + Consoles.ErrorLogLines(console, "buildozer stderr: ", output.GetStderr()); + } + + public sealed class BuildozerCommand + { + private readonly IReadOnlyList _targets; + private readonly string _cmd; + + internal BuildozerCommand(IReadOnlyList targets, string cmd) + { + _targets = Preconditions.CheckNotNull(targets); + _cmd = Preconditions.CheckNotNull(cmd); + } + + internal BuildozerCommand(string targets, string cmd) + { + _targets = ImmutableArray.Create(Preconditions.CheckNotNull(targets)); + _cmd = Preconditions.CheckNotNull(cmd); + } + + public override string ToString() => _cmd + "|" + string.Join('|', _targets); + } + + /// + /// + internal void Run(Console console, string checkoutDir, IEnumerable commands) + { + string unused = RunCaptureOutput(console, checkoutDir, commands); + } + + /// Runs buildozer with the given commands. + /// + /// + internal string RunCaptureOutput( + Console console, string checkoutDir, IEnumerable commands) + { + var commandList = commands.ToList(); + var args = new List + { + BuildozerBin, + "-buildifier=" + _buildifierOptions.BuildifierBin, + }; + + // We only use -k in keep going mode because it shows less errors (http://b/69386431) + if (_workflowOptions.IgnoreNoop) + { + args.Add("-k"); + } + args.Add("-f"); + args.Add("-"); + try + { + var cmd = new Copybara.Util.Command(args.ToArray(), null, checkoutDir); + CommandOutputWithStatus output = _generalOptions.NewCommandRunner(cmd) + .WithVerbose(_generalOptions.IsVerbose()) + .WithInput(Encoding.UTF8.GetBytes(string.Join('\n', commandList))) + .Execute(); + if (output.GetStdout().Length != 0) + { + Logger.LogInformation("buildozer stdout: {Stdout}", output.GetStdout()); + } + if (output.GetStderr().Length != 0) + { + Logger.LogInformation("buildozer stderr: {Stderr}", output.GetStderr()); + } + return output.GetStdout(); + } + catch (BadExitStatusWithOutputException e) + { + // Don't print the output for common/known errors. + if (_generalOptions.IsVerbose()) + { + LogError(console, e.GetOutput()); + } + if (e.GetResult().TerminationStatus.GetExitCode() == 3) + { + // Buildozer exits with code == 3 when the build file was not modified and no output + // was generated. This happens with expressions that match multiple targets, like + // :%java_library + throw new TargetNotFoundException( + CommandsMessage("Buildozer could not find a target for", commandList)); + } + if (e.GetResult().TerminationStatus.GetExitCode() == 2) + { + var errors = e.GetOutput().GetStderr() + .Split('\n') + .Where(s => !(s.Length == 0 || s.StartsWith("fixed ", StringComparison.Ordinal))) + .ToList(); + var notFoundMsg = new List(); + bool allNotFound = true; + foreach (string error in errors) + { + Match matcher = TargetNotFound.Match(error); + if (matcher.Success) + { + notFoundMsg.Add( + $"Buildozer could not find a target for {matcher.Groups["error"].Value}"); + } + else if (error.Contains("no such file or directory") + || error.Contains("not a directory")) + { + notFoundMsg.Add("Buildozer could not find build file: " + error); + } + else + { + allNotFound = false; + } + } + if (allNotFound) + { + throw new TargetNotFoundException(string.Join('\n', notFoundMsg)); + } + } + // Otherwise we have already printed above. + if (!_generalOptions.IsVerbose()) + { + LogError(console, e.GetOutput()); + } + throw new ValidationException( + string.Format( + "{0}\nCommand stderr:{1}", + CommandsMessage("Failed to execute buildozer with args", commandList), + e.GetOutput().GetStderr()), + e); + } + catch (CommandException e) + { + string message = string.Format( + "Error '{0}' running buildozer command: {1}", + e.Message, + e.GetCommand()); + console.Error(message); + throw new ValidationException(message, e); + } + } + + private static string CommandsMessage(string prefix, IEnumerable commands) => + prefix + ":\n " + string.Join("\n ", commands); +} diff --git a/src/Copybara.Core/Buildozer/BuildozerPrintExecutor.cs b/src/Copybara.Core/Buildozer/BuildozerPrintExecutor.cs new file mode 100644 index 000000000..e2a8f9ebd --- /dev/null +++ b/src/Copybara.Core/Buildozer/BuildozerPrintExecutor.cs @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Buildozer; +using Copybara.Exceptions; +using BuildozerCommand = Copybara.Buildozer.BuildozerOptions.BuildozerCommand; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Buildozer; + +/// A class that can run a 'buildozer print' command. +public class BuildozerPrintExecutor +{ + private readonly BuildozerOptions _options; + private readonly Console _console; + + private BuildozerPrintExecutor(BuildozerOptions options, Console console) + { + _options = options; + _console = console; + } + + public static BuildozerPrintExecutor Create(BuildozerOptions options, Console console) => + new(options, console); + + /// + /// Runs a Buildozer print command. + /// + /// The checkout directory to run in. + /// The attribute from the target rule to print. + /// The target to print from. + /// A string with the buildozer print output. + /// If there is an issue running buildozer print. + public string Run(string checkoutDir, string attr, string target) + { + try + { + var command = new BuildozerCommand(target, $"print {attr}"); + return _options.RunCaptureOutput( + _console, checkoutDir, ImmutableArray.Create(command)); + } + catch (TargetNotFoundException e) + { + throw new ValidationException("Buildozer could not find the specified target", e); + } + } +} diff --git a/src/Copybara.Core/Buildozer/BuildozerTransformation.cs b/src/Copybara.Core/Buildozer/BuildozerTransformation.cs new file mode 100644 index 000000000..6f57d9055 --- /dev/null +++ b/src/Copybara.Core/Buildozer/BuildozerTransformation.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Buildozer; + +namespace Copybara.Buildozer; + +/// +/// Common interface implemented by all buildozer transformations. +/// +/// Used by to batch multiple invocations in one buildozer cli +/// call. +/// +public interface IBuildozerTransformation : ITransformation +{ + /// Actions to run before calling buildozer. For example creating files. + void BeforeRun(TransformWork work) + { + } + + /// List of commands to execute. + IEnumerable GetCommands(); +} diff --git a/src/Copybara.Core/Buildozer/Command.cs b/src/Copybara.Core/Buildozer/Command.cs new file mode 100644 index 000000000..e9b2afc13 --- /dev/null +++ b/src/Copybara.Core/Buildozer/Command.cs @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Buildozer; + +/// Represents a possibly-reversible Buildozer command. +[StarlarkBuiltin("Command", Doc = "Buildozer command type")] +public sealed class Command : IStarlarkPrintableValue +{ + private readonly string _command; + private readonly string? _reverse; + + private Command(string command, string? reverse) + { + _command = Preconditions.CheckNotNull(command); + Preconditions.CheckArgument(command.Trim().Length != 0, "Found empty command"); + Preconditions.CheckArgument( + reverse == null || reverse.Trim().Length != 0, + "Found empty reversal command. Command was: {0}", + command); + + _reverse = reverse; + new ArgValidator(command).Validate(); + if (reverse != null) + { + new ArgValidator(reverse).Validate(); + } + } + + internal static Command FromConfig(string command, string? reverse) + { + if (reverse == null) + { + List components = command + .Split(' ', 2, StringSplitOptions.RemoveEmptyEntries) + .ToList(); + if (components.Count == 2) + { + reverse = ReverseArgs(components[0], components[1]); + } + } + + try + { + return new Command(command, reverse); + } + catch (ArgumentException ex) + { + throw new EvalException(ex.Message, ex); + } + } + + private sealed class ArgValidator + { + private readonly List _argv; + + internal ArgValidator(string command) + { + _argv = SplitArgv(command); + } + + private void ValidateCount(bool valid, string requirement) + { + Preconditions.CheckArgument( + valid, + "'{0}' requires {1}, but got: {2}", + _argv[0], + requirement, + ArgCount()); + } + + private int ArgCount() => _argv.Count - 1; + + internal void Validate() + { + Preconditions.CheckArgument( + _argv.Count != 0, "Expected an operation, but got empty string."); + switch (_argv[0]) + { + case "del_subinclude": + case "rename": + case "copy": + case "copy_no_overwrite": + ValidateCount(ArgCount() == 2, "exactly 2 arguments"); + break; + case "fix": + case "print": + case "remove_comment": + break; // can take 0+ + case "replace_subinclude": + case "move": + ValidateCount(ArgCount() >= 3, "at least 3 arguments"); + break; + case "delete": + ValidateCount(ArgCount() == 0, "exactly 0 arguments"); + break; + case "replace": + ValidateCount(ArgCount() == 3, "exactly 3 arguments"); + break; + case "comment": + case "remove": + case "set": + case "set_if_absent": + ValidateCount(ArgCount() >= 1, "at least 1 argument"); + break; + case "add": + case "new_load": + case "new": + ValidateCount(ArgCount() >= 2, "at least 2 arguments"); + break; + default: + // We assume that all unary operations are covered. + Preconditions.CheckArgument( + ArgCount() > 1, "Expected an operation, but got '{0}'.", _argv[0]); + break; + } + } + } + + public bool IsImmutable() => true; + + public void Repr(Printer printer, StarlarkSemantics semantics) + { + printer.Append( + string.Format("buildozer.cmd({0}, reverse = {1})", _command, _reverse)); + } + + /// + /// Returns the command and arguments concatenated, which can be passed directly to Buildozer. + /// + public override string ToString() => _command; + + /// Returns the reverse version of this command. + /// if this instance is not reversible + internal Command Reverse() + { + if (_reverse == null) + { + throw new NonReversibleValidationException( + "The current command is not auto-reversible and a reverse was not provided: " + + _command); + } + + return new Command(_reverse, _command); + } + + /// Calculates the reversal of a command whose reversal has not been manually specified. + private static string? ReverseArgs(string commandName, string args) + { + switch (commandName) + { + case "add": + return "remove " + args; + case "remove": + if (args.Contains(' ')) + { + // Do not reverse 'remove attr' operation. Only 'remove attr value' + return "add " + args; + } + return null; + case "replace": + List reverseArgs = SplitArgv(args); + if (reverseArgs.Count != 3) + { + throw StarlarkRt.Errorf( + "Cannot reverse '{0} {1}', expected three arguments, but found {2}.", + commandName, args, reverseArgs.Count); + } + (reverseArgs[1], reverseArgs[2]) = (reverseArgs[2], reverseArgs[1]); + return "replace " + string.Join(' ', reverseArgs); + } + return null; + } + + private static List SplitArgv(string argv) => + argv.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); +} diff --git a/src/Copybara.Core/Buildozer/Target.cs b/src/Copybara.Core/Buildozer/Target.cs new file mode 100644 index 000000000..e1a57209e --- /dev/null +++ b/src/Copybara.Core/Buildozer/Target.cs @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Copybara.Common; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Buildozer; + +/// Specifies a target, including the package and name of target. +internal sealed class Target +{ + private static readonly Regex TargetNamePattern = new("^[^:]*:[^:]+$", RegexOptions.Compiled); + + private readonly string _pkg; + private readonly string _name; + + private Target(string[] components) + { + Preconditions.CheckArgument( + components.Length == 2, "{0}", string.Join(", ", components)); + + _pkg = Preconditions.CheckNotNull(components[0]); + _name = Preconditions.CheckNotNull(components[1]); + } + + public string GetPackage() => _pkg; + + public string GetName() => _name; + + public override string ToString() => _pkg + ":" + _name; + + /// + /// Parses a target specified in configuration. + /// + /// target specified in the form PKG:TARGET_NAME + /// if is not formatted correctly + internal static Target FromConfig(string configString) + { + if (configString.StartsWith("/", StringComparison.Ordinal)) + { + throw new EvalException("target must be relative and not start with '/' or '//'"); + } + if (!TargetNamePattern.IsMatch(configString)) + { + throw new EvalException( + "target must be in the form of {PKG}:{TARGET_NAME}, e.g. foo/bar:baz"); + } + return new Target(configString.Split(':', 2)); + } + + internal static ImmutableArray AsStringList(IReadOnlyList targets) => + targets.Select(t => t.ToString()).ToImmutableArray(); +} diff --git a/src/Copybara.Core/Buildozer/TargetNotFoundException.cs b/src/Copybara.Core/Buildozer/TargetNotFoundException.cs new file mode 100644 index 000000000..77da1bdd1 --- /dev/null +++ b/src/Copybara.Core/Buildozer/TargetNotFoundException.cs @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Buildozer; + +/// Error thrown when a target couldn't be found during a Buildozer transformation. +public class TargetNotFoundException : Exception +{ + internal TargetNotFoundException(string msg) + : base(msg) + { + } +} diff --git a/src/Copybara.Core/Checks/Checker.cs b/src/Copybara.Core/Checks/Checker.cs new file mode 100644 index 000000000..c6c8a9688 --- /dev/null +++ b/src/Copybara.Core/Checks/Checker.cs @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Checks; + +// TODO(port): minimal port of com.google.copybara.checks.Checker, created here because the http +// package depends on it. Consolidate if the checks package gets a fuller port. + +/// A generic interface for performing checks on string contents and files. +[StarlarkBuiltin("checker", Doc = "A checker to be run on arbitrary data and files")] +public interface IChecker : IStarlarkValue +{ + /// Performs a check on the given contents. + /// if the check produced errors. + void DoCheck(ImmutableDictionary fields, Console console); + + /// Performs a check on the files inside a given path. + /// if the check produced errors. + void DoCheck(string target, Console console); +} diff --git a/src/Copybara.Core/Checks/CheckerException.cs b/src/Copybara.Core/Checks/CheckerException.cs new file mode 100644 index 000000000..aa5661ccd --- /dev/null +++ b/src/Copybara.Core/Checks/CheckerException.cs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.Checks; + +// TODO(port): minimal port of com.google.copybara.checks.CheckerException. + +/// Indicates an exception running a . +public class CheckerException : ValidationException +{ + public CheckerException(string message) + : base(message) + { + } + + public CheckerException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Credentials/ConstantCredentialIssuer.cs b/src/Copybara.Core/Credentials/ConstantCredentialIssuer.cs new file mode 100644 index 000000000..699913233 --- /dev/null +++ b/src/Copybara.Core/Credentials/ConstantCredentialIssuer.cs @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Credentials; + +/// A static CredentialIssuer, e.g. a password, username, api key, etc. +public class ConstantCredentialIssuer : CredentialIssuer +{ + private readonly string _secret; + private readonly string _name; + private readonly bool _open; + + public static ConstantCredentialIssuer CreateConstantSecret(string name, string secret) => + new(Preconditions.CheckNotNull(name), Preconditions.CheckNotNull(secret), false); + + public static ConstantCredentialIssuer CreateConstantOpenValue(string value) => + new(Preconditions.CheckNotNull(value), value, true); + + private ConstantCredentialIssuer(string name, string secret, bool open) + { + _secret = secret; + _name = name; + _open = open; + } + + public Credential Issue() => + _open ? new OpenCredential(_secret) : new StaticSecret(_name, _secret); + + public ImmutableSetMultimap Describe() => + ImmutableSetMultimap.CreateBuilder() + .Put("type", "constant") + .Put("name", _name) + .Put("open", _open ? "true" : "false") + .Build(); +} diff --git a/src/Copybara.Core/Credentials/Credential.cs b/src/Copybara.Core/Credentials/Credential.cs new file mode 100644 index 000000000..98ffbc339 --- /dev/null +++ b/src/Copybara.Core/Credentials/Credential.cs @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Credentials; + +/// Holder for a credential. +public interface Credential +{ + /// A safe value that describes the credential. + string PrintableValue(); + + /// Whether the credential is still believed to be valid. + bool Valid(); + + /// The raw secret, this should not be used outside of framework code. + /// if the secret cannot be retrieved. + string ProvideSecret(); +} diff --git a/src/Copybara.Core/Credentials/CredentialIssuer.cs b/src/Copybara.Core/Credentials/CredentialIssuer.cs new file mode 100644 index 000000000..d91158d96 --- /dev/null +++ b/src/Copybara.Core/Credentials/CredentialIssuer.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Starlark.Eval; + +namespace Copybara.Credentials; + +/// +/// An object able to mint credentials. The issuer should handle caching etc. +/// +public interface CredentialIssuer : IStarlarkValue +{ + /// Issue a to be used by an endpoint. + /// if minting the credential fails. + Credential Issue(); + + /// Metadata describing this issuer. + ImmutableSetMultimap Describe(); +} diff --git a/src/Copybara.Core/Credentials/CredentialIssuingException.cs b/src/Copybara.Core/Credentials/CredentialIssuingException.cs new file mode 100644 index 000000000..e44c6072e --- /dev/null +++ b/src/Copybara.Core/Credentials/CredentialIssuingException.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.Credentials; + +/// An exception thrown if minting a credential fails. +public class CredentialIssuingException : ValidationException +{ + public CredentialIssuingException(string message) + : base(message) + { + } + + public CredentialIssuingException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Credentials/CredentialModule.cs b/src/Copybara.Core/Credentials/CredentialModule.cs new file mode 100644 index 000000000..1d5984fb8 --- /dev/null +++ b/src/Copybara.Core/Credentials/CredentialModule.cs @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; +using ConsoleImpl = Copybara.Util.Console.Console; + +namespace Copybara.Credentials; + +/// Starlark builtins to handle credentials. +[StarlarkBuiltin("credentials", Doc = "Module for working with credentials.")] +public class CredentialModule : IStarlarkValue +{ + protected readonly CredentialOptions Options; + protected readonly ConsoleImpl Console; + + public CredentialModule(ConsoleImpl console, CredentialOptions options) + { + Console = console; + Options = options; + } + + [StarlarkMethod( + "static_secret", + Doc = "Holder for secrets that can be in plaintext within the config.")] + public CredentialIssuer StaticSecret( + [Param(Name = "name", Doc = "A name for this secret.")] string name, + [Param(Name = "secret", Doc = "The secret value.")] string secret) + { + return ConstantCredentialIssuer.CreateConstantSecret(name, secret); + } + + [StarlarkMethod( + "static_value", + Doc = "Holder for credentials that are safe to read/log (e.g. 'x-access-token') .")] + public CredentialIssuer StaticValue( + [Param(Name = "value", Doc = "The open value.")] string value) + { + return ConstantCredentialIssuer.CreateConstantOpenValue(value); + } + + [StarlarkMethod( + "toml_key_source", + Doc = + "Supply an authentication credential from the " + + "file pointed to by the --http-credential-file flag.")] + public CredentialIssuer TomlKeySource( + [Param( + Name = "dot_path", + Doc = "Dot path to the data field containing the credential.", + AllowedTypes = new[] { typeof(string) })] + string dotPath) + { + if (Options.CredentialFile == null) + { + throw new ValidationException( + "Credential file for toml key source has not been supplied"); + } + + return new TomlKeySource(Options.CredentialFile, dotPath); + } + + [StarlarkMethod( + "username_password", + Doc = "A pair of username and password credential issuers.")] + public UsernamePasswordIssuer UsernamePassword( + [Param( + Name = "username", + Doc = "Username credential.", + AllowedTypes = new[] { typeof(CredentialIssuer) })] + CredentialIssuer username, + [Param( + Name = "password", + Doc = "Password credential.", + AllowedTypes = new[] { typeof(CredentialIssuer) })] + CredentialIssuer password) + { + return Credentials.UsernamePasswordIssuer.Create(username, password); + } +} diff --git a/src/Copybara.Core/Credentials/CredentialOptions.cs b/src/Copybara.Core/Credentials/CredentialOptions.cs new file mode 100644 index 000000000..dc421685b --- /dev/null +++ b/src/Copybara.Core/Credentials/CredentialOptions.cs @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Credentials; + +/// Flags related to credentials. +public class CredentialOptions : IOption +{ + /* + TODO(b/270712326) enable this flag + [Parameter( + Names = "--credential-file", + Description = "location of toml file for reading credentials")] + */ + + /// Location of the toml file for reading credentials. + public string? CredentialFile { get; set; } +} diff --git a/src/Copybara.Core/Credentials/CredentialRetrievalException.cs b/src/Copybara.Core/Credentials/CredentialRetrievalException.cs new file mode 100644 index 000000000..a41a7d95b --- /dev/null +++ b/src/Copybara.Core/Credentials/CredentialRetrievalException.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.Credentials; + +/// An exception thrown if retrieving a credential fails. +public class CredentialRetrievalException : ValidationException +{ + public CredentialRetrievalException(string message) + : base(message) + { + } + + public CredentialRetrievalException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Credentials/OpenCredential.cs b/src/Copybara.Core/Credentials/OpenCredential.cs new file mode 100644 index 000000000..82d7b4f6e --- /dev/null +++ b/src/Copybara.Core/Credentials/OpenCredential.cs @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Credentials; + +/// A non-secret Credential, e.g. a system username like "x-access-token". +public class OpenCredential : Credential +{ + private readonly string _open; + + public OpenCredential(string open) + { + _open = Preconditions.CheckNotNull(open); + } + + public string PrintableValue() => _open; + + public bool Valid() => true; + + public string ProvideSecret() => _open; + + public override string ToString() => PrintableValue(); +} diff --git a/src/Copybara.Core/Credentials/StaticSecret.cs b/src/Copybara.Core/Credentials/StaticSecret.cs new file mode 100644 index 000000000..585e77c41 --- /dev/null +++ b/src/Copybara.Core/Credentials/StaticSecret.cs @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Credentials; + +/// A static secret Credential, e.g. a password, api key, etc. +public class StaticSecret : Credential +{ + private readonly string _secret; + protected readonly string Name; + + public StaticSecret(string name, string secret) + { + _secret = Preconditions.CheckNotNull(secret); + Name = Preconditions.CheckNotNull(name); + } + + public virtual string PrintableValue() => $""; + + public virtual bool Valid() => true; + + public virtual string ProvideSecret() => _secret; + + public override string ToString() => PrintableValue(); +} diff --git a/src/Copybara.Core/Credentials/TomlKeySource.cs b/src/Copybara.Core/Credentials/TomlKeySource.cs new file mode 100644 index 000000000..36c66d19e --- /dev/null +++ b/src/Copybara.Core/Credentials/TomlKeySource.cs @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using Copybara.Common; +using Starlark.Eval; + +namespace Copybara.Credentials; + +/// Fetches a value located within a toml file. +/// +/// The upstream implementation uses the org.tomlj library. This port ships a minimal, +/// self-contained TOML reader that supports the subset of TOML needed to resolve a dotted key +/// path to a string value (top-level and [table] sections, dotted keys, basic and literal +/// strings). It is not a full TOML parser. +/// +public class TomlKeySource : CredentialIssuer, IStarlarkValue +{ + private readonly string _file; + private readonly string _dotPath; + + public TomlKeySource(string file, string keyPath) + { + _file = file; + _dotPath = keyPath; + } + + public Credential Issue() + { + IReadOnlyDictionary values; + try + { + values = ParseStringValues(File.ReadAllText(_file)); + } + catch (IOException e) + { + throw new CredentialIssuingException("Error reading Toml file.", e); + } + + if (!values.TryGetValue(_dotPath, out var data)) + { + throw new CredentialIssuingException( + string.Format("key {0} not found in file {1}", _dotPath, _file)); + } + + return new StaticSecret(_dotPath, data); + } + + public ImmutableSetMultimap Describe() => + ImmutableSetMultimap.CreateBuilder() + .Put("type", "Toml") + .Put("dotPath", _dotPath) + .Build(); + + /// + /// Parses the string-valued entries of a TOML document into a flat map keyed by fully-qualified + /// dotted path (e.g. foo.bar.baz). + /// + private static IReadOnlyDictionary ParseStringValues(string content) + { + var result = new Dictionary(); + string prefix = ""; + + foreach (var rawLine in content.Split('\n')) + { + var line = StripComment(rawLine).Trim(); + if (line.Length == 0) + { + continue; + } + + if (line[0] == '[') + { + // Table header: [a.b.c] or [[a.b]] (array of tables — treated like a table header). + int end = line.IndexOf(']'); + if (end < 0) + { + continue; + } + + var header = line.Substring(1, end - 1).Trim(); + if (header.StartsWith("[") && header.EndsWith("]")) + { + header = header.Substring(1, header.Length - 2).Trim(); + } + + prefix = JoinKeyParts(header); + continue; + } + + int eq = line.IndexOf('='); + if (eq < 0) + { + continue; + } + + var key = line.Substring(0, eq).Trim(); + var valueText = line.Substring(eq + 1).Trim(); + + if (!TryParseString(valueText, out var value)) + { + continue; + } + + var fullKey = JoinKeyParts(key); + if (prefix.Length > 0) + { + fullKey = prefix + "." + fullKey; + } + + result[fullKey] = value; + } + + return result; + } + + private static string JoinKeyParts(string key) + { + // Normalize dotted keys, stripping quotes and surrounding whitespace from each part. + var parts = key.Split('.'); + var builder = new StringBuilder(); + for (var i = 0; i < parts.Length; i++) + { + if (i > 0) + { + builder.Append('.'); + } + + builder.Append(Unquote(parts[i].Trim())); + } + + return builder.ToString(); + } + + private static string Unquote(string s) + { + if (s.Length >= 2 && + ((s[0] == '"' && s[^1] == '"') || (s[0] == '\'' && s[^1] == '\''))) + { + return s.Substring(1, s.Length - 2); + } + + return s; + } + + private static bool TryParseString(string valueText, out string value) + { + value = ""; + if (valueText.Length < 2) + { + return false; + } + + char quote = valueText[0]; + if (quote == '"' && valueText[^1] == '"') + { + value = Unescape(valueText.Substring(1, valueText.Length - 2)); + return true; + } + + if (quote == '\'' && valueText[^1] == '\'') + { + // Literal string: no escaping. + value = valueText.Substring(1, valueText.Length - 2); + return true; + } + + return false; + } + + private static string Unescape(string s) + { + if (s.IndexOf('\\') < 0) + { + return s; + } + + var builder = new StringBuilder(s.Length); + for (var i = 0; i < s.Length; i++) + { + var c = s[i]; + if (c == '\\' && i + 1 < s.Length) + { + var next = s[++i]; + builder.Append(next switch + { + 'n' => '\n', + 't' => '\t', + 'r' => '\r', + '"' => '"', + '\\' => '\\', + _ => next, + }); + } + else + { + builder.Append(c); + } + } + + return builder.ToString(); + } + + private static string StripComment(string line) + { + // Removes a trailing '#' comment, honoring quoted strings. + bool inBasic = false; + bool inLiteral = false; + for (var i = 0; i < line.Length; i++) + { + var c = line[i]; + if (c == '"' && !inLiteral) + { + inBasic = !inBasic; + } + else if (c == '\'' && !inBasic) + { + inLiteral = !inLiteral; + } + else if (c == '#' && !inBasic && !inLiteral) + { + return line.Substring(0, i); + } + } + + return line; + } +} diff --git a/src/Copybara.Core/Credentials/TtlSecret.cs b/src/Copybara.Core/Credentials/TtlSecret.cs new file mode 100644 index 000000000..85d8a1d90 --- /dev/null +++ b/src/Copybara.Core/Credentials/TtlSecret.cs @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Credentials; + +/// A credential with a limited TTL. +public class TtlSecret : StaticSecret +{ + private readonly DateTimeOffset _ttl; + private readonly Func _clock; + + public TtlSecret(string secret, string name, DateTimeOffset ttl, Func clock) + : base(name, secret) + { + _ttl = ttl; + _clock = Preconditions.CheckNotNull(clock); + } + + public override string PrintableValue() => + $""; + + public override string ProvideSecret() + { + DateTimeOffset now = _clock(); + if (_ttl < now) + { + throw new CredentialRetrievalException( + string.Format( + "Credential {0} expired {1} seconds ago.", + PrintableValue(), (long)(now - _ttl).TotalSeconds)); + } + + return base.ProvideSecret(); + } + + public override bool Valid() => _ttl > _clock().AddSeconds(/* 10s grace */ 10); + + public override string ToString() => PrintableValue(); +} diff --git a/src/Copybara.Core/Credentials/UsernamePasswordIssuer.cs b/src/Copybara.Core/Credentials/UsernamePasswordIssuer.cs new file mode 100644 index 000000000..8e65760e6 --- /dev/null +++ b/src/Copybara.Core/Credentials/UsernamePasswordIssuer.cs @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Starlark.Eval; + +namespace Copybara.Credentials; + +/// A username/password pair issuer. +/// +/// In upstream this is a nested @AutoValue class of CredentialModule. It is lifted to +/// a top-level record here so peer packages can reference it as +/// Copybara.Credentials.UsernamePasswordIssuer. +/// +public sealed record UsernamePasswordIssuer( + CredentialIssuer Username, + CredentialIssuer Password) : IStarlarkValue +{ + public ImmutableArray> DescribeCredentials() => + ImmutableArray.Create(Username.Describe(), Password.Describe()); + + public static UsernamePasswordIssuer Create( + CredentialIssuer username, CredentialIssuer password) => new(username, password); +} diff --git a/src/Copybara.Core/Format/BuildifierFormat.cs b/src/Copybara.Core/Format/BuildifierFormat.cs new file mode 100644 index 000000000..24c0627b0 --- /dev/null +++ b/src/Copybara.Core/Format/BuildifierFormat.cs @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; +using Copybara.Util.Console; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Command = Copybara.Util.Command; +using CommandException = Copybara.Util.CommandException; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Format; + +/// Format using buildifier. +public class BuildifierFormat : ITransformation +{ + private static readonly ILogger Logger = NullLogger.Instance; + + private readonly BuildifierOptions _buildifierOptions; + private readonly GeneralOptions _generalOptions; + private readonly Glob _glob; + private readonly LintMode _lintMode; + private readonly ImmutableArray _warnings; + private readonly string? _type; + + internal BuildifierFormat( + BuildifierOptions buildifierOptions, + GeneralOptions generalOptions, + Glob glob, + LintMode lintMode, + ImmutableArray warnings, + string? type) + { + _buildifierOptions = Preconditions.CheckNotNull(buildifierOptions); + _generalOptions = Preconditions.CheckNotNull(generalOptions); + _glob = Preconditions.CheckNotNull(glob); + _lintMode = lintMode; + _warnings = warnings; + _type = type; + } + + public TransformationStatus Transform(TransformWork work) + { + string checkoutDir = work.GetCheckoutDir(); + IPathMatcher pathMatcher = _glob.RelativeTo(checkoutDir); + var paths = ImmutableArray.CreateBuilder(); + foreach (string file in Directory.EnumerateFiles( + checkoutDir, "*", SearchOption.AllDirectories)) + { + if (pathMatcher.Matches(file)) + { + paths.Add(Path.GetFullPath(file)); + } + } + + ImmutableArray builtPaths = paths.ToImmutable(); + if (builtPaths.Length == 0) + { + return TransformationStatus.Noop(_glob + " didn't match any build file to format"); + } + + for (int i = 0; i < builtPaths.Length; i += _buildifierOptions.BatchSize) + { + var sublist = builtPaths.Skip(i).Take(_buildifierOptions.BatchSize).ToList(); + Run(work.GetConsole(), checkoutDir, sublist); + } + + return TransformationStatus.Success(); + } + + /// Runs buildifier with the given arguments. + private void Run(Console console, string checkoutDir, IReadOnlyList args) + { + var argBuilder = new List { _buildifierOptions.BuildifierBin }; + if (_type != null) + { + argBuilder.Add("-type=" + _type); + } + if (_lintMode != LintMode.Off) + { + argBuilder.Add("-lint=" + _lintMode.ToString().ToLowerInvariant()); + if (!_warnings.IsDefaultOrEmpty) + { + argBuilder.Add("-warnings=" + string.Join(",", _warnings)); + } + } + argBuilder.AddRange(args); + + try + { + var cmd = new Command(argBuilder.ToArray(), null, checkoutDir); + CommandOutputWithStatus output = _generalOptions.NewCommandRunner(cmd) + .WithVerbose(_generalOptions.IsVerbose()) + .Execute(); + if (output.GetStdout().Length != 0) + { + Logger.LogInformation("buildifier stdout: {Stdout}", output.GetStdout()); + } + if (output.GetStderr().Length != 0) + { + Logger.LogInformation("buildifier stderr: {Stderr}", output.GetStderr()); + } + } + catch (BadExitStatusWithOutputException e) + { + Log(console, e.GetOutput()); + ValidationException.CheckCondition( + e.GetResult().TerminationStatus.GetExitCode() != 1, + "Build file(s) couldn't be formatted because there was a syntax error"); + throw new IOException( + "Failed to execute buildifier with args: " + string.Join(" ", args), e); + } + catch (CommandException e) + { + throw new IOException( + "Failed to execute buildifier with args: " + string.Join(" ", args), e); + } + } + + private static void Log(Console console, CommandOutput output) + { + Consoles.LogLines(console, "buildifier stdout: ", output.GetStdout()); + Consoles.LogLines(console, "buildifier stderr: ", output.GetStderr()); + } + + public ITransformation Reverse() => this; + + public string Describe() => "Buildifier"; + + /// Valid modes that we support for buildifier -lint flag. + public enum LintMode + { + Off, + + // Warn, // Not exposed for now since we don't show the stderr/out warnings in the console + Fix, + } +} diff --git a/src/Copybara.Core/Format/BuildifierOptions.cs b/src/Copybara.Core/Format/BuildifierOptions.cs new file mode 100644 index 000000000..5b1800e15 --- /dev/null +++ b/src/Copybara.Core/Format/BuildifierOptions.cs @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Format; + +/// Specifies how Buildifier is executed. +public class BuildifierOptions : IOption +{ + [Flag( + "--buildifier-bin", + "Binary to use for buildifier (Default is /usr/bin/buildifier)", + Hidden = true)] + public string BuildifierBin { get; set; } = "/usr/bin/buildifier"; + + [Flag("--buildifier-batch-size", "Process files in batches this size")] + public int BatchSize { get; set; } = 200; +} diff --git a/src/Copybara.Core/Format/FormatModule.cs b/src/Copybara.Core/Format/FormatModule.cs new file mode 100644 index 000000000..94dcfe08d --- /dev/null +++ b/src/Copybara.Core/Format/FormatModule.cs @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Config; +using Copybara.Format; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Format; + +/// Skylark module for transforming the code to Google's style/guidelines. +[StarlarkBuiltin( + "format", + Doc = "Module for formatting the code to Google's style/guidelines")] +public class FormatModule : IStarlarkValue +{ + private static readonly ImmutableHashSet BuildifierTypeValues = + ImmutableHashSet.Create("auto", "bzl", "build", "workspace"); + + private static readonly Glob DefaultBuildifierPaths = + Glob.CreateGlob(ImmutableArray.Create("**.bzl", "**/BUILD", "BUILD")); + + protected readonly WorkflowOptions WorkflowOptions; + protected readonly BuildifierOptions BuildifierOptions; + protected readonly GeneralOptions GeneralOptions; + + public FormatModule( + WorkflowOptions workflowOptions, + BuildifierOptions buildifierOptions, + GeneralOptions generalOptions) + { + WorkflowOptions = Preconditions.CheckNotNull(workflowOptions); + BuildifierOptions = Preconditions.CheckNotNull(buildifierOptions); + GeneralOptions = Preconditions.CheckNotNull(generalOptions); + } + + [StarlarkMethod( + "buildifier", + Doc = "Formats the BUILD files using buildifier.")] + public ITransformation Buildifier( + [Param( + Name = "paths", + AllowedTypes = new[] { typeof(Glob), typeof(StarlarkList), typeof(NoneType) }, + Doc = "Paths of the files to format relative to the workdir.", + DefaultValue = "None", + Named = true)] + object? paths, + [Param( + Name = "type", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Doc = + "The type of the files. Can be 'auto', 'bzl', 'build' or 'workspace'. Note that" + + " this is not recommended to be set and might break in the future. The" + + " default is 'auto'. This mode formats as BUILD files \"BUILD\"," + + " \"BUILD.bazel\", \"WORKSPACE\" and \"WORKSPACE.bazel\" files. The rest as" + + " bzl files. Prefer to use those names for BUILD files instead of setting" + + " this flag.", + DefaultValue = "'auto'", + Named = true)] + object? type, + [Param( + Name = "lint", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Doc = + "If buildifier --lint should be used. This fixes several common issues. Note that" + + " this transformation is difficult to revert. For example if it removes a" + + " load statement because is not used after removing a rule, then the reverse" + + " workflow needs to add back the load statement (core.replace or similar). " + + " Possible values: `OFF`, `FIX`. Default is `OFF`", + DefaultValue = "None", + Named = true)] + object? lint, + [Param( + Name = "lint_warnings", + AllowedTypes = new[] { typeof(Sequence) }, + DefaultValue = "[]", + Doc = "Warnings used in the lint mode. Default is buildifier default", + Named = true)] + object warnings) + { + string? typeStr = SkylarkUtil.ConvertFromNoneable(type, null); + if (typeStr != null) + { + SkylarkUtil.Check( + BuildifierTypeValues.Contains(typeStr), + "Non-valid type: {0}. Valid types: {1}", + typeStr, + string.Join(", ", BuildifierTypeValues)); + } + + string lintStr = SkylarkUtil.ConvertFromNoneable(lint, "OFF")!; + BuildifierFormat.LintMode lintMode = ParseLintMode(lintStr); + + var warningsList = SkylarkUtil.ConvertStringList(warnings, "lint_warnings"); + SkylarkUtil.Check( + lintMode != BuildifierFormat.LintMode.Off || warningsList.Count == 0, + "Warnings can only be used when lint is set to FIX"); + + return new BuildifierFormat( + BuildifierOptions, + GeneralOptions, + Glob.WrapGlob(paths, DefaultBuildifierPaths)!, + lintMode, + warningsList.ToImmutableArray(), + typeStr); + } + + private static BuildifierFormat.LintMode ParseLintMode(string value) + { + if (Enum.TryParse(value, ignoreCase: true, out var result) + && Enum.IsDefined(result)) + { + return result; + } + + throw StarlarkRt.Errorf( + "Invalid value '{0}' for field 'lint'. Valid values are: {1}", + value, + string.Join(", ", Enum.GetNames())); + } +} diff --git a/src/Copybara.Core/Git/CannotIntegrateException.cs b/src/Copybara.Core/Git/CannotIntegrateException.cs new file mode 100644 index 000000000..3205966ac --- /dev/null +++ b/src/Copybara.Core/Git/CannotIntegrateException.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.Git; + +/// +/// Thrown if git destination integrate failed for some reason. Port of +/// com.google.copybara.git.CannotIntegrateException. +/// +public class CannotIntegrateException : ValidationException +{ + public CannotIntegrateException(string message) + : base(message) + { + } + + public CannotIntegrateException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Git/FetchResult.cs b/src/Copybara.Core/Git/FetchResult.cs new file mode 100644 index 000000000..9540926d5 --- /dev/null +++ b/src/Copybara.Core/Git/FetchResult.cs @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; + +namespace Copybara.Git; + +/// +/// The result of executing a git fetch command. Port of +/// com.google.copybara.git.FetchResult. +/// +public sealed class FetchResult +{ + private readonly ImmutableDictionary _deleted; + private readonly ImmutableDictionary _inserted; + private readonly ImmutableDictionary _updated; + + public FetchResult( + IReadOnlyDictionary before, + IReadOnlyDictionary after) + { + var deleted = ImmutableDictionary.CreateBuilder(); + var inserted = ImmutableDictionary.CreateBuilder(); + var updated = ImmutableDictionary.CreateBuilder(); + + foreach (var entry in before) + { + if (!after.TryGetValue(entry.Key, out var afterVal)) + { + deleted[entry.Key] = entry.Value; + } + else if (!Equals(afterVal, entry.Value)) + { + updated[entry.Key] = new RefUpdate(entry.Value, afterVal); + } + } + foreach (var entry in after) + { + if (!before.ContainsKey(entry.Key)) + { + inserted[entry.Key] = entry.Value; + } + } + + _deleted = deleted.ToImmutable(); + _inserted = inserted.ToImmutable(); + _updated = updated.ToImmutable(); + } + + public override string ToString() => + $"FetchResult{{deleted={FormatMap(_deleted)}, inserted={FormatMap(_inserted)}," + + $" updated={FormatMap(_updated)}}}"; + + private static string FormatMap(ImmutableDictionary map) => + "{" + string.Join(", ", map.Select(e => $"{e.Key}={e.Value}")) + "}"; + + public IReadOnlyDictionary GetDeleted() => _deleted; + + public IReadOnlyDictionary GetInserted() => _inserted; + + public IReadOnlyDictionary GetUpdated() => _updated; + + /// A reference update for a fetch command. Contains before and after SHA-1. + public sealed class RefUpdate + { + private readonly GitRevision _before; + private readonly GitRevision _after; + + public RefUpdate(GitRevision before, GitRevision after) + { + _before = before; + _after = after; + } + + public GitRevision GetBefore() => _before; + + public GitRevision GetAfter() => _after; + + public override string ToString() => _before.GetHash() + " -> " + _after.GetHash(); + } +} diff --git a/src/Copybara.Core/Git/GitCredential.cs b/src/Copybara.Core/Git/GitCredential.cs new file mode 100644 index 000000000..2225d9859 --- /dev/null +++ b/src/Copybara.Core/Git/GitCredential.cs @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; + +namespace Copybara.Git; + +/// +/// Utility class for executing 'git credential' commands. Port of +/// com.google.copybara.git.GitCredential. +/// +public sealed class GitCredential +{ + private static readonly Regex NewLine = new(@"\r\n|\n|\r", RegexOptions.Compiled); + + private readonly TimeSpan _timeout; + private readonly GitEnvironment _gitEnv; + + public GitCredential(TimeSpan timeout, GitEnvironment gitEnv) + { + _timeout = timeout; + _gitEnv = Preconditions.CheckNotNull(gitEnv); + } + + /// + /// Execute 'git credential fill' for a url. + /// + /// the directory to execute the command in. This is important if credential + /// configuration is set in the local git config. + /// url to get the credentials from + /// a username and password + /// If the url doesn't have a protocol, is not valid, or the + /// username/password couldn't be found. + public UserPassword Fill(string gitDir, string url) + { + var env = _gitEnv.WithNoGitPrompt().GetEnvironment(); + + Uri uri; + try + { + uri = new Uri(url); + } + catch (Exception e) when (e is UriFormatException or InvalidOperationException) + { + throw new ValidationException("Cannot get credentials for " + url, e); + } + string? protocol = uri.Scheme; + ValidationException.CheckCondition( + !string.IsNullOrEmpty(protocol), "Cannot find the protocol for %s", url); + string host = uri.Host; + + var cmd = new Command( + new[] { _gitEnv.ResolveGitBinary(), "--git-dir=" + gitDir, "credential", "fill" }, + env, + gitDir); + + var request = new StringBuilder(); + request.Append($"protocol={protocol}\nhost={host}\n"); + if (!string.IsNullOrEmpty(uri.AbsolutePath)) + { + request.Append($"path={uri.AbsolutePath.TrimStart('/')}\n"); + } + request.Append('\n'); + + CommandOutputWithStatus result; + try + { + // DON'T LOG THE OUTPUT. WE DON'T WANT TO ACCIDENTALLY LOG THE PASSWORD! + result = new CommandRunner(cmd, _timeout) + .WithMaxStdOutLogLines(0) + .WithInput(Encoding.UTF8.GetBytes(request.ToString())) + .Execute(); + } + catch (BadExitStatusWithOutputException e) + { + string errStr = e.GetOutput().GetStderr(); + ValidationException.CheckCondition( + !errStr.Contains("could not read"), + "Interactive prompting of passwords for git is disabled," + + " use git credential store before calling Copybara."); + throw new RepoException("Error getting credentials:\n" + errStr, e); + } + catch (CommandException e) + { + throw new RepoException("Error getting credentials", e); + } + + var map = new Dictionary(); + foreach (var line in NewLine.Split(result.GetStdout())) + { + if (line.Length == 0) + { + continue; + } + int idx = line.IndexOf('='); + if (idx < 0) + { + continue; + } + map[line.Substring(0, idx)] = line.Substring(idx + 1); + } + + if (!map.TryGetValue("username", out var username)) + { + throw new RepoException("git credentials for " + url + " didn't return a username"); + } + if (!map.TryGetValue("password", out var password)) + { + throw new RepoException("git credentials for " + url + " didn't return a password"); + } + return new UserPassword(username, password); + } + + /// A class that contains a username and password for git repositories. + public sealed class UserPassword + { + private readonly string _username; + private readonly string _password; + + internal UserPassword(string username, string password) + { + _username = Preconditions.CheckNotNull(username); + _password = Preconditions.CheckNotNull(password); + } + + // DON'T CHANGE THIS: never expose the password in ToString. + public override string ToString() => + $"UserPassword{{username={_username}, password=(hidden)}}"; + + public string GetUsername() => _username; + + /// Get the password. BE CAREFUL AND DON'T LOG IT! + public string GetPasswordBeCareful() => _password; + } +} diff --git a/src/Copybara.Core/Git/GitEnvironment.cs b/src/Copybara.Core/Git/GitEnvironment.cs new file mode 100644 index 000000000..0ee245dd6 --- /dev/null +++ b/src/Copybara.Core/Git/GitEnvironment.cs @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; + +namespace Copybara.Git; + +/// +/// Environment for running git commands. Port of +/// com.google.copybara.git.GitEnvironment. +/// +public sealed class GitEnvironment +{ + private readonly ImmutableDictionary _environment; + private readonly bool _noGitPrompt; + + public GitEnvironment(IReadOnlyDictionary environment) + : this(environment, noGitPrompt: false) + { + } + + public GitEnvironment(IReadOnlyDictionary environment, bool noGitPrompt) + { + _environment = Preconditions.CheckNotNull(environment).ToImmutableDictionary(); + _noGitPrompt = noGitPrompt; + } + + /// + /// Returns the environment to pass to git subprocesses. Explicitly forces the output language to + /// english so parsing of git's output succeeds independently of the user's default locale. + /// + public ImmutableDictionary GetEnvironment() + { + var env = new Dictionary(_environment); + + // Explicitly set output language to english so parsing of git's output succeeds + // independently of users default locale. + env["LANG"] = "en_US.UTF-8"; + + if (_noGitPrompt) + { + env["GIT_TERMINAL_PROMPT"] = "0"; + } + + return env.ToImmutableDictionary(); + } + + /// + /// Returns a copy of this environment, with the given vars added. If a key is already present, + /// its value is overwritten. + /// + public GitEnvironment WithVars(IReadOnlyDictionary vars) + { + var allVars = new Dictionary(_environment); + foreach (var kvp in vars) + { + allVars[kvp.Key] = kvp.Value; + } + return new GitEnvironment(allVars, _noGitPrompt); + } + + /// + /// Returns a copy of this environment, setting explicitly to prevent Git from asking for + /// username/password and fail if the credentials cannot be resolved. + /// + public GitEnvironment WithNoGitPrompt() => new(_environment, noGitPrompt: true); + + /// + /// Returns a String representing the git binary to be executed. + /// + /// The env var GIT_EXEC_PATH determines where Git looks for its sub-programs, but + /// also the regular git binaries (git, git-upload-pack, etc) are duplicated in + /// GIT_EXEC_PATH. + /// + /// If the env var is not set, then we will execute "git", that it will be resolved in the + /// path as usual. + /// + public string ResolveGitBinary() + { + if (_environment.TryGetValue("GIT_EXEC_PATH", out var execPath)) + { + return Path.Combine(execPath, "git"); + } + return "git"; + } +} diff --git a/src/Copybara.Core/Git/GitRepoType.cs b/src/Copybara.Core/Git/GitRepoType.cs new file mode 100644 index 000000000..e6d15f6e5 --- /dev/null +++ b/src/Copybara.Core/Git/GitRepoType.cs @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Copybara.Common; + +// Domain 'Console' collides with System.Console; qualify. +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Git; + +/// +/// Git repository type. Knowing the repository type allows us to provide a better experience, like +/// allowing to import GitHub PR / Gerrit changes using the web url as the reference. Port of +/// com.google.copybara.git.GitRepoType. +/// +/// NOTE: The GitHub / GitLab / Gerrit resolution paths are owned by peer files +/// (Gerrit*/GitHub*/GitLab*) that are being ported separately. Until those land, those repo types +/// delegate to the standard GIT resolution. The GIT path (the common case) is fully ported. +/// +public enum GitRepoType +{ + /// A standard git repository. This is the default. + Git, + + /// A git repository hosted in GitHub. + GitHub, + + /// A git repository hosted in GitLab. + GitLab, + + /// A Gerrit code review repository. + Gerrit, +} + +/// Resolution logic for . +public static class GitRepoTypeMethods +{ + private static readonly Regex GitUrl = new( + @"(\w+://)(.+@)*([\w.]+)(:[\d]+)?/*(.*)", RegexOptions.Compiled); + + private static readonly Regex FileUrl = new("file://(.*)", RegexOptions.Compiled); + + /// Example: "54d2a09b272f22a6d27e76b891f36213b98e0ddc random text" + private static readonly Regex Sha1WithReviewData = new( + "^(?:[a-f0-9]{40}|[a-f0-9]{64}) (.+)$", RegexOptions.Compiled); + + private static readonly Regex Sha1WithReviewDataCapture = new( + "^((?:[a-f0-9]{40}|[a-f0-9]{64})) (.+)$", RegexOptions.Compiled); + + /// + /// Resolve a reference for the given repo type. Currently GitHub/GitLab/Gerrit delegate to the + /// standard GIT resolution (their specialized handling is owned by peer files). + /// + public static GitRevision ResolveRef( + this GitRepoType type, + GitRepository repository, + string repoUrl, + string @ref, + GeneralOptions generalOptions, + bool describeVersion, + bool partialFetch, + int? fetchDepth) + { + // TODO(peer): GitHub PR / GitLab MR / Gerrit change resolution is provided by the + // Gerrit*/GitHub*/GitLab* files being ported separately. Until then, all types fall through + // to the standard GIT resolution below. + return ResolveGitRef( + repository, repoUrl, @ref, generalOptions, describeVersion, partialFetch, fetchDepth); + } + + /// + /// Standard git resolution. Supports SHA-1 references reachable from heads, valid git refs, and + /// fetching HEAD or a reference from a git url. + /// + private static GitRevision ResolveGitRef( + GitRepository repository, + string repoUrl, + string @ref, + GeneralOptions generalOptions, + bool describeVersion, + bool partialFetch, + int? fetchDepth) + { + Match sha1WithPatchSet = Sha1WithReviewDataCapture.Match(@ref); + if (sha1WithPatchSet.Success) + { + GitRevision rev = repository.FetchSingleRefWithTags( + repoUrl, + sha1WithPatchSet.Groups[1].Value, + fetchTags: describeVersion, + partialFetch, + fetchDepth); + return new GitRevision( + repository, + rev.GetHash(), + sha1WithPatchSet.Groups[2].Value, + rev.ContextReference(), + rev.AssociatedLabels(), + repoUrl); + } + + if (!GitUrl.IsMatch(@ref) && !FileUrl.IsMatch(@ref)) + { + // If ref is not a url try a normal fetch of repoUrl and ref. + return FetchFromUrl( + repository, repoUrl, @ref, describeVersion, partialFetch, fetchDepth); + } + + Console console = generalOptions.GetConsole(); + console.Warn("Git origin URL overwritten in the command line as " + @ref); + console.Progress("Fetching HEAD for " + @ref); + + int spaceIdx = @ref.LastIndexOf(' '); + // Treat "http://someurl ref" as a url and a reference. + if (spaceIdx != -1) + { + return FetchFromUrl( + repository, + @ref.Substring(0, spaceIdx), + @ref.Substring(spaceIdx + 1), + describeVersion, + partialFetch, + fetchDepth); + } + return FetchFromUrl(repository, @ref, "HEAD", describeVersion, partialFetch, fetchDepth); + } + + private static GitRevision FetchFromUrl( + GitRepository repository, + string repoUrl, + string @ref, + bool describeVersion, + bool partialFetch, + int? fetchDepth) => + repository.FetchSingleRefWithTags( + repoUrl, @ref, fetchTags: describeVersion, partialFetch, fetchDepth); +} diff --git a/src/Copybara.Core/Git/GitRepository.cs b/src/Copybara.Core/Git/GitRepository.cs new file mode 100644 index 000000000..996369bf5 --- /dev/null +++ b/src/Copybara.Core/Git/GitRepository.cs @@ -0,0 +1,2686 @@ +/* + * Copyright (C) 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; + +namespace Copybara.Git; + +/// +/// A class for manipulating Git repositories. Port of +/// com.google.copybara.git.GitRepository. +/// +/// Most operations are backed by the git binary invoked through +/// , which mirrors the upstream Java implementation (which shells out for +/// nearly every operation). LibGit2Sharp is available (referenced by the project) for the few +/// operations where it maps cleanly. +/// +public class GitRepository +{ + public static readonly TimeSpan DefaultRepoTimeout = TimeSpan.FromMinutes(15); + + // TODO: Make this generic (Using URIish). + private static readonly Regex FullUri = new( + "([a-z][a-z0-9+-]+@[a-zA-Z0-9_.-]+(:.+)?|^[a-z][a-z0-9+-]+://.*)$", RegexOptions.Compiled); + + private static readonly Regex LsTreeElement = new( + @"([0-9]{6}) (commit|tag|tree|blob) ([a-f0-9]{40,64})\t(.*)", RegexOptions.Compiled); + + private static readonly Regex LsRemoteOutputLine = new( + @"([a-f0-9]{40,64}|ref: refs/heads/\S+)\t(.+)", RegexOptions.Compiled); + + private static readonly Regex HashPattern = new("^[a-f0-9]{6,64}$", RegexOptions.Compiled); + + private static readonly Regex CompleteHashPattern = new( + "^(?:[a-f0-9]{40}|[a-f0-9]{64})$", RegexOptions.Compiled); + + private static readonly Regex DefaultBranchPattern = new( + @"ref: (refs/heads/(\S+))", RegexOptions.Compiled | RegexOptions.Singleline); + + // Pattern for matching URLs with a scheme, such as http:// or rpc://. + private static readonly Regex UrlWithSchemePattern = new( + "^([a-z][a-z0-9+-]+://)(.*)$", RegexOptions.Compiled); + + private static readonly Regex FailedRebase = new( + "(Failed to merge in the changes|Could not apply.*)", RegexOptions.Compiled); + + private static readonly ImmutableArray RefNotFoundErrors = + [ + new Regex("pathspec '(.+)' did not match any file", RegexOptions.Compiled), + new Regex( + "ambiguous argument '(.+)': unknown revision or path not in the working tree", + RegexOptions.Compiled), + ]; + + private static readonly Regex FetchCannotResolveErrors = new( + "(fatal: [Cc]ouldn't find remote ref" + + "|no such remote ref" + + "|fatal: no matching remote head" + + "|upload-pack: not our ref" + + "|ERR want .+ not valid)", + RegexOptions.Compiled); + + private static readonly Regex NoGitRepository = new( + "does not appear to be a git repository", RegexOptions.Compiled); + + private static readonly Regex ProtectedBranch = new( + "([Pp]rotected branch hook declined)", RegexOptions.Compiled); + + private static readonly Regex ScpUriPattern = new( + @"^(?:([a-z][a-z0-9+-]+)@)?([a-zA-Z0-9_.-]+)([:/])([^/].*|/|/[^/].*)$", + RegexOptions.Compiled); + + /// Label to be used for marking the original revision id for migrated commits. + public const string GitOriginRevId = "GitOrigin-RevId"; + + // Git exits with 128 in several circumstances. For example failed rebase. + private static bool IsNonCrashErrorExitCode(int code) => (code >= 1 && code <= 10) || code == 128; + + private const int DefaultMaxLogLines = 4_000; + protected const int DefaultMaxLsRemoteLines = int.MaxValue; + + public const string GitDescribeRequestedVersion = "GIT_DESCRIBE_REQUESTED_VERSION"; + public const string GitDescribeChangeVersion = "GIT_DESCRIBE_CHANGE_VERSION"; + public const string GitDescribeFirstParent = "GIT_DESCRIBE_FIRST_PARENT"; + public const string GitSequentialRevisionNumber = "GIT_SEQUENTIAL_REVISION_NUMBER"; + public const string GitDescribeAbbrev = "GIT_DESCRIBE_ABBREV"; + public const string GitTagPointsAt = "GIT_TAG_POINTS_TO"; + public const string HttpPermissionDenied = "The requested URL returned error: 403"; + public const string FullRefNamespace = "_copybara_full_ref"; + public const string CopybaraFetchNamespace = "refs/copybara_fetch"; + + private readonly string _gitDir; + private readonly string? _workTree; + private readonly bool _verbose; + protected readonly GitEnvironment GitEnv; + private readonly TimeSpan _repoTimeout; + protected readonly PushOptionsValidator PushOptionsValidatorField; + protected readonly bool NoVerify; + + private static readonly Dictionary CharToStatusCode = + Enum.GetValues().ToDictionary(StatusCodeMethods.GetCode); + + protected GitRepository( + string gitDir, + string? workTree, + bool verbose, + GitEnvironment gitEnv, + TimeSpan repoTimeout, + bool noVerify, + PushOptionsValidator pushOptionsValidator) + { + _gitDir = Preconditions.CheckNotNull(gitDir); + _workTree = workTree; + _verbose = verbose; + GitEnv = Preconditions.CheckNotNull(gitEnv); + _repoTimeout = repoTimeout; + NoVerify = noVerify; + PushOptionsValidatorField = Preconditions.CheckNotNull(pushOptionsValidator); + } + + /// Creates a new repository in the given directory. The new repo is not bare. + public static GitRepository NewRepo( + bool verbose, string path, GitEnvironment gitEnv, TimeSpan repoTimeout, bool noVerify) => + new( + Path.Combine(path, ".git"), + path, + verbose, + gitEnv, + repoTimeout, + noVerify, + new PushOptionsValidator(null)); + + /// Creates a new repository in the given directory. The new repo is not bare. + public static GitRepository NewRepo( + bool verbose, + string path, + GitEnvironment gitEnv, + TimeSpan repoTimeout, + bool noVerify, + PushOptionsValidator pushOptionsValidator) => + new(Path.Combine(path, ".git"), path, verbose, gitEnv, repoTimeout, noVerify, + pushOptionsValidator); + + /// Creates a new repository with a default repo timeout. The new repo is not bare. + public static GitRepository NewRepo(bool verbose, string path, GitEnvironment gitEnv) => + NewRepo(verbose, path, gitEnv, DefaultRepoTimeout, noVerify: false); + + /// Create a new bare repository. + public static GitRepository NewBareRepo( + string gitDir, GitEnvironment gitEnv, bool verbose, TimeSpan repoTimeout, bool noVerify) => + new(gitDir, null, verbose, gitEnv, repoTimeout, noVerify, new PushOptionsValidator(null)); + + /// Create a new bare repository with a push options validator. + public static GitRepository NewBareRepo( + string gitDir, + GitEnvironment gitEnv, + bool verbose, + TimeSpan repoTimeout, + bool noVerify, + PushOptionsValidator pushOptionsValidator) => + new(gitDir, null, verbose, gitEnv, repoTimeout, noVerify, pushOptionsValidator); + + /// + /// Get the version of git that will be used. Returns null if git cannot be found. + /// + private static string? Version(GitEnvironment gitEnv) + { + try + { + return ExecuteGit( + Directory.GetCurrentDirectory(), + new[] { "version" }, + gitEnv, + verbose: false, + timeout: null).GetStdout(); + } + catch (CommandException) + { + return null; + } + } + + /// Validate that a refspec is valid. + /// if the refspec is not valid + internal static void ValidateRefSpec(GitEnvironment gitEnv, string cwd, string refspec) + { + if (QuickRefspecValidation(refspec)) + { + return; + } + try + { + ExecuteGit( + cwd, + new[] { "check-ref-format", "--allow-onelevel", "--refspec-pattern", refspec }, + gitEnv, + verbose: false, + timeout: null); + } + catch (CommandException) + { + string? version = Version(gitEnv); + throw new InvalidRefspecException( + version != null + ? "Invalid refspec: " + refspec + : $"Cannot find git binary at '{gitEnv.ResolveGitBinary()}'"); + } + } + + private static readonly Regex BasicRefspecComponent = new( + "^[A-Za-z0-9_-][A-Za-z0-9_.-]*$", RegexOptions.Compiled); + + /// + /// Skip calling the CLI for common refspecs that we know are safe. Returning false is always + /// safe (but less optimal). + /// + private static bool QuickRefspecValidation(string refspec) + { + if (!refspec.StartsWith("refs/", StringComparison.Ordinal) + || refspec.EndsWith('.') + || refspec.Contains("..", StringComparison.Ordinal) + || refspec.EndsWith(".lock", StringComparison.Ordinal)) + { + return false; + } + bool wildcard = false; + foreach (var component in refspec.Split('/')) + { + if (component == "*") + { + if (wildcard) + { + return false; + } + wildcard = true; + continue; + } + if (!BasicRefspecComponent.IsMatch(component)) + { + return false; + } + } + return true; + } + + /// Fetch a reference from a git url. + public GitRevision FetchSingleRef(string url, string @ref, bool partialFetch, int? depth) => + FetchSingleRefWithTags(url, @ref, fetchTags: false, partialFetch, depth); + + public GitRevision FetchSingleRefWithTags( + string url, string @ref, bool fetchTags, bool partialFetch, int? depth) + { + if (@ref.Contains(':') || @ref.Contains('*')) + { + throw new CannotResolveRevisionException( + "Fetching refspecs that contain local ref path locations or wildcards is not" + + " supported. Invalid ref: " + @ref); + } + bool isHashRef = IsHashReference(@ref); + if (isHashRef) + { + bool tags = !partialFetch && fetchTags; + try + { + Fetch(url, prune: false, force: true, Array.Empty(), partialFetch, depth, tags); + } + catch (CannotResolveRevisionException) + { + // Some servers are configured without HEAD. That is fine, we'll try fetching the SHA. + } + try + { + return ResolveReferenceWithContext(@ref, contextRef: @ref, url); + } + catch (Exception e) when (e is RepoException or CannotResolveRevisionException) + { + // Ignore, the fetch below will attempt using the SHA-1. + } + } + + var refspec = new List + { + $"{@ref}:{CopybaraFetchNamespace}/{@ref}", + }; + if (fetchTags) + { + refspec.Add("refs/tags/*:refs/tags/*"); + } + + if (!@ref.StartsWith("refs/", StringComparison.Ordinal)) + { + var fullRefspec = new List(refspec); + if (!isHashRef) + { + fullRefspec.Add( + $"refs/*/{@ref}:{CopybaraFetchNamespace}/refs/*/{@ref}{FullRefNamespace}"); + } + + try + { + Fetch(url, prune: false, force: true, fullRefspec, partialFetch, depth, tags: false); + return ResolveReferenceWithContext( + $"{CopybaraFetchNamespace}/{@ref}", contextRef: @ref, url); + } + catch (Exception e) when (e is RepoException or CannotResolveRevisionException) + { + // Ignore, the fetch below will attempt using a simpler refspec. + } + } + + Fetch(url, prune: false, force: true, refspec, partialFetch, depth, tags: false); + return ResolveReferenceWithContext( + $"{CopybaraFetchNamespace}/{@ref}", contextRef: @ref, url); + } + + public GitRevision AddDescribeVersion(GitRevision rev) + { + var describeLabels = ImmutableListMultimap.CreateBuilder(); + describeLabels.Put(GitDescribeRequestedVersion, Describe(rev, false)!); + describeLabels.Put(GitDescribeFirstParent, Describe(rev, true)); + string? describeAbbrev = DescribeAbbrev(rev); + if (describeAbbrev != null) + { + describeLabels.Put(GitDescribeAbbrev, describeAbbrev); + } + return rev.WithLabels(describeLabels.Build()); + } + + internal string? Describe(GitRevision rev, bool fallback, params string[] arg) + { + try + { + var args = new List { "describe" }; + args.AddRange(arg); + args.Add("--"); + args.Add(rev.GetHash()); + return SimpleCommand(args).GetStdout().Trim(); + } + catch (RepoException) + { + if (!fallback) + { + return null; + } + return SimpleCommand("describe", "--always", "--", rev.GetHash()).GetStdout().Trim(); + } + } + + public string Describe(GitRevision rev, bool firstParent) => + Describe(rev, true, firstParent ? new[] { "--first-parent" } : Array.Empty())!; + + /// Finds a tag that exactly points to the given revision. + public string? DescribeExactMatch(GitRevision rev) => + Describe(rev, false, "--exact-match", "--tags"); + + public string? DescribeAbbrev(GitRevision rev) + { + string? contextRef = rev.ContextReference(); + if (!string.IsNullOrEmpty(contextRef) + && !GitRevision.CompleteGitHashPattern.IsMatch(contextRef)) + { + try + { + foreach (var tag in TagPointsAt(rev)) + { + if (tag == contextRef) + { + return tag; + } + } + } + catch (RepoException) + { + // Cannot get `git tag --points-at`. Falling back to `git describe`. + } + } + return Describe(rev, false, "--tag", "--abbrev=0"); + } + + public IReadOnlyList TagPointsAt(GitRevision rev) => + SimpleCommand("tag", "--points-at", rev.GetHash()).GetStdout().Trim().Split('\n'); + + public string ShowDiff(string referenceFrom, string referenceTo) + { + Preconditions.CheckNotNull(referenceFrom, "Parameter referenceFrom should not be null"); + Preconditions.CheckNotNull(referenceTo, "Parameter referenceTo should not be null"); + return SimpleCommand("diff", referenceFrom, referenceTo).GetStdout(); + } + + /// Fetch zero or more refspecs in the local repository. + public FetchResult Fetch( + string url, + bool prune, + bool force, + IEnumerable refspecs, + bool partialFetch, + int? depth, + bool tags) + { + var args = new List { "fetch", ValidateUrl(url) }; + if (tags) + { + args.Add("--tags"); + } + if (depth.HasValue) + { + args.Add($"--depth={depth.Value}"); + } + if (partialFetch) + { + args.Add("--filter=blob:none"); + } + args.Add("--verbose"); + args.Add("--progress"); + if (prune) + { + args.Add("-p"); + } + if (force) + { + args.Add("-f"); + } + + var requestedRefs = new List(); + foreach (var @ref in refspecs) + { + Refspec refSpec = CreateRefSpec(@ref); + requestedRefs.Add(refSpec.GetOrigin()); + args.Add(@ref); + } + + var before = ShowRef(); + CommandOutputWithStatus output = GitAllowNonZeroExit(CommandRunner.NoInput, args, _repoTimeout); + if (output.GetTerminationStatus().Success()) + { + var after = ShowRef(); + return new FetchResult(before, after); + } + CheckFetchError( + output.GetStderr(), url, requestedRefs, output.GetTerminationStatus().GetExitCode()); + throw ThrowUnknownGitError(output, args); + } + + public void CheckFetchError( + string stdErr, string url, IReadOnlyList requestedRefs, int exitCode) + { + if (stdErr.Length == 0 || FetchCannotResolveErrors.IsMatch(stdErr)) + { + throw new CannotResolveRevisionException( + "Cannot find reference(s): [" + string.Join(", ", requestedRefs) + "]"); + } + if (NoGitRepository.IsMatch(stdErr)) + { + throw new CannotResolveRevisionException( + $"Invalid Git repository: {url}. Error: {stdErr}"); + } + if (stdErr.Contains("Server does not allow request for unadvertised object")) + { + throw new CannotResolveRevisionException($"{url}: {stdErr.Trim()}"); + } + if (stdErr.Contains("Permission denied") + || stdErr.Contains("Could not read from remote repository") + || stdErr.Contains(HttpPermissionDenied) + || stdErr.Contains("Repository not found")) + { + throw new AccessValidationException(stdErr); + } + } + + /// Create a refspec from a string. + public Refspec CreateRefSpec(string @ref) => Refspec.Create(GitEnv, _gitDir, @ref); + + public LogCmd Log(string referenceExpr) => LogCmd.Create(this, referenceExpr); + + public PushCmd Push() => + new( + this, + url: null, + ImmutableArray.Empty, + prune: false, + force: false, + ImmutableDictionary.Empty, + ImmutableArray.Empty, + PushOptionsValidatorField); + + public MergeCmd Merge(string branch, IReadOnlyList commits) => + MergeCmd.Create(this, branch, commits, _ => true); + + public TagCmd Tag(string tagName) => new(this, tagName, null, false); + + /// Runs a git ls-remote for a repository url from the current directory. + public static IReadOnlyDictionary LsRemote( + string url, ICollection refs, GitEnvironment gitEnv, int maxLogLines) => + LsRemote( + ".", url, refs, gitEnv, maxLogLines, DefaultMaxLsRemoteLines, Array.Empty()); + + /// Runs a git ls-remote for a repository url from the current directory. + public static IReadOnlyDictionary LsRemote( + string url, + ICollection refs, + GitEnvironment gitEnv, + ICollection flags, + int maxLsRemoteLimit) => + LsRemote(".", url, refs, gitEnv, DefaultMaxLogLines, maxLsRemoteLimit, flags); + + private static IReadOnlyDictionary LsRemote( + string cwd, + string url, + ICollection refs, + GitEnvironment gitEnv, + int maxLogLines, + int maxLsRemoteLimit, + ICollection flags) + { + var result = ImmutableDictionary.CreateBuilder(); + var args = new List { "ls-remote" }; + args.AddRange(flags); + try + { + args.Add(ValidateUrl(url)); + } + catch (ValidationException e) + { + throw new RepoException("Invalid url: " + url, e); + } + args.AddRange(refs); + + CommandOutputWithStatus output; + try + { + output = ExecuteGit(cwd, args, gitEnv, false, maxLogLines, null); + } + catch (BadExitStatusWithOutputException e) + { + string stderr = e.GetOutput().GetStderr(); + if (stderr.Contains("Please make sure you have the correct access rights") + || stderr.Contains(HttpPermissionDenied)) + { + throw new AccessValidationException( + $"Permission denied running ls-remote for '{url}' and refs" + + $" '[{string.Join(", ", refs)}]': Exit code" + + $" {e.GetOutput().GetTerminationStatus().GetExitCode()}, Output:\n{stderr}", + e); + } + throw new RepoException( + $"Error running ls-remote for '{url}' and refs '[{string.Join(", ", refs)}]':" + + $" Exit code {e.GetOutput().GetTerminationStatus().GetExitCode()}," + + $" Output:\n{stderr}", + e); + } + catch (CommandException e) + { + throw new RepoException( + $"Error running ls-remote for '{url}' and refs '[{string.Join(", ", refs)}]'", e); + } + + if (output.GetTerminationStatus().Success()) + { + int rowsAccumulated = 0; + foreach (var line in output.GetStdout().Split('\n')) + { + if (line.Length == 0) + { + continue; + } + if (maxLsRemoteLimit >= 0 && rowsAccumulated >= maxLsRemoteLimit) + { + break; + } + Match matcher = LsRemoteOutputLine.Match(line); + if (!matcher.Success) + { + throw new RepoException("Unexpected format for ls-remote output: " + line); + } + result[matcher.Groups[2].Value] = matcher.Groups[1].Value; + rowsAccumulated++; + if (DefaultBranchPattern.IsMatch(line)) + { + break; + } + } + } + return result.ToImmutable(); + } + + /// ls-remote using this repository environment and default max log lines. + public IReadOnlyDictionary LsRemote(string url, ICollection refs) => + LsRemote(url, refs, DefaultMaxLogLines); + + public IReadOnlyDictionary LsRemote( + string url, ICollection refs, int maxLogLines) => + LsRemote(url, refs, maxLogLines, Array.Empty()); + + public IReadOnlyDictionary LsRemote( + string url, ICollection refs, int maxLogLines, ICollection flags) => + LsRemote(GetCwd(), url, refs, GitEnv, maxLogLines, DefaultMaxLsRemoteLines, flags); + + public IReadOnlyDictionary LsRemote( + string url, ICollection refs, ICollection flags, int maxLsRemoteLimit) => + LsRemote(GetCwd(), url, refs, GitEnv, DefaultMaxLogLines, maxLsRemoteLimit, flags); + + public IReadOnlyDictionary LsRemote( + string url, ICollection refs, ICollection flags) => + LsRemote(GetCwd(), url, refs, GitEnv, DefaultMaxLogLines, DefaultMaxLsRemoteLines, flags); + + internal static string ValidateUrl(string url) + { + // support remote helper syntax ::
+ int sep = url.IndexOf("::", StringComparison.Ordinal); + if (sep >= 0 && url.IndexOf("::", sep + 2, StringComparison.Ordinal) < 0) + { + return url.Substring(0, sep) + "::" + ValidateUrl(url.Substring(sep + 2)); + } + + ValidationException.CheckCondition( + !url.StartsWith("http://", StringComparison.Ordinal), + "URL '%s' is not valid - should be using https.", + url); + if (FullUri.IsMatch(url)) + { + return url; + } + + // Support local folders + if (Directory.Exists(url)) + { + return url; + } + throw new RepoException($"URL '{url}' is not valid"); + } + + /// Execute show-ref and return a map from reference name to GitRevision(SHA-1). + public IReadOnlyDictionary ShowRef(IEnumerable refs) + { + var result = ImmutableDictionary.CreateBuilder(); + var args = new List { "show-ref" }; + args.AddRange(refs); + CommandOutput commandOutput = + GitAllowNonZeroExit(CommandRunner.NoInput, args, CommandRunner.DefaultTimeout); + + if (commandOutput.GetStderr().Length != 0) + { + throw new RepoException( + $"Error executing show-ref on {GetGitDir()} git repo:\n{commandOutput.GetStderr()}"); + } + + foreach (var line in commandOutput.GetStdout().Split('\n')) + { + if (line.Length == 0) + { + continue; + } + var strings = line.Split(' '); + Preconditions.CheckState( + strings.Length == 2 && CompleteHashPattern.IsMatch(strings[0]), + "Cannot parse line: '%s'", + line); + result[strings[1]] = new GitRevision(this, strings[0]); + } + return result.ToImmutable(); + } + + /// Execute show-ref and return a map from reference name to GitRevision(SHA-1). + public IReadOnlyDictionary ShowRef() => ShowRef(Array.Empty()); + + internal string MergeBase(string commit1, string commit2) => + SimpleCommand("merge-base", commit1, commit2).GetStdout().Trim(); + + internal bool IsAncestor(string ancestor, string commit) + { + CommandOutputWithStatus result = GitAllowNonZeroExit( + CommandRunner.NoInput, + new[] { "merge-base", "--is-ancestor", "--", ancestor, commit }, + CommandRunner.DefaultTimeout); + if (result.GetTerminationStatus().Success()) + { + return true; + } + if (result.GetTerminationStatus().GetExitCode() == 1) + { + return false; + } + throw new RepoException( + "Error executing git merge-base --is-ancestor:\n" + result.GetStderr()); + } + + /// Returns an instance equivalent to this one but with a different work tree. + public GitRepository WithWorkTree(string newWorkTree) => + new(_gitDir, newWorkTree, _verbose, GitEnv, _repoTimeout, NoVerify, PushOptionsValidatorField); + + /// The Git work tree. Returns null for bare repos. + public string? GetWorkTree() => _workTree; + + public string GetGitDir() => _gitDir; + + /// Can be overwritten to add custom behavior. + protected virtual string RunPush(PushCmd pushCmd) + { + var cmd = new List { "push", "--progress" }; + + foreach (var pushOption in pushCmd.PushOptions) + { + cmd.Add($"--push-option={pushOption}"); + } + if (pushCmd.Prune) + { + cmd.Add("--prune"); + } + if (pushCmd.Force) + { + cmd.Add("--force"); + } + foreach (var entry in pushCmd.ForceLease) + { + cmd.Add($"--force-with-lease={entry.Key}:{entry.Value}"); + } + if (NoVerify) + { + cmd.Add("--no-verify"); + } + if (pushCmd.Url != null) + { + cmd.Add(ValidateUrl(pushCmd.Url)); + foreach (var refspec in pushCmd.Refspecs) + { + cmd.Add(refspec.ToString()); + } + } + try + { + return SimpleCommand(_repoTimeout, cmd).GetStderr(); + } + catch (RepoException e) + { + if (e.Message.Contains(HttpPermissionDenied)) + { + throw new AccessValidationException("Permission error pushing to " + pushCmd.Url, e); + } + throw; + } + } + + /// git branch command. + public sealed class BranchCmd + { + private readonly GitRepository _repo; + private readonly string _name; + private readonly string? _startPoint; + + internal BranchCmd(GitRepository repo, string name, string? startPoint) + { + _repo = repo; + _name = Preconditions.CheckNotNull(name); + _startPoint = startPoint; + } + + /// Create the branch from this commit. If not set, it uses current HEAD. + public BranchCmd WithStartPoint(string startPoint) => + new(_repo, _name, Preconditions.CheckNotNull(startPoint)); + + public void Run() + { + var args = new List { "branch", _name }; + if (_startPoint != null) + { + args.Add(_startPoint); + } + _repo.SimpleCommand(args); + } + } + + public BranchCmd Branch(string name) => new(this, name, null); + + /// A class that represents 'git cherry-pick' command and options. + public sealed class CherryPickCmd + { + private readonly GitRepository _repo; + private readonly ImmutableArray _commits; + private readonly int? _parentNumber; + private readonly bool _addCommitOriginInfo; + private readonly bool _fastForward; + private readonly bool _allowEmpty; + + internal CherryPickCmd( + GitRepository repo, + ImmutableArray commits, + int? parentNumber, + bool addCommitOriginInfo, + bool fastForward, + bool allowEmpty) + { + _repo = repo; + _commits = commits; + _parentNumber = parentNumber; + _addCommitOriginInfo = addCommitOriginInfo; + _fastForward = fastForward; + _allowEmpty = allowEmpty; + } + + public CherryPickCmd ParentNumber(int parentNumber) => + new(_repo, _commits, parentNumber, _addCommitOriginInfo, _fastForward, _allowEmpty); + + public CherryPickCmd AddCommitOriginInfo(bool addCommitOriginInfo) => + new(_repo, _commits, _parentNumber, addCommitOriginInfo, _fastForward, _allowEmpty); + + public CherryPickCmd FastForward(bool fastForward) => + new(_repo, _commits, _parentNumber, _addCommitOriginInfo, fastForward, _allowEmpty); + + public CherryPickCmd AllowEmpty(bool allowEmpty) => + new(_repo, _commits, _parentNumber, _addCommitOriginInfo, _fastForward, allowEmpty); + + public void Run() + { + var args = new List { "cherry-pick" }; + if (_parentNumber != null) + { + args.Add("-m"); + args.Add(_parentNumber.Value.ToString(CultureInfo.InvariantCulture)); + } + if (_addCommitOriginInfo) + { + args.Add("-x"); + } + if (_fastForward) + { + args.Add("--ff"); + } + if (_allowEmpty) + { + args.Add("--allow-empty"); + } + args.AddRange(_commits); + _repo.SimpleCommand(args); + } + } + + public CherryPickCmd CherryPick(IEnumerable commits) => + new(this, commits.ToImmutableArray(), null, false, false, false); + + public void AbortCherryPick() => SimpleCommand("cherry-pick", "--abort"); + + /// An add command bound to the repo. + public sealed class AddCmd + { + private readonly GitRepository _repo; + private readonly bool _force; + private readonly bool _all; + private readonly IReadOnlyList _files; + private readonly string? _pathSpecFromFile; + + internal AddCmd( + GitRepository repo, + bool force, + bool all, + IReadOnlyList files, + string? pathSpecFromFile) + { + _repo = repo; + _force = force; + _all = all; + _files = Preconditions.CheckNotNull(files); + _pathSpecFromFile = pathSpecFromFile; + } + + public AddCmd Force() => new(_repo, true, _all, _files, _pathSpecFromFile); + + public AddCmd All() + { + Preconditions.CheckState(_files.Count == 0, "'all' and passing files is incompatible"); + Preconditions.CheckState( + _pathSpecFromFile == null, "'all' and pathSpecFromFile is incompatible"); + return new AddCmd(_repo, _force, true, _files, _pathSpecFromFile); + } + + public AddCmd Files(IEnumerable files) + { + Preconditions.CheckState(!_all, "'all' and passing files is incompatible"); + Preconditions.CheckState( + _pathSpecFromFile == null, "'pathSpecFromFile' and passing files is incompatible"); + return new AddCmd(_repo, _force, false, files.ToImmutableArray(), _pathSpecFromFile); + } + + public AddCmd PathSpecFromFile(string pathSpecFromFile) + { + Preconditions.CheckState(!_all, "'pathSpecFromFile' and passing files is incompatible"); + Preconditions.CheckState( + _files.Count == 0, "'pathSpecFromFile' and passing files is incompatible"); + return new AddCmd(_repo, _force, false, _files, pathSpecFromFile); + } + + public AddCmd Files(params string[] files) => Files((IEnumerable)files); + + public void Run() + { + var @params = new List { "add" }; + if (_force) + { + @params.Add("-f"); + } + if (_all) + { + @params.Add("--all"); + } + if (_pathSpecFromFile != null) + { + @params.Add("--pathspec-from-file=" + _pathSpecFromFile); + } + @params.Add("--"); + @params.AddRange(_files); + _repo.Git(_repo.GetCwd(), null, _repo.AddGitDirAndWorkTreeParams(@params)); + } + } + + public AddCmd Add() => new(this, false, false, Array.Empty(), null); + + /// Get a field from a configuration. + private string? GetConfigField(string field, string? configFile) + { + var @params = new List { "config" }; + if (configFile != null) + { + @params.Add("-f"); + @params.Add(configFile); + } + @params.Add("--get"); + @params.Add(field); + CommandOutputWithStatus @out = + GitAllowNonZeroExit(CommandRunner.NoInput, @params, CommandRunner.DefaultTimeout); + if (@out.GetTerminationStatus().Success()) + { + return @out.GetStdout().Trim(); + } + if (@out.GetTerminationStatus().GetExitCode() == 1 && @out.GetStderr().Length == 0) + { + return null; + } + throw new RepoException("Error executing git config:\n" + @out.GetStderr()); + } + + private IReadOnlySet GetSubmoduleNames() + { + if (!File.Exists(Path.Combine(GetCwd(), ".gitmodules"))) + { + return ImmutableHashSet.Empty; + } + var @params = new List { "config", "-f", ".gitmodules", "-l", "--name-only" }; + CommandOutputWithStatus @out = GitAllowNonZeroExit( + CommandRunner.NoInput, AddGitDirAndWorkTreeParams(@params), CommandRunner.DefaultTimeout); + if (@out.GetTerminationStatus().Success()) + { + var modules = new LinkedHashSet(); + foreach (var raw in @out.GetStdout().Trim().Split('\n')) + { + var line = raw.Trim(); + if (!line.StartsWith("submodule.", StringComparison.Ordinal)) + { + continue; + } + int lastDot = line.LastIndexOf('.'); + modules.Add(line.Substring( + "submodule.".Length, + (lastDot > 0 ? lastDot : line.Length) - "submodule.".Length)); + } + return modules.ToImmutableHashSet(); + } + if (@out.GetTerminationStatus().GetExitCode() == 1 && @out.GetStderr().Length == 0) + { + return ImmutableHashSet.Empty; + } + throw new RepoException("Error executing git config:\n" + @out.GetStderr()); + } + + /// Resolves a git reference to the SHA-1 reference. + public string ParseRef(string @ref) + { + CommandOutputWithStatus result = GitAllowNonZeroExit( + CommandRunner.NoInput, + new[] { "rev-list", "-1", @ref, "--" }, + CommandRunner.DefaultTimeout); + if (!result.GetTerminationStatus().Success()) + { + throw new CannotResolveRevisionException("Cannot find reference '" + @ref + "'"); + } + string sha1 = result.GetStdout().Trim(); + Preconditions.CheckState( + CompleteHashPattern.IsMatch(sha1), "Should be resolved to a complete hash: %s", sha1); + return sha1; + } + + internal bool RefExists(string @ref) + { + try + { + ParseRef(@ref); + return true; + } + catch (CannotResolveRevisionException) + { + return false; + } + } + + /// An object capable of performing a 'git rebase' operation. + public sealed class RebaseCmd + { + private readonly GitRepository _repo; + private readonly string? _branch; + private readonly string _upstream; + private readonly string? _into; + private readonly string? _errorAdvice; + + internal RebaseCmd( + GitRepository repo, + string upstream, + string? branch, + string? into, + string? errorAdvice) + { + _repo = repo; + _branch = branch; + _upstream = upstream; + _into = into; + _errorAdvice = errorAdvice; + } + + public RebaseCmd Branch(string branch) => + new(_repo, _upstream, branch, _into, _errorAdvice); + + public RebaseCmd Into(string into) => new(_repo, _upstream, _branch, into, _errorAdvice); + + public RebaseCmd ErrorAdvice(string errorAdvice) => + new(_repo, _upstream, _branch, _into, errorAdvice); + + /// Run 'git rebase'. + /// if there is a conflict + public void Run() + { + var cmd = new List { "rebase", _upstream }; + if (_branch != null) + { + cmd.Add(_branch); + } + if (_into != null) + { + cmd.Add("--into"); + cmd.Add(_into); + } + + CommandOutputWithStatus output = + _repo.GitAllowNonZeroExit(CommandRunner.NoInput, cmd, CommandRunner.DefaultTimeout); + if (output.GetTerminationStatus().Success()) + { + return; + } + if (FailedRebase.IsMatch(output.GetStderr())) + { + throw new RebaseConflictException( + $"Conflict detected while rebasing {_repo._workTree} to {_branch}. Please sync" + + " or update the change in the origin and retry. Git output was:\n" + + $"{output.GetStdout()}{(_errorAdvice != null ? ". " + _errorAdvice : "")}"); + } + throw new RepoException(output.GetStderr()); + } + } + + public RebaseCmd RebaseCmdFor(string upstream) => + new(this, Preconditions.CheckNotNull(upstream), null, null, null); + + /// Try to cherry pick a commit. If it fails, cherry-pick is aborted and false returned. + public bool TryToCherryPick(string commit) + { + try + { + SimpleCommand("cherry-pick", commit); + return true; + } + catch (RepoException) + { + try + { + AbortCherryPick(); + } + catch (RepoException) + { + // cherry-pick --abort failed. + } + return false; + } + } + + /// Return the branch name of HEAD. If it fails, return the sha1 of HEAD. + public GitRevision GetHeadRef() + { + try + { + string reference = GetPrimaryBranch(); + return new GitRevision( + this, ParseRef(reference), null, reference, + ImmutableListMultimap.Empty, null); + } + catch (RepoException) + { + return new GitRevision(this, ResolveReference("HEAD").GetHash()); + } + } + + /// Check whether the remote sha1's tree is the same as repo's HEAD. + public bool HasSameTree(string remoteCommit) + { + GitLogEntry newChange = Log("HEAD").WithLimit(1).Run()[^1]; + SimpleCommand("checkout", "-b", "cherry_pick" + Guid.NewGuid(), "HEAD~1"); + if (TryToCherryPick(remoteCommit)) + { + GitLogEntry oldWithCherryPick = Log("HEAD").WithLimit(1).Run()[^1]; + return oldWithCherryPick.Tree == newChange.Tree; + } + return false; + } + + /// Checks out the given ref, quietly and throwing away local changes. + public CommandOutput ForceCheckout( + string @ref, IReadOnlySet checkoutPaths, TimeSpan commandTimeout) => + ForceCheckout(@ref, commandTimeout, new[] { "-q", "-f" }, checkoutPaths); + + /// Checks out the given ref, quietly and throwing away local changes. + public CommandOutput ForceCheckout(string @ref) => ForceCheckout(@ref, (TimeSpan?)null); + + /// Checks out the given ref, quietly and throwing away local changes. + public CommandOutput ForceCheckout(string @ref, TimeSpan? commandTimeout) => + ForceCheckout(@ref, commandTimeout, new[] { "-q", "-f" }, ImmutableHashSet.Empty); + + private CommandOutput ForceCheckout( + string @ref, + TimeSpan? commandTimeout, + IReadOnlyList checkoutArgs, + IReadOnlySet checkoutPaths) + { + Preconditions.CheckArgument( + !string.IsNullOrEmpty(@ref), + "Expected a non-empty ref for force checkout but got '%s'", + @ref); + + var argv = new List { "checkout" }; + argv.AddRange(checkoutArgs); + argv.Add(@ref); + argv.AddRange(checkoutPaths.Where(e => e.Length != 0)); + + return SimpleCommand(commandTimeout, argv); + } + + /// Set the sparse checkout. + public CommandOutput SetSparseCheckout(IReadOnlySet checkoutPaths) + { + var argv = new List { "sparse-checkout", "set" }; + argv.AddRange(checkoutPaths.Where(s => s.Length != 0)); + argv.Add("--cone"); + return SimpleCommand(argv); + } + + // Git's ISO8601 format does not deal with subseconds. + private const string IsoOffsetDateTimeNoSubseconds = "yyyy-MM-dd HH:mm:sszzz"; + + // The effective bytes for command-line arguments is ~128k. Setting an arbitrary max of 64k. + private const int ArbitraryMaxArgSize = 64_000; + + public void Commit(string author, DateTimeOffset timestamp, string message) => + Commit(Preconditions.CheckNotNull(author), amend: false, timestamp, + Preconditions.CheckNotNull(message)); + + public void Commit(string? author, bool amend, DateTimeOffset? timestamp, string message) + { + if (IsEmptyStaging() && !amend) + { + string baseline = "unknown"; + try + { + baseline = ParseRef("HEAD"); + } + catch (Exception e) when (e is CannotResolveRevisionException or RepoException) + { + // Cannot find baseline. + } + throw new EmptyChangeException( + $"Migration of the revision resulted in an empty change from baseline" + + $" '{baseline}'.\nIs the change already migrated?"); + } + + var @params = new List { "commit" }; + if (author != null) + { + @params.Add("--author"); + @params.Add(author); + } + if (timestamp != null) + { + @params.Add("--date"); + @params.Add(timestamp.Value.ToString( + IsoOffsetDateTimeNoSubseconds, CultureInfo.InvariantCulture)); + } + if (amend) + { + @params.Add("--amend"); + } + if (NoVerify) + { + @params.Add("--no-verify"); + } + string? descriptionFile = null; + try + { + if (Encoding.UTF8.GetByteCount(message) > ArbitraryMaxArgSize) + { + descriptionFile = Path.Combine(GetCwd(), Guid.NewGuid() + ".desc"); + File.WriteAllBytes(descriptionFile, Encoding.UTF8.GetBytes(message)); + @params.Add("-F"); + @params.Add(Path.GetFullPath(descriptionFile)); + } + else + { + @params.Add("-m"); + @params.Add(message); + } + Git(GetCwd(), null, AddGitDirAndWorkTreeParams(@params)); + } + catch (IOException e) + { + throw new RepoException( + "Could not commit change: Failed to write file " + descriptionFile, e); + } + finally + { + try + { + if (descriptionFile != null && File.Exists(descriptionFile)) + { + File.Delete(descriptionFile); + } + } + catch (IOException) + { + // Could not delete description file. + } + } + } + + /// Check if staging is empty (a commit would fail with EmptyChangeException). + private bool IsEmptyStaging() + { + CommandOutput status = SimpleCommand("diff", "--staged", "--stat"); + return status.GetStdout().Trim().Length == 0; + } + + public IReadOnlyList Status() + { + CommandOutput output = Git( + GetCwd(), null, AddGitDirAndWorkTreeParams(new[] { "status", "--porcelain" })); + var builder = new List(); + foreach (var line in output.GetStdout().Split('\n')) + { + if (line.Length == 0) + { + continue; + } + // Format 'XY file (-> file)?' + var rest = line.Substring(3); + int arrow = rest.IndexOf(" -> ", StringComparison.Ordinal); + string fileName; + string? newFileName; + if (arrow < 0) + { + fileName = rest; + newFileName = null; + } + else + { + fileName = rest.Substring(0, arrow); + newFileName = rest.Substring(arrow + 4); + } + builder.Add(new StatusFile( + fileName, newFileName, ToStatusCode(line[0]), ToStatusCode(line[1]))); + } + return builder; + } + + private StatusCode ToStatusCode(char c) => + CharToStatusCode.TryGetValue(c, out var code) + ? code + : throw new InvalidOperationException($"Cannot find status code for '{c}'"); + + /// Find submodules information for the current repository. + internal IEnumerable ListSubmodules(string currentRemoteUrl, GitRevision @ref) + { + var result = new List(); + foreach (var submoduleName in GetSubmoduleNames()) + { + string? path = GetSubmoduleField(submoduleName, "path"); + if (path == null) + { + throw new RepoException("Path is required for submodule " + submoduleName); + } + string? url = GetSubmoduleField(submoduleName, "url"); + if (url == null) + { + throw new RepoException("Url is required for submodule " + submoduleName); + } + string? branch = GetSubmoduleField(submoduleName, "branch"); + if (branch != null && branch == ".") + { + branch = @ref.ContextReference(); + } + FileUtil.CheckNormalizedRelative(path); + if (url.StartsWith("../", StringComparison.Ordinal) + || url.StartsWith("./", StringComparison.Ordinal)) + { + url = ResolveRelativeUrl(currentRemoteUrl, submoduleName, url); + } + try + { + result.Add(new Submodule(ValidateUrl(url), submoduleName, branch, path)); + } + catch (ValidationException e) + { + throw new RepoException("Invalid url: " + url, e); + } + } + return result; + } + + internal IReadOnlyList LsTree( + GitRevision reference, string? treeish, bool recursive, bool fullName) + { + var result = new List(); + var args = new List { "ls-tree", reference.GetHash() }; + if (recursive) + { + args.Add("-r"); + } + if (fullName) + { + args.Add("--full-name"); + } + args.Add("-z"); + if (treeish != null) + { + args.Add("--"); + args.Add(treeish); + } + + string stdout = SimpleCommand(args).GetStdout(); + foreach (var line in stdout.Split('\0')) + { + if (line.Length == 0) + { + continue; + } + Match matcher = LsTreeElement.Match(line); + if (!matcher.Success) + { + throw new RepoException("Unexpected format for ls-tree output: " + line); + } + string mode = matcher.Groups[1].Value; + GitObjectType objectType = Enum.Parse(matcher.Groups[2].Value, true); + string sha1 = matcher.Groups[3].Value; + string path = matcher.Groups[4].Value; + result.Add(new TreeElement(objectType, sha1, path, mode)); + } + return result; + } + + private string ResolveRelativeUrl( + string currentRemoteUrl, string submoduleName, string relativeUrl) + { + Match scpUrl = ScpUriPattern.Match(currentRemoteUrl); + if (scpUrl.Success) + { + string? user = scpUrl.Groups[1].Success ? scpUrl.Groups[1].Value : null; + string separator = scpUrl.Groups[3].Value; + // '/' separator with no user means it's not an scp url. + if (!(separator == "/" && user == null)) + { + return ResolveRelativeScpUrl( + user, scpUrl.Groups[2].Value, scpUrl.Groups[4].Value, submoduleName, relativeUrl); + } + } + return ResolveRelativeStandardUrl(currentRemoteUrl, submoduleName, relativeUrl); + } + + private string ResolveRelativeStandardUrl( + string currentRemoteUrl, string submoduleName, string relativeUrl) + { + Match matcher = UrlWithSchemePattern.Match(currentRemoteUrl); + if (!matcher.Success) + { + throw new RepoException("Cannot resolve relative URL for: " + currentRemoteUrl); + } + string scheme = matcher.Groups[1].Value; + string path = matcher.Groups[2].Value; + var traversable = path.Split('/').Where(s => s.Length != 0).ToList(); + string resolved = ResolveRelativeSegments(traversable, relativeUrl, submoduleName); + return scheme + (path.StartsWith('/') ? "/" : "") + resolved; + } + + private string ResolveRelativeScpUrl( + string? user, string host, string path, string submoduleName, string relativeUrl) + { + var traversable = path.Split('/').Where(s => s.Length != 0).ToList(); + string resolved = ResolveRelativeSegments(traversable, relativeUrl, submoduleName); + return (user != null ? user + "@" : "") + + host + + ":" + + (path.StartsWith('/') ? "/" : "") + + resolved; + } + + private string ResolveRelativeSegments( + IReadOnlyList baseSegments, string relativeUrl, string submoduleName) + { + var segments = new List(baseSegments); + foreach (var part in relativeUrl.Split('/').Where(s => s.Length != 0)) + { + if (part == ".") + { + continue; + } + if (part == "..") + { + if (segments.Count == 0) + { + throw new RepoException( + $"Cannot resolve relative url '{relativeUrl}' for submodule" + + $" '{submoduleName}': navigating above root"); + } + segments.RemoveAt(segments.Count - 1); + } + else + { + segments.Add(part); + } + } + return string.Join("/", segments); + } + + private string? GetSubmoduleField(string submoduleName, string field) => + GetConfigField($"submodule.{submoduleName}.{field}", ".gitmodules"); + + private string GetCwd() => _workTree ?? _gitDir; + + private IReadOnlyList AddGitDirAndWorkTreeParams(IEnumerable argv) + { + Preconditions.CheckState( + Directory.Exists(_gitDir), + "git repository dir '%s' doesn't exist or is not a directory", + _gitDir); + var allArgv = new List { "--git-dir=" + _gitDir }; + if (_workTree != null) + { + allArgv.Add("--work-tree=" + _workTree); + } + allArgv.AddRange(argv); + return allArgv; + } + + /// Initializes the repository. + public GitRepository Init() => Init(null); + + /// Initializes the repository with the specified object format. + public GitRepository Init(GitHashAlgorithm? objectFormat) + { + try + { + Directory.CreateDirectory(_gitDir); + if (_workTree != null) + { + Directory.CreateDirectory(_workTree); + } + } + catch (IOException e) + { + throw new RepoException("Cannot create directories: " + e.Message, e); + } + var args = new List { "init" }; + if (objectFormat != null) + { + args.Add("--object-format=" + objectFormat.Value.ToString().ToLowerInvariant()); + } + if (_workTree != null && Path.Combine(_workTree, ".git") == _gitDir) + { + args.Add("."); + Git(_workTree, null, args); + } + else + { + args.Add("--bare"); + Git(_gitDir, null, args); + } + return this; + } + + /// Returns whether the repository is initialized. + public bool IsInitialized() => + File.Exists(Path.Combine(_gitDir, "HEAD")) + || File.Exists(Path.Combine(_gitDir, ".git", "HEAD")); + + /// Returns the object format of the remote repository (sha1 or sha256). + public GitHashAlgorithm GetRemoteObjectFormat(string fetchUrl) + { + CommandOutputWithStatus output; + try + { + output = ExecuteGit( + GetCwd(), + new[] { "ls-remote", fetchUrl }, + GitEnv.WithVars(new Dictionary { ["GIT_TRACE_PACKET"] = "1" }), + verbose: false, + maxLogLines: DefaultMaxLogLines, + timeout: TimeSpan.FromSeconds(10)); + } + catch (CommandException e) + { + throw new RepoException("Cannot get remote object format for " + fetchUrl, e); + } + string stderr = output.GetStderr(); + return stderr.Contains("object-format=sha256") + ? GitHashAlgorithm.Sha256 + : GitHashAlgorithm.Sha1; + } + + public GitRepository WithCredentialHelper(string credentialHelper) + { + ReplaceLocalConfigField("credential", "helper", Preconditions.CheckNotNull(credentialHelper)); + return this; + } + + public GitRepository WithHttpFollowRedirectsOption(string option) + { + ReplaceLocalConfigField("http", "followRedirects", Preconditions.CheckNotNull(option)); + return this; + } + + public void ReplaceLocalConfigField(string category, string field, string value) => + SimpleCommand("config", "--replace-all", "--local", $"{category}.{field}", value); + + public GitRepository EnablePartialFetch() + { + try + { + SimpleCommand("config", "core.repositoryFormatVersion", "1"); + SimpleCommand("config", "extensions.partialClone", "origin"); + } + catch (Exception) + { + // Partial Clone not supported; ignore. + } + return this; + } + + public void SetRemoteOriginUrl(string url) + { + try + { + SimpleCommand("config", "remote.origin.url", url); + } + catch (RepoException) + { + // Ignore. + } + } + + public GitCredential.UserPassword CredentialFill(string url) => + new GitCredential(TimeSpan.FromMinutes(1), GitEnv).Fill(_gitDir, url); + + /// Runs a git command with --git-dir and --work-tree set. + public CommandOutput SimpleCommand(TimeSpan? timeout, params string[] argv) => + SimpleCommand(timeout, (IReadOnlyList)argv); + + public CommandOutput SimpleCommand(params string[] argv) => + SimpleCommand((IReadOnlyList)argv); + + public CommandOutput SimpleCommand(TimeSpan? timeout, IReadOnlyList argv) => + Git(GetCwd(), timeout, AddGitDirAndWorkTreeParams(argv)); + + public CommandOutput SimpleCommand(IReadOnlyList argv) => + Git(GetCwd(), null, AddGitDirAndWorkTreeParams(argv)); + + internal CommandOutput SimpleCommandNoRedirectOutput(params string[] argv) + { + var @params = AddGitDirAndWorkTreeParams(argv); + try + { + return ExecuteGit(GetCwd(), @params, GitEnv, verbose: false, maxLogLines: 0, timeout: null); + } + catch (BadExitStatusWithOutputException e) + { + CommandOutputWithStatus output = e.GetOutput(); + foreach (var error in RefNotFoundErrors) + { + Match matcher = error.Match(output.GetStderr()); + if (matcher.Success) + { + throw new RepoException("Cannot find reference '" + matcher.Groups[1].Value + "'"); + } + } + throw ThrowUnknownGitError(output, @params); + } + catch (CommandException e) + { + throw new RepoException("Error executing 'git': " + e.Message, e); + } + } + + internal void ForceClean() + { + Preconditions.CheckNotNull( + _workTree, "Clean only acts on the worktree. A worktree is needed"); + SimpleCommand("clean", "-f", "-d"); + } + + /// Execute git apply. + /// if it cannot apply the change + public void Apply(byte[] stdin, bool index) + { + CommandOutputWithStatus output = GitAllowNonZeroExit( + stdin, + index ? new[] { "apply", "--index" } : new[] { "apply" }, + CommandRunner.DefaultTimeout); + if (output.GetTerminationStatus().Success()) + { + return; + } + if (output.GetTerminationStatus().GetExitCode() == 1) + { + throw new RebaseConflictException("Couldn't apply patch:\n" + output.GetStderr()); + } + throw new RepoException("Couldn't apply patch:\n" + output.GetStderr()); + } + + /// Invokes git in cwd and returns the output if successful. + public CommandOutput Git(string cwd, params string[] @params) => + Git(cwd, null, @params); + + protected CommandOutput Git(string cwd, TimeSpan? timeout, IEnumerable @params) + { + try + { + return ExecuteGit(cwd, @params, GitEnv, _verbose, timeout); + } + catch (BadExitStatusWithOutputException e) + { + CommandOutputWithStatus output = e.GetOutput(); + foreach (var error in RefNotFoundErrors) + { + Match matcher = error.Match(output.GetStderr()); + if (matcher.Success) + { + throw new RepoException("Cannot find reference '" + matcher.Groups[1].Value + "'"); + } + } + throw ThrowUnknownGitError(output, @params); + } + catch (CommandException e) + { + throw new RepoException("Error executing 'git': " + e.Message, e); + } + } + + private RepoException ThrowUnknownGitError( + CommandOutputWithStatus output, IEnumerable @params) => + throw new RepoException( + $"Error executing 'git {string.Join(' ', @params)}'(exit code" + + $" {output.GetTerminationStatus().GetExitCode()}). Stderr: {output.GetStderr()}\n"); + + /// + /// Execute git allowing program non-zero exit codes (0-10 and 128). Still fails for exit codes + /// like 127 (Command not found). + /// + protected CommandOutputWithStatus GitAllowNonZeroExit( + byte[] stdin, IEnumerable @params, TimeSpan defaultTimeout) => + GitAllowNonZeroExit(stdin, @params, defaultTimeout, -1); + + protected CommandOutputWithStatus GitAllowNonZeroExit( + byte[] stdin, IEnumerable @params, TimeSpan defaultTimeout, int maxLogLines) + { + try + { + var allParams = new List { GitEnv.ResolveGitBinary() }; + allParams.AddRange(AddGitDirAndWorkTreeParams(@params)); + var cmd = new Command(allParams.ToArray(), GitEnv.GetEnvironment(), GetCwd()); + var runner = new CommandRunner(cmd, defaultTimeout) + .WithVerbose(_verbose) + .WithInput(stdin); + if (maxLogLines != -1) + { + runner = runner.WithMaxStdOutLogLines(maxLogLines); + } + return runner.Execute(); + } + catch (BadExitStatusWithOutputException e) + { + CommandOutputWithStatus output = e.GetOutput(); + int exitCode = output.GetTerminationStatus().GetExitCode(); + if (IsNonCrashErrorExitCode(exitCode)) + { + return output; + } + throw ThrowUnknownGitError(output, @params); + } + catch (CommandException e) + { + throw new RepoException("Error executing 'git': " + e.Message, e); + } + } + + private static CommandOutputWithStatus ExecuteGit( + string cwd, + IEnumerable @params, + GitEnvironment gitEnv, + bool verbose, + TimeSpan? timeout) => + ExecuteGit(cwd, @params, gitEnv, verbose, DefaultMaxLogLines, timeout); + + private static CommandOutputWithStatus ExecuteGit( + string cwd, + IEnumerable @params, + GitEnvironment gitEnv, + bool verbose, + int maxLogLines, + TimeSpan? timeout) + { + var allParams = new List { gitEnv.ResolveGitBinary() }; + allParams.AddRange(@params); + var cmd = new Command(allParams.ToArray(), gitEnv.GetEnvironment(), cwd); + var runner = (timeout.HasValue + ? new CommandRunner(cmd, timeout.Value) + : new CommandRunner(cmd)) + .WithVerbose(verbose); + return maxLogLines >= 0 ? runner.WithMaxStdOutLogLines(maxLogLines).Execute() : runner.Execute(); + } + + public override string ToString() => + $"GitRepository{{gitDir={_gitDir}, workTree={_workTree}, verbose={_verbose}}}"; + + /// Resolve a reference. + internal GitRevision ResolveReferenceWithContext( + string reference, string? contextRef, string url) + { + if (GitRevision.CompleteGitHashPattern.IsMatch(reference)) + { + if (CheckSha1Exists(reference)) + { + return new GitRevision(this, reference, url); + } + throw new CannotResolveRevisionException( + $"Cannot find '{reference}' object in the repository ({url})"); + } + return new GitRevision( + this, ParseRef(reference), null, contextRef, + ImmutableListMultimap.Empty, url); + } + + /// Resolve a reference. + public GitRevision ResolveReference(string reference) + { + if (GitRevision.CompleteGitHashPattern.IsMatch(reference)) + { + if (CheckSha1Exists(reference)) + { + return new GitRevision(this, reference); + } + throw new CannotResolveRevisionException( + "Cannot find '" + reference + "' object in the repository"); + } + return new GitRevision(this, ParseRef(reference)); + } + + /// Checks if a SHA-1 object exists in the repository. + private bool CheckSha1Exists(string reference) + { + var @params = new[] { "cat-file", "-e", reference }; + CommandOutputWithStatus output = + GitAllowNonZeroExit(CommandRunner.NoInput, @params, CommandRunner.DefaultTimeout); + if (output.GetTerminationStatus().Success()) + { + return true; + } + if (output.GetStderr().Length == 0) + { + return false; + } + throw ThrowUnknownGitError(output, @params); + } + + public byte[] ReadFileBytes(string revision, string path) + { + CommandOutputWithStatus result = GitAllowNonZeroExit( + CommandRunner.NoInput, + new[] { "--no-pager", "show", $"{revision}:{path}" }, + CommandRunner.DefaultTimeout, + 0); + if (!result.GetTerminationStatus().Success()) + { + throw new RepoException($"Cannot read file '{path}' in '{revision}'"); + } + return result.GetStdoutBytes(); + } + + /// Reads a file at the given revision. + public string ReadFile(string revision, string path) => + Encoding.UTF8.GetString(ReadFileBytes(revision, path)); + + public string ReadSymlink(string revision, string path) => ReadFile(revision, path); + + /// Returns the commit hash at which the given file was last modified. + public string LastModified(string revision, string path) + { + CommandOutputWithStatus result = GitAllowNonZeroExit( + CommandRunner.NoInput, + new[] { "--no-pager", "log", "--pretty=format:%H", "--max-count=1", revision, "--", path }, + CommandRunner.DefaultTimeout, + 0); + if (!result.GetTerminationStatus().Success()) + { + throw new RepoException( + $"Cannot get last modified revision of '{path}' in '{revision}'"); + } + return result.GetStdout(); + } + + public void Checkout(Glob glob, string destRoot, GitRevision rev) + { + var treeElements = LsTree(rev, null, true, true); + var pathMatcher = glob.RelativeTo(destRoot); + + void CheckoutFiles(IReadOnlyList files) + { + var args = new List + { + "--git-dir", _gitDir, + "--work-tree", destRoot, + "checkout", rev.GetHash(), "--", + }; + args.AddRange(files); + Git(GetCwd(), args.ToArray()); + } + + var pendingFiles = new List(); + int pendingFilesLength = 0; + foreach (var file in treeElements) + { + var path = file.Path; + if (pathMatcher.Matches(Path.Combine(destRoot, path))) + { + pendingFiles.Add(path); + pendingFilesLength += path.Length; + } + // Work around "argument list too long" by batching. + if (pendingFilesLength > 128 * 1024) + { + CheckoutFiles(pendingFiles); + pendingFiles = new List(); + pendingFilesLength = 0; + } + } + if (pendingFilesLength > 0) + { + CheckoutFiles(pendingFiles); + } + } + + internal GitRevision CommitTree(string message, string tree, IReadOnlyList parents) + { + var args = new List { "commit-tree", tree }; + foreach (var parent in parents) + { + args.Add("-p"); + args.Add(parent.GetHash()); + } + args.Add("-m"); + args.Add(message); + return new GitRevision( + this, Git(GetCwd(), null, AddGitDirAndWorkTreeParams(args)).GetStdout().Trim()); + } + + /// Creates a reference from a complete SHA-1 without validating it exists. + internal GitRevision CreateReferenceFromCompleteSha1(string @ref) => new(this, @ref); + + private bool IsHashReference(string @ref) => HashPattern.IsMatch(@ref); + + /// Information of a submodule of this repository. + public sealed record Submodule(string Url, string Name, string? Branch, string Path); + + internal sealed record TreeElement(GitObjectType Type, string Ref, string Path, string Mode) + { + public const string SymlinkMode = "120000"; + } + + internal enum GitObjectType + { + Blob, + Commit, + Tag, + Tree, + } + + public sealed record StatusFile( + string File, string? NewFileName, StatusCode IndexStatus, StatusCode WorkdirStatus); + + public enum StatusCode + { + Unmodified, + Modified, + Added, + Deleted, + Renamed, + Copied, + UpdatedButUnmerged, + Untracked, + Ignored, + ChangeType, + } + + /// Hook to rewrite exceptions thrown by the git invocation, e.g. user error. + protected virtual void HandlePushException(Exception e, PushCmd cmd) + { + // Non-fast-forward errors usually mean the destination has commits the origin doesn't. + if (e.Message.Contains("(non-fast-forward)") || e.Message.Contains("(fetch first)")) + { + throw new NonFastForwardRepositoryException( + $"Failed to push to {cmd.Url} {FormatRefspecs(cmd.Refspecs)}, because local/origin" + + " history is behind destination", + e); + } + if (e.Message.Contains("(stale info)")) + { + throw new NonFastForwardRepositoryException( + $"Failed to push to {cmd.Url} {FormatRefspecs(cmd.Refspecs)}, because destination is" + + " not in expected state", + e); + } + if (e is RepoException or ValidationException) + { + throw e; + } + } + + private static string FormatRefspecs(IReadOnlyList refspecs) => + "[" + string.Join(", ", refspecs.Select(r => r.ToString())) + "]"; + + /// An object capable of performing a 'git push' operation to a remote repository. + public sealed record PushCmd + { + private readonly GitRepository _repo; + public string? Url { get; } + public ImmutableArray Refspecs { get; } + public bool Prune { get; } + public bool Force { get; } + public ImmutableDictionary ForceLease { get; } + public ImmutableArray PushOptions { get; } + private readonly PushOptionsValidator _pushOptionsValidator; + + public PushCmd( + GitRepository repo, + string? url, + ImmutableArray refspecs, + bool prune, + bool force, + ImmutableDictionary forceLease, + ImmutableArray pushOptions, + PushOptionsValidator pushOptionsValidator) + { + _repo = Preconditions.CheckNotNull(repo); + Preconditions.CheckArgument( + refspecs.IsEmpty || url != null, "refspec can only be used when a url is passed"); + Url = url; + Refspecs = refspecs; + Prune = prune; + Force = force; + ForceLease = Preconditions.CheckNotNull(forceLease); + PushOptions = pushOptions; + _pushOptionsValidator = pushOptionsValidator; + } + + public PushCmd WithRefspecs(string url, IEnumerable refspecs) => + new( + _repo, Preconditions.CheckNotNull(url), refspecs.ToImmutableArray(), Prune, Force, + ForceLease, PushOptions, _pushOptionsValidator); + + public PushCmd WithForceLease(IReadOnlyDictionary forceLease) => + new( + _repo, Url, Refspecs, Prune, Force, forceLease.ToImmutableDictionary(), PushOptions, + _pushOptionsValidator); + + public PushCmd WithPrune(bool prune) => + new(_repo, Url, Refspecs, prune, Force, ForceLease, PushOptions, _pushOptionsValidator); + + public PushCmd WithForce(bool force) => + new(_repo, Url, Refspecs, Prune, force, ForceLease, PushOptions, _pushOptionsValidator); + + /// Returns a new instance with the given push options. + /// if the push options fail validation + public PushCmd WithPushOptions(ImmutableArray newPushOptions) + { + _pushOptionsValidator.Validate(newPushOptions); + return new PushCmd( + _repo, Url, Refspecs, Prune, Force, ForceLease, newPushOptions, + _pushOptionsValidator); + } + + /// Runs the push command and returns the response from the server. + public string Run() + { + string? output = null; + try + { + output = _repo.RunPush(this); + } + catch (Exception e) when (e is RepoException or ValidationException) + { + _repo.HandlePushException(e, this); + } + ValidationException.CheckCondition( + output != null && !ProtectedBranch.IsMatch(output), + "Cannot push to %s refspecs %s. Please request an admin of the repo to verify the" + + " branch protection rules at %s/settings/branches if you think it's a legit" + + " branch.", + Url!, + FormatRefspecs(Refspecs), + Url!); + return output!; + } + } + + /// An object capable of performing a 'git merge' operation. + public class MergeCmd + { + protected string Branch; + protected string MergeMessage; + protected string FastForward; + protected GitRepository Repo; + protected IReadOnlyList Commits; + internal Func, bool> Validator; + + public MergeCmd( + GitRepository repo, + string branch, + string mergeMessage, + IReadOnlyList commits, + string fastForward, + Func, bool> validator) + { + Preconditions.CheckArgument( + fastForward is "--no-ff" or "--ff-only" or "--ff"); + Repo = Preconditions.CheckNotNull(repo); + Validator = validator; + Branch = Preconditions.CheckNotNull(branch); + MergeMessage = mergeMessage; + FastForward = Preconditions.CheckNotNull(fastForward); + Commits = Preconditions.CheckNotNull(commits); + } + + public static MergeCmd Create( + GitRepository repo, + string branch, + IReadOnlyList commits, + Func, bool> validator) => + new(repo, branch, "", commits, "--ff", validator); + + public MergeCmd WithFFMode(string ffMode) => + new(Repo, Branch, MergeMessage, Commits, ffMode, Validator); + + public MergeCmd WithMessage(string message) => + new(Repo, Branch, message, Commits, FastForward, Validator); + + public void Run(IReadOnlyDictionary configs) + { + Preconditions.CheckArgument( + Validator(configs), "Error could not validate git configs in %s", configs); + var command = new List(); + foreach (var entry in configs) + { + command.Add("-c"); + command.Add($"{entry.Key}={entry.Value}"); + } + command.Add("merge"); + command.Add(Branch); + if (!string.IsNullOrEmpty(MergeMessage)) + { + command.Add("-m"); + command.Add(MergeMessage); + } + command.Add(FastForward); + command.AddRange(Commits); + if (Repo.NoVerify) + { + command.Add("--no-verify"); + } + Repo.SimpleCommand(command); + } + } + + /// + /// An object capable of performing a 'git log' operation and returning a list of + /// . + /// + public sealed record LogCmd + { + private readonly GitRepository _repo; + private readonly string _refExpr; + private readonly int _limit; + private readonly ImmutableArray _paths; + private readonly bool _firstParent; + private readonly bool _includeStat; + private readonly bool _includeBody; + private readonly string? _grepString; + private readonly bool _includeMergeDiff; + private readonly int _skip; + private readonly int _batchSize; + private readonly bool _includeTags; + private readonly bool _noWalk; + private readonly bool _topoOrder; + + private const string CommitField = "commit"; + private const string ParentsField = "parents"; + private const string TreeField = "tree"; + private const string AuthorField = "author"; + private const string AuthorDateField = "author_date"; + private const string CommitterField = "committer"; + private const string CommitterDate = "committer_date"; + private const string TagField = "tag"; + private const string BeginBody = "begin_body"; + private const string EndBody = "end_body"; + private const string CommitSeparator = "copybara"; + private static readonly Regex Unindent = new("\n ", RegexOptions.Compiled); + private const string Group = "--\n"; + + private LogCmd( + GitRepository repo, + string refExpr, + int limit, + ImmutableArray paths, + bool firstParent, + bool includeStat, + bool includeBody, + string? grepString, + bool includeMergeDiff, + int skip, + int batchSize, + bool includeTags, + bool noWalk, + bool topoOrder) + { + _repo = repo; + _refExpr = refExpr; + _limit = limit; + _paths = paths; + _firstParent = firstParent; + _includeStat = includeStat; + _includeBody = includeBody; + _grepString = grepString; + _includeMergeDiff = includeMergeDiff; + _skip = skip; + _batchSize = batchSize; + _includeTags = includeTags; + _noWalk = noWalk; + _topoOrder = topoOrder; + } + + internal static LogCmd Create(GitRepository repository, string refExpr) => + new( + Preconditions.CheckNotNull(repository), + Preconditions.CheckNotNull(refExpr), + 0, + ImmutableArray.Empty, + firstParent: true, + includeStat: false, + includeBody: true, + grepString: null, + includeMergeDiff: false, + skip: 0, + batchSize: 0, + includeTags: false, + noWalk: false, + topoOrder: false); + + private LogCmd Copy( + int? limit = null, + ImmutableArray? paths = null, + bool? firstParent = null, + bool? includeStat = null, + bool? includeBody = null, + string? grepString = null, + bool? includeMergeDiff = null, + int? skip = null, + int? batchSize = null, + bool? includeTags = null, + bool? noWalk = null, + bool? topoOrder = null) => + new( + _repo, + _refExpr, + limit ?? _limit, + paths ?? _paths, + firstParent ?? _firstParent, + includeStat ?? _includeStat, + includeBody ?? _includeBody, + grepString ?? _grepString, + includeMergeDiff ?? _includeMergeDiff, + skip ?? _skip, + batchSize ?? _batchSize, + includeTags ?? _includeTags, + noWalk ?? _noWalk, + topoOrder ?? _topoOrder); + + public LogCmd WithLimit(int limit) + { + Preconditions.CheckArgument(limit > 0); + return Copy(limit: limit); + } + + public LogCmd WithSkip(int skip) + { + Preconditions.CheckArgument(skip >= 0); + return Copy(skip: skip); + } + + public LogCmd WithBatchSize(int batchSize) + { + Preconditions.CheckArgument(batchSize >= 0); + return Copy(batchSize: batchSize); + } + + public LogCmd WithPaths(ImmutableArray paths) + { + Preconditions.CheckArgument(paths.All(s => s.Trim().Length != 0)); + return Copy(paths: paths); + } + + public LogCmd FirstParent(bool firstParent) => Copy(firstParent: firstParent); + + public LogCmd TopoOrder(bool topoOrder) => Copy(topoOrder: topoOrder); + + public LogCmd IncludeFiles(bool includeStat) => Copy(includeStat: includeStat); + + public LogCmd IncludeMergeDiff(bool includeMergeDiff) => + Copy(includeMergeDiff: includeMergeDiff); + + public LogCmd IncludeBody(bool includeBody) => Copy(includeBody: includeBody); + + public LogCmd Grep(string? grepString) => Copy(grepString: grepString); + + public LogCmd IncludeTags(bool includeTags) => Copy(includeTags: includeTags); + + public LogCmd NoWalk(bool noWalk) => Copy(noWalk: noWalk); + + /// Run 'git log' and returns zero or more . + public IReadOnlyList Run() + { + var cmd = new List { "log", "--no-color", CreateFormat(_includeBody, _includeTags) }; + if (_includeStat) + { + cmd.Add("--name-only"); + cmd.Add("--no-renames"); + } + if (_firstParent) + { + cmd.Add("--first-parent"); + } + if (_includeMergeDiff) + { + cmd.Add("-m"); + } + cmd.Add("-z"); + if (_includeTags) + { + cmd.Add("--tags"); + } + if (_noWalk) + { + cmd.Add("--no-walk"); + } + if (_topoOrder) + { + cmd.Add("--topo-order"); + } + if (!string.IsNullOrEmpty(_grepString)) + { + cmd.Add("--grep"); + cmd.Add(_grepString); + } + cmd.Add(_refExpr); + if (!_paths.IsEmpty) + { + cmd.Add("--"); + cmd.AddRange(_paths); + } + return RunGitLog(cmd); + } + + private IReadOnlyList RunGitLog(List cmd) + { + var res = new List(); + IReadOnlyList batchRes; + int batchSkip = _skip; + int overallLimit = _limit; + do + { + var batchCmd = new List(cmd); + int batchLimit = _limit == 0 + ? _batchSize + : (_batchSize == 0 ? overallLimit : Math.Min(_batchSize, overallLimit)); + if (batchSkip > 0) + { + batchCmd.Add("--skip"); + batchCmd.Add(batchSkip.ToString(CultureInfo.InvariantCulture)); + } + if (batchLimit > 0) + { + batchCmd.Add("-" + batchLimit); + } + // Avoid logging since git log can return LOT of entries. + CommandOutput output = _limit is > 0 and < 10 + ? _repo.SimpleCommand(batchCmd) + : _repo.SimpleCommandNoRedirectOutput(batchCmd.ToArray()); + batchRes = ParseLog(output.GetStdout(), _includeBody); + if (_batchSize > 0) + { + batchSkip += batchRes.Select(e => e.Commit.GetHash()).Distinct().Count(); + overallLimit -= batchSkip; + } + res.AddRange(batchRes); + } + while (_batchSize > 0 && (_limit == 0 || overallLimit > 0) && batchRes.Count != 0); + return res; + } + + private IReadOnlyList ParseLog(string log, bool includeBody) + { + if (log.Length == 0) + { + return Array.Empty(); + } + + var commits = new List(); + foreach (var msg in SplitOn( + log.Substring(CommitSeparator.Length), "\0" + CommitSeparator)) + { + var groups = SplitOn(msg, "\n" + Group); + + var fields = new Dictionary(); + foreach (var kvLine in groups[0].Split('\n')) + { + int idx = kvLine.IndexOf('='); + if (idx < 0) + { + continue; + } + fields[kvLine.Substring(0, idx)] = kvLine.Substring(idx + 1); + } + + string? body = null; + if (includeBody) + { + body = Unindent.Replace(groups[1], "\n"); + body = body.Substring( + BeginBody.Length + 1, body.Length - EndBody.Length - 1 - (BeginBody.Length + 1)); + body = body.Replace("\r\n", "\n"); + } + + ImmutableHashSet? files = null; + if (_includeStat) + { + string fileString = groups[2]; + if (fileString.StartsWith("\0\n", StringComparison.Ordinal)) + { + fileString = fileString.Substring(2); + } + files = fileString.Split('\0').Where(s => s.Length != 0).ToImmutableHashSet(); + } + + var parents = new List(); + foreach (var parent in GetField(fields, ParentsField) + .Split(' ') + .Where(s => s.Length != 0)) + { + parents.Add(_repo.CreateReferenceFromCompleteSha1(parent)); + } + + string tree = GetField(fields, TreeField); + string commit = GetField(fields, CommitField); + + string? tagString = _includeTags ? GetField(fields, TagField) : null; + GitRevision? tag = tagString != null + ? _repo.CreateReferenceFromCompleteSha1(commit).WithContextReference(tagString) + : null; + + try + { + commits.Add(new GitLogEntry( + _repo.CreateReferenceFromCompleteSha1(commit), + parents, + tree, + AuthorParser.Parse(GetField(fields, AuthorField)), + AuthorParser.Parse(GetField(fields, CommitterField)), + TryParseDate(fields, AuthorDateField, commit), + TryParseDate(fields, CommitterDate, commit), + body, + files, + tag)); + } + catch (InvalidAuthorException e) + { + throw new RepoException($"Error in commit '{commit}'. Invalid author.", e); + } + } + return commits; + } + + // Splitter.on(literal) semantics (not regex). + private static List SplitOn(string input, string separator) + { + var result = new List(); + int start = 0; + while (true) + { + int idx = input.IndexOf(separator, start, StringComparison.Ordinal); + if (idx < 0) + { + result.Add(input.Substring(start)); + break; + } + result.Add(input.Substring(start, idx - start)); + start = idx + separator.Length; + } + return result; + } + + private static DateTimeOffset TryParseDate( + IReadOnlyDictionary fields, string dateField, string commit) + { + string value = GetField(fields, dateField); + if (DateTimeOffset.TryParse( + value, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var parsed)) + { + return parsed; + } + return DateTimeOffset.UnixEpoch; + } + + private static string GetField(IReadOnlyDictionary fields, string field) => + fields.TryGetValue(field, out var value) + ? value + : throw new InvalidOperationException($"{field} not present"); + + private static string CreateFormat(bool includeBody, bool includeTags) + { + return ("--format=" + + CommitSeparator + + CommitField + "=%H\n" + + ParentsField + "=%P\n" + + TreeField + "=%T\n" + + AuthorField + "=%an <%ae>\n" + + AuthorDateField + "=%aI\n" + + CommitterField + "=%cn <%ce>\n" + + CommitterDate + "=%cI" + + (includeTags ? "\n" + TagField + "=%S\n" : "\n") + + Group + + (includeBody + ? BeginBody + "\n" + "%w(0,4,4)%B%w(0,0,0)\n" + EndBody + "\n" + : "\n") + + Group) + .Replace("\n", "%n") + .Replace("", "%x01"); + } + } + + /// An object that represents a commit as returned by 'git log'. + public sealed record GitLogEntry( + GitRevision Commit, + IReadOnlyList Parents, + string Tree, + Author Author, + Author Committer, + DateTimeOffset AuthorDate, + DateTimeOffset CommitDate, + string? Body, + ImmutableHashSet? Files, + GitRevision? Tag); + + public string GitCmd() => + "git --git-dir=" + _gitDir + (_workTree != null ? " --work-tree=" + _workTree : ""); + + /// An object capable of performing a 'git tag' operation. + public sealed record TagCmd( + GitRepository Repo, string TagName, string? TagMessage, bool ForceFlag) + { + internal static TagCmd Create(GitRepository gitRepository, string tagName) => + new(gitRepository, tagName, null, false); + + public TagCmd WithAnnotatedTag(string tagMessage) => + new(Repo, TagName, tagMessage, ForceFlag); + + public TagCmd Force(bool force) => new(Repo, TagName, TagMessage, force); + + public void Run() + { + var cmd = new List { "tag" }; + if (TagMessage != null) + { + cmd.Add("-a"); + } + cmd.Add(TagName); + if (TagMessage != null) + { + cmd.Add("-m"); + cmd.Add(TagMessage); + } + if (ForceFlag) + { + cmd.Add("--force"); + } + Repo.SimpleCommand(cmd); + } + } + + /// Returns the repo's primary branch, e.g. "main". + public string GetPrimaryBranch() => + SimpleCommand("symbolic-ref", "--short", "HEAD").GetStdout().Trim(); + + /// Returns the primary branch of a remote repo, e.g. "main". + public string? GetPrimaryBranch(string uri) + { + IReadOnlyDictionary refs; + try + { + refs = LsRemote( + uri, new[] { "HEAD", "main", "master" }, DefaultMaxLogLines, new[] { "--symref" }); + } + catch (ValidationException e) + { + if (e is AccessValidationException) + { + throw; + } + throw new RepoException("Error parsing primary branch", e); + } + foreach (var key in refs.Values) + { + Match matcher = DefaultBranchPattern.Match(key); + if (matcher.Success) + { + return matcher.Groups[2].Value; + } + } + // Repo has no HEAD, try to guess by testing which branches exist. + if (refs.ContainsKey("refs/heads/main") && !refs.ContainsKey("refs/heads/master")) + { + return "main"; + } + if (refs.ContainsKey("refs/heads/master") && !refs.ContainsKey("refs/heads/main")) + { + return "master"; + } + return null; + } + + public string GetCurrentBranch() + { + try + { + string rev = SimpleCommand("symbolic-ref", "--short", "HEAD").GetStdout().Trim(); + return rev == "HEAD" ? "" : rev; + } + catch (RepoException re) + { + if (re.Message.Contains("ref HEAD is not a symbolic ref")) + { + return ""; + } + throw; + } + } + + /// Interface for validating git options and providing useful error messages. + public interface IOptionsValidator + { + void Validate(IReadOnlyList options); + } + + /// A validator that validates push options against an allowlist. + public sealed record PushOptionsValidator(ImmutableArray? AllowedOptions) + : IOptionsValidator + { + public void Validate(IReadOnlyList options) + { + if (AllowedOptions == null) + { + return; + } + var allowedKeys = AllowedOptions.Value.ToImmutableHashSet(); + var invalid = options.Where(o => !IsAllowedOption(o, allowedKeys)).ToList(); + if (invalid.Count != 0) + { + throw new ValidationException( + "Push options have failed validation. The allowed push options are [" + + string.Join(", ", allowedKeys) + + "], but found push options not on the allowlist: [" + + string.Join(", ", invalid) + "]"); + } + } + + private static bool IsAllowedOption(string option, IReadOnlySet allowedKeys) + { + int idx = option.IndexOf('='); + if (idx >= 0) + { + return allowedKeys.Contains(option.Substring(0, idx)); + } + return allowedKeys.Contains(option); + } + } +} + +/// Helpers for . +internal static class StatusCodeMethods +{ + public static char GetCode(this GitRepository.StatusCode code) => + code switch + { + GitRepository.StatusCode.Unmodified => ' ', + GitRepository.StatusCode.Modified => 'M', + GitRepository.StatusCode.Added => 'A', + GitRepository.StatusCode.Deleted => 'D', + GitRepository.StatusCode.Renamed => 'R', + GitRepository.StatusCode.Copied => 'C', + GitRepository.StatusCode.UpdatedButUnmerged => 'U', + GitRepository.StatusCode.Untracked => '?', + GitRepository.StatusCode.Ignored => '!', + GitRepository.StatusCode.ChangeType => 'T', + _ => throw new ArgumentOutOfRangeException(nameof(code)), + }; +} + +/// A small insertion-ordered set. Mirrors Java's LinkedHashSet usage. +internal sealed class LinkedHashSet : System.Collections.Generic.IEnumerable +{ + private readonly List _order = new(); + private readonly HashSet _set = new(); + + public bool Add(T item) + { + if (_set.Add(item)) + { + _order.Add(item); + return true; + } + return false; + } + + public ImmutableHashSet ToImmutableHashSet() => _order.ToImmutableHashSet(); + + public System.Collections.Generic.IEnumerator GetEnumerator() => _order.GetEnumerator(); + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => + GetEnumerator(); +} diff --git a/src/Copybara.Core/Git/GitRevision.cs b/src/Copybara.Core/Git/GitRevision.cs new file mode 100644 index 000000000..4b672a0fc --- /dev/null +++ b/src/Copybara.Core/Git/GitRevision.cs @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Revision; + +namespace Copybara.Git; + +/// A Git repository reference. Port of com.google.copybara.git.GitRevision. +public sealed class GitRevision : IRevision +{ + public static readonly Regex CompleteGitHashPattern = + new("^(?:[a-f0-9]{40}|[a-f0-9]{64})$", RegexOptions.Compiled); + + private readonly GitRepository _repository; + private readonly GitHashAlgorithm _hashAlgorithm; + private readonly string _hash; + private readonly string? _reference; + private readonly ImmutableListMultimap _associatedLabels; + private readonly string? _reviewReference; + private readonly string? _url; + private string? _describe; + private string? _describeAbbrev; + private string? _revisionNumber; + private string? _fullReferenceCache; + private bool _fullReferenceComputed; + + private readonly object _lock = new(); + + /// + /// Create a git revision from a complete (40 or 64 characters) git hash string. + /// + public GitRevision(GitRepository repository, string hash) + : this(repository, hash, null, null, ImmutableListMultimap.Empty, null) + { + } + + /// + /// Create a git revision from a complete git hash string with a url. + /// + public GitRevision(GitRepository repository, string hash, string? url) + : this(repository, hash, null, null, ImmutableListMultimap.Empty, url) + { + } + + /// + /// Create a git revision from a complete (40 or 64 characters) git hash string. + /// + /// git repository that should contain the hash + /// the commit hash + /// an arbitrary string that allows to keep track of the revision of + /// the code review being migrated. + /// a stable name that describes where this is coming from. Could be a git + /// reference like 'master'. + /// labels associated with this reference + /// if present, the url of the repository that the revision comes from + public GitRevision( + GitRepository repository, + string hash, + string? reviewReference, + string? reference, + ImmutableListMultimap associatedLabels, + string? url) + { + _reviewReference = reviewReference; + Preconditions.CheckArgument( + CompleteGitHashPattern.IsMatch(hash), + "Reference '%s' is not a full git hash (40 characters SHA-1 or 64 characters SHA-256)", + hash); + + _repository = Preconditions.CheckNotNull(repository); + _hash = hash; + _hashAlgorithm = GitHashAlgorithmMethods.From(hash); + _reference = reference; + + var labels = ImmutableListMultimap.CreateBuilder(); + var existing = new HashSet(); + foreach (var key in associatedLabels.Keys) + { + existing.Add(key); + labels.PutAll(key, associatedLabels.Get(key)); + } + string shortHash = hash.Substring(0, 7); + // TODO: Remove GIT_SHA1 and GIT_SHORT_SHA1 labels once all instances are updated. + if (_hashAlgorithm == GitHashAlgorithm.Sha1) + { + PutIfMissing(labels, existing, "GIT_SHA1", hash); + PutIfMissing(labels, existing, "GIT_SHORT_SHA1", shortHash); + } + PutIfMissing(labels, existing, "GIT_HASH", hash); + PutIfMissing(labels, existing, "GIT_SHORT_HASH", shortHash); + _associatedLabels = labels.Build(); + _url = url; + } + + private static void PutIfMissing( + ImmutableListMultimap.Builder labels, + HashSet existing, + string key, + string value) + { + if (existing.Add(key)) + { + labels.Put(key, value); + } + } + + public string? ContextReference() => _reference; + + public string? FixedReference() => _hash; + + public string? FullReference() + { + lock (_lock) + { + if (_fullReferenceComputed) + { + return _fullReferenceCache; + } + + if (_reference == null || _reference.StartsWith("refs/", StringComparison.Ordinal)) + { + _fullReferenceCache = _reference; + _fullReferenceComputed = true; + return _fullReferenceCache; + } + + try + { + var matchingRefs = new List(); + var refs = _repository.ShowRef( + new[] { _reference, _reference + GitRepository.FullRefNamespace }); + foreach (var e in refs) + { + if (!e.Key.StartsWith( + GitRepository.CopybaraFetchNamespace + "/refs/", StringComparison.Ordinal)) + { + continue; + } + if (e.Value.GetHash() != _hash) + { + continue; + } + matchingRefs.Add(GetCleanedFullReference(e.Key)); + } + + if (matchingRefs.Count != 0) + { + // Git allows branches and tags with the same name. Prioritize tags over branches. + string? tag = matchingRefs.FirstOrDefault( + e => e.StartsWith("refs/tags/", StringComparison.Ordinal)); + _fullReferenceCache = tag ?? matchingRefs[0]; + } + } + catch (RepoException) + { + // Could not determine full reference; leave unset for this call. + return null; + } + + _fullReferenceComputed = true; + return _fullReferenceCache; + } + } + + private static string GetCleanedFullReference(string originalRef) + { + string fullRef = originalRef; + const string prefix = "refs/copybara_fetch/"; + if (fullRef.StartsWith(prefix, StringComparison.Ordinal)) + { + fullRef = fullRef.Substring(prefix.Length); + } + if (fullRef.EndsWith(GitRepository.FullRefNamespace, StringComparison.Ordinal)) + { + fullRef = fullRef.Substring(0, fullRef.Length - GitRepository.FullRefNamespace.Length); + } + return fullRef; + } + + public DateTimeOffset? ReadTimestamp() + { + var entry = _repository.Log(_hash).WithLimit(1).Run(); + if (entry.Count == 0) + { + throw new RepoException($"Cannot find '{_hash}' in the git repository"); + } + return entry[0].AuthorDate; + } + + public string AsString() => + _hash + (_reviewReference == null ? "" : " " + _reviewReference); + + public string GetHash() => _hash; + + public GitHashAlgorithm GetHashAlgorithm() => _hashAlgorithm; + + public string? GetReviewReference() => _reviewReference; + + public override string ToString() + { + var sb = new System.Text.StringBuilder("GitRevision{"); + var parts = new List(); + if (_url != null) + { + parts.Add($"url={_url}"); + } + if (_reference != null) + { + parts.Add($"reference={_reference}"); + } + parts.Add($"hash={_hash}"); + sb.Append(string.Join(", ", parts)); + sb.Append('}'); + return sb.ToString(); + } + + public string? GetUrl() => _url; + + public string? GetRevisionType() => "Git"; + + public override bool Equals(object? o) + { + if (ReferenceEquals(this, o)) + { + return true; + } + if (o is null || GetType() != o.GetType()) + { + return false; + } + var that = (GitRevision)o; + return _hash == that._hash; + } + + public override int GetHashCode() => _hash.GetHashCode(); + + public ImmutableListMultimap AssociatedLabels() => _associatedLabels; + + public IReadOnlyList AssociatedLabel(string label) + { + // We only return git describe if specifically asked for this label + if (label == GitRepository.GitDescribeChangeVersion) + { + return PopulateDescribe(); + } + if (label == GitRepository.GitSequentialRevisionNumber) + { + return PopulateRevisionNumber(); + } + if (label == GitRepository.GitDescribeAbbrev) + { + return PopulateDescribeAbbrev(); + } + if (label == GitRepository.GitTagPointsAt) + { + return PopulateTagPointsAt(); + } + return _associatedLabels.Get(label); + } + + /// Lazily compute describe. + private IReadOnlyList PopulateDescribe() + { + lock (_lock) + { + if (_describe == null) + { + try + { + _describe = _repository.Describe(this, false); + } + catch (RepoException) + { + _describe = _hash.Substring(0, 7); + } + } + return new[] { _describe! }; + } + } + + private IReadOnlyList PopulateTagPointsAt() + { + lock (_lock) + { + if (_associatedLabels.ContainsKey(GitRepository.GitTagPointsAt)) + { + return _associatedLabels.Get(GitRepository.GitTagPointsAt); + } + try + { + return _repository.TagPointsAt(this); + } + catch (RepoException) + { + // Cannot get 'tag --points-at' output. + } + return Array.Empty(); + } + } + + private IReadOnlyList PopulateDescribeAbbrev() + { + lock (_lock) + { + if (_associatedLabels.ContainsKey(GitRepository.GitDescribeAbbrev)) + { + return _associatedLabels.Get(GitRepository.GitDescribeAbbrev); + } + if (_describeAbbrev == null) + { + try + { + _describeAbbrev = _repository.DescribeAbbrev(this); + } + catch (RepoException) + { + // Cannot get closest tag. + } + } + return new[] { _describeAbbrev ?? "" }; + } + } + + private IReadOnlyList PopulateRevisionNumber() + { + lock (_lock) + { + if (_revisionNumber == null) + { + try + { + var cmdout = _repository.SimpleCommand("rev-list", "--count", _hash); + _revisionNumber = cmdout.GetStdout().Trim(); + } + catch (RepoException) + { + _revisionNumber = ""; + } + } + return new[] { _revisionNumber! }; + } + } + + internal GitRevision WithUrl(string url) => + new(_repository, _hash, _reviewReference, _reference, _associatedLabels, url); + + internal GitRevision WithContextReference(string tag) => + new(_repository, _hash, _reviewReference, tag, _associatedLabels, _url); + + internal GitRevision WithLabels(ImmutableListMultimap labels) => + new( + _repository, + _hash, + _reviewReference, + _reference, + IRevision.AddNewLabels(_associatedLabels, labels), + _url); +} + +/// Supported Git hash algorithms. +public enum GitHashAlgorithm +{ + /// SHA-1 hash algorithm (40 characters). + Sha1, + + /// SHA-256 hash algorithm (64 characters). + Sha256, +} + +/// Helpers for . +public static class GitHashAlgorithmMethods +{ + public static int GetLength(this GitHashAlgorithm algorithm) => + algorithm == GitHashAlgorithm.Sha1 ? 40 : 64; + + /// Returns the algorithm corresponding to the length of the given hash. + public static GitHashAlgorithm From(string hash) => + hash.Length switch + { + 40 => GitHashAlgorithm.Sha1, + 64 => GitHashAlgorithm.Sha256, + _ => throw new ArgumentException( + $"Invalid hash length {hash.Length}: '{hash}'"), + }; +} diff --git a/src/Copybara.Core/Git/IntegrateLabel.cs b/src/Copybara.Core/Git/IntegrateLabel.cs new file mode 100644 index 000000000..37d611ad1 --- /dev/null +++ b/src/Copybara.Core/Git/IntegrateLabel.cs @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Git; + +/// +/// A label value that describes what to integrate. Port of +/// com.google.copybara.git.IntegrateLabel. +/// +public interface IIntegrateLabel +{ + /// Get the merge message. + string MergeMessage(IReadOnlyList labelsToAdd); + + /// Get the revision to integrate. + GitRevision GetRevision(); + + static IIntegrateLabel GenericGitRevision(GitRevision revision) + { + Preconditions.CheckNotNull(revision); + return new GenericGitRevisionLabel(revision); + } + + static string WithLabels(string msg, IReadOnlyList labelsToAdd) + { + var result = ChangeMessage.ParseMessage(msg); + foreach (var labelFinder in labelsToAdd) + { + result = result.WithLabel( + labelFinder.GetName(), labelFinder.GetSeparator(), labelFinder.GetValue()); + } + return result.ToString(); + } + + private sealed class GenericGitRevisionLabel : IIntegrateLabel + { + private readonly GitRevision _revision; + + public GenericGitRevisionLabel(GitRevision revision) => _revision = revision; + + public string MergeMessage(IReadOnlyList labelsToAdd) => + IIntegrateLabel.WithLabels("Merge of " + _revision.GetHash(), labelsToAdd); + + public GitRevision GetRevision() => _revision; + + public override string ToString() => "Merge of " + _revision.GetHash(); + } +} diff --git a/src/Copybara.Core/Git/InvalidRefspecException.cs b/src/Copybara.Core/Git/InvalidRefspecException.cs new file mode 100644 index 000000000..7767847ed --- /dev/null +++ b/src/Copybara.Core/Git/InvalidRefspecException.cs @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.Git; + +/// +/// Exception thrown when an invalid refspec is passed to Copybara via config, flags, etc. Port of +/// com.google.copybara.git.InvalidRefspecException. +/// +public class InvalidRefspecException : ValidationException +{ + public InvalidRefspecException(string message) + : base(message) + { + } +} diff --git a/src/Copybara.Core/Git/MergeResult.cs b/src/Copybara.Core/Git/MergeResult.cs new file mode 100644 index 000000000..e08cccf83 --- /dev/null +++ b/src/Copybara.Core/Git/MergeResult.cs @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git; + +/// +/// The result returned by git merge when used in Starlark. For example in git.mirror dynamic +/// actions. Port of com.google.copybara.git.MergeResult. +/// +[StarlarkBuiltin( + "git_merge_result", + Doc = "The result returned by git merge when used in Starlark. For example in git.mirror" + + " dynamic actions.")] +public sealed class MergeResult : IStarlarkValue +{ + private readonly bool _error; + private readonly string? _errorMsg; + + private MergeResult(bool error, string? errorMsg) + { + _error = error; + _errorMsg = errorMsg; + } + + /// Create a merge result that was successful. + public static MergeResult Success() => new(false, null); + + /// Create a merge result that failed, normally due to a conflict. + public static MergeResult Error(string errorMsg) => new(true, errorMsg); + + [StarlarkMethod( + "error", + Doc = "True if the merge execution resulted in an error. False otherwise", + StructField = true)] + public bool IsError() => _error; + + [StarlarkMethod( + "error_msg", + Doc = "Error message from git if the merge resulted in a conflict/error. Users must check" + + " error field before accessing this field.", + StructField = true)] + public string? GetErrorMsg() + { + ValidationException.CheckCondition( + _error, + "Access to error_msg is forbidden for merges that don't result in an error"); + return _errorMsg; + } +} diff --git a/src/Copybara.Core/Git/NonFastForwardRepositoryException.cs b/src/Copybara.Core/Git/NonFastForwardRepositoryException.cs new file mode 100644 index 000000000..2cac4e8a0 --- /dev/null +++ b/src/Copybara.Core/Git/NonFastForwardRepositoryException.cs @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.Git; + +/// +/// This exception should be a , but sometimes it is not a user +/// error, but just a concurrency issue. In that case we prefer retries. Port of +/// com.google.copybara.git.NonFastForwardRepositoryException. +/// +public class NonFastForwardRepositoryException : RepoException +{ + public NonFastForwardRepositoryException(string message, Exception? cause) + : base(message, cause) + { + } +} diff --git a/src/Copybara.Core/Git/RebaseConflictException.cs b/src/Copybara.Core/Git/RebaseConflictException.cs new file mode 100644 index 000000000..f3d0ce19d --- /dev/null +++ b/src/Copybara.Core/Git/RebaseConflictException.cs @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.Git; + +/// +/// Indicates that the rebase failed because of a conflict. Port of +/// com.google.copybara.git.RebaseConflictException. +/// +public class RebaseConflictException : ValidationException +{ + public RebaseConflictException(string message) + : base(message) + { + } +} diff --git a/src/Copybara.Core/Git/Refspec.cs b/src/Copybara.Core/Git/Refspec.cs new file mode 100644 index 000000000..148b18617 --- /dev/null +++ b/src/Copybara.Core/Git/Refspec.cs @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; + +namespace Copybara.Git; + +/// +/// Represents a git refspec. Port of com.google.copybara.git.Refspec. +/// +public sealed class Refspec +{ + private readonly string _origin; + private readonly string _destination; + private readonly bool _allowNoFastForward; + + private Refspec(string origin, string destination, bool allowNoFastForward) + { + _origin = origin; + _destination = destination; + _allowNoFastForward = allowNoFastForward; + } + + public string GetOrigin() => _origin; + + public string GetDestination() => _destination; + + public bool IsAllowNoFastForward() => _allowNoFastForward; + + /// + /// Converts a reference from the origin to the destination reference using the refspec. + /// + /// Note that the should match the origin refspec. + /// + public string Convert(string originRef) + { + if (!_origin.Contains('*')) + { + Preconditions.CheckArgument( + originRef == _origin, "originRef=%s origin=%s", originRef, _origin); + return _destination; + } + else + { + var origSplit = _origin.Split('*'); + Preconditions.CheckState(origSplit.Length == 2); + string fromPrefix = origSplit[0]; + string fromSuffix = origSplit[1]; + Preconditions.CheckArgument( + originRef.StartsWith(fromPrefix, StringComparison.Ordinal) + && originRef.EndsWith(fromSuffix, StringComparison.Ordinal), + "originRef=%s origin=%s", + originRef, + _origin); + string middle = + originRef.Substring( + fromPrefix.Length, originRef.Length - fromSuffix.Length - fromPrefix.Length); + + var destSplit = _destination.Split('*'); + Preconditions.CheckState(destSplit.Length == 2); + string toPrefix = destSplit[0]; + string toSuffix = destSplit[1]; + return toPrefix + middle + toSuffix; + } + } + + /// + /// Tests whether a ref matches the origin pattern of the refspec. + /// + public bool MatchesOrigin(string originRef) + { + if (!_origin.Contains('*')) + { + return originRef == _origin; + } + else + { + var origSplit = _origin.Split('*'); + Preconditions.CheckState(origSplit.Length == 2); + string fromPrefix = origSplit[0]; + string fromSuffix = origSplit[1]; + return originRef.StartsWith(fromPrefix, StringComparison.Ordinal) + && originRef.EndsWith(fromSuffix, StringComparison.Ordinal); + } + } + + public Refspec WithAllowNoFastForward() => + new(_origin, _destination, allowNoFastForward: true); + + public Refspec OriginToOrigin() => new(_origin, _origin, _allowNoFastForward); + + public Refspec DestinationToDestination() => + new(_destination, _destination, _allowNoFastForward); + + public Refspec Invert() => new(_destination, _origin, _allowNoFastForward); + + /// Same as , but does not provide Location data. + public static Refspec CreateBuiltin(GitEnvironment gitEnv, string cwd, string refspecParam) => + Create(gitEnv, cwd, refspecParam); + + public static Refspec Create(GitEnvironment gitEnv, string cwd, string refspecParam) + { + if (string.IsNullOrEmpty(refspecParam)) + { + throw new InvalidRefspecException("Empty refspec is not allowed"); + } + bool allowNoFastForward = false; + string refspecStr = refspecParam; + if (refspecStr.StartsWith('+')) + { + allowNoFastForward = true; + refspecStr = refspecStr.Substring(1); + } + var elements = refspecStr.Split(':'); + if (elements.Length > 2) + { + throw new InvalidRefspecException( + "Invalid refspec. Multiple ':' found: '" + refspecParam); + } + string origin = elements[0]; + string destination = origin; + GitRepository.ValidateRefSpec(gitEnv, cwd, origin); + if (elements.Length > 1) + { + destination = elements[1]; + GitRepository.ValidateRefSpec(gitEnv, cwd, destination); + } + if (origin.Contains('*') != destination.Contains('*')) + { + throw new InvalidRefspecException( + "Wildcard only used in one part of the refspec: " + refspecParam); + } + return new Refspec(origin, destination, allowNoFastForward); + } + + public override string ToString() => + (_allowNoFastForward ? "+" : "") + _origin + ":" + _destination; +} diff --git a/src/Copybara.Core/Git/SameGitTree.cs b/src/Copybara.Core/Git/SameGitTree.cs new file mode 100644 index 000000000..d5d0a1c8b --- /dev/null +++ b/src/Copybara.Core/Git/SameGitTree.cs @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.Git; + +/// +/// A class comparing the git tree of a repo's HEAD sha1 with any sha1. Port of +/// com.google.copybara.git.SameGitTree. +/// +public sealed class SameGitTree +{ + private readonly GitRepository _repo; + private readonly string _repoUrl; + private readonly GeneralOptions _generalOptions; + private readonly bool _partialFetch; + + public SameGitTree( + GitRepository repo, string repoUrl, GeneralOptions generalOptions, bool partialFetch) + { + _repo = repo; + _repoUrl = repoUrl; + _generalOptions = generalOptions; + _partialFetch = partialFetch; + } + + private string SaveOldHead() + { + GitRevision gitRevision = _repo.GetHeadRef(); + return gitRevision.ContextReference() ?? gitRevision.GetHash(); + } + + /// + /// Compare the git tree of the repo's HEAD with the given sha1. + /// + /// It will save the current head at the repo, fetch the sha1, then compare their git trees. + /// Regardless of the checking status, the repo will be force set back to its previous head. + /// + public bool HasSameTree(string sha1) + { + string oldHead = SaveOldHead(); + try + { + using (_generalOptions.Profiler().Start("fetch_remote_sha1")) + { + _repo.Fetch( + _repoUrl, + prune: false, + force: true, + new[] { sha1 }, + _partialFetch, + depth: null, + tags: false); + return _repo.HasSameTree(sha1); + } + } + catch (Exception e) when (e is RepoException or ValidationException) + { + _generalOptions.GetConsole().WarnFmt( + "Cannot compare git tree of head %s with sha1 %s.", oldHead, sha1); + } + finally + { + _repo.ForceCheckout(oldHead); + } + return false; + } +} diff --git a/src/Copybara.Core/Hashing/HashingModule.cs b/src/Copybara.Core/Hashing/HashingModule.cs new file mode 100644 index 000000000..4a949af9c --- /dev/null +++ b/src/Copybara.Core/Hashing/HashingModule.cs @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Security.Cryptography; +using System.Text; +using Copybara.Config; +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Hashing; + +/// Hashing utilities. +[StarlarkBuiltin("hashing", Doc = "utilities for hashing")] +public class HashingModule : IStarlarkValue +{ + [StarlarkMethod( + "path_md5_sum", + Doc = + "Return the md5 hash of a file at a checkout path. Do not use unless working with legacy" + + " systems that require MD5.\n" + + "WARNING: do not use unless working with legacy systems that require MD5")] + public string PathMd5Sum( + [Param(Name = "path", Doc = "checkout path pointing to a file to be hashed")] + CheckoutPath path) + { + using var hashFunc = MD5.Create(); + return HashFile(path.FullPath(), hashFunc); + } + + [StarlarkMethod( + "path_sha256_sum", + Doc = "Return the sha256 hash of a file at a checkout path")] + public string PathSha256Sum( + [Param(Name = "path", Doc = "checkout path pointing to a file to be hashed")] + CheckoutPath path) + { + using var hashFunc = SHA256.Create(); + return HashFile(path.FullPath(), hashFunc); + } + + [StarlarkMethod( + "str_sha256_sum", + Doc = "Return the hash of a list of objects based on the algorithm specified")] + public string HashStringWithSha256( + [Param( + Name = "input", + Named = true, + Doc = "One or more string inputs to hash.", + AllowedTypes = new[] { typeof(ISequence), typeof(string) })] + object input) + { + if (input is string singleInput) + { + return ToHex(SHA256.HashData(Encoding.UTF8.GetBytes(singleInput))); + } + + var stringInputs = SkylarkUtil.ConvertStringList(input, "input"); + if (stringInputs.Count == 0) + { + throw new ValidationException( + "hashing.hash_str_with_sha256 cannot be called with an empty object list."); + } + + using var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + foreach (string stringInput in stringInputs) + { + hasher.AppendData(Encoding.UTF8.GetBytes(stringInput)); + } + return ToHex(hasher.GetHashAndReset()); + } + + private static string HashFile(string hashPath, HashAlgorithm hashFunc) + { + using var stream = File.OpenRead(hashPath); + return ToHex(hashFunc.ComputeHash(stream)); + } + + private static string ToHex(byte[] bytes) => Convert.ToHexStringLower(bytes); +} diff --git a/src/Copybara.Core/Html/HtmlElement.cs b/src/Copybara.Core/Html/HtmlElement.cs new file mode 100644 index 000000000..4ae9cee67 --- /dev/null +++ b/src/Copybara.Core/Html/HtmlElement.cs @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Xml; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Html; + +/// Object to hold an HTML element. +[StarlarkBuiltin("html_element", Doc = "A HTML element.")] +public sealed class HtmlElement : IStarlarkValue +{ + private readonly XmlElement _element; + + public HtmlElement(XmlElement element) + { + _element = element; + } + + [StarlarkMethod("attr", Doc = "Get an attribute value by key")] + public string Attr( + [Param(Name = "key", Doc = "the (case-sensitive) attribute key")] string key) => + // jsoup returns the empty string when the attribute is absent. + _element.GetAttribute(key); +} diff --git a/src/Copybara.Core/Html/HtmlModule.cs b/src/Copybara.Core/Html/HtmlModule.cs new file mode 100644 index 000000000..e9a510101 --- /dev/null +++ b/src/Copybara.Core/Html/HtmlModule.cs @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Xml; +using System.Xml.XPath; +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Html; + +/// Starlark methods for working with HTML content. +/// +/// NOTE(port): upstream uses jsoup, which provides a full lenient HTML5 parser. To avoid adding a +/// NuGet dependency, this port uses a minimal tolerant HTML-to-XML conversion (see +/// ) and evaluates XPath with the in-box API. +/// GAPS (TODO): jsoup's full HTML5 tree construction (implied tags, error recovery, entity table, +/// namespace handling) is not reproduced. Only reasonably well-formed HTML with balanced tags is +/// handled; malformed markup that jsoup would repair may fail to parse or select here. +/// +[StarlarkBuiltin("html", Doc = "Set of functions to work with HTML in copybara")] +public sealed class HtmlModule : IStarlarkValue +{ + // Void (self-closing) HTML elements that never have a closing tag. + private static readonly HashSet VoidElements = new(StringComparer.OrdinalIgnoreCase) + { + "area", "base", "br", "col", "embed", "hr", "img", "input", + "link", "meta", "param", "source", "track", "wbr", + }; + + [StarlarkMethod( + "xpath", + Doc = "Run an xpath expression on HTML content to select elements. This only supports" + + " a subset of xpath expressions.")] + public IReadOnlyList SelectElements( + [Param(Name = "content", Doc = "The HTML content", Named = true)] + string htmlContent, + [Param(Name = "expression", Doc = "XPath expression to select elements", Named = true)] + string expression) + { + XmlDocument doc; + try + { + doc = HtmlToXml(htmlContent); + } + catch (XmlException e) + { + throw new ValidationException("Error parsing HTML", e); + } + + var results = new List(); + try + { + var navigator = doc.CreateNavigator()!; + XPathNodeIterator iterator = navigator.Select(expression); + while (iterator.MoveNext()) + { + if (iterator.Current?.UnderlyingObject is XmlElement element) + { + results.Add(new HtmlElement(element)); + } + } + } + catch (XPathException e) + { + throw new ValidationException("Error evaluating XPath expression", e); + } + + return results; + } + + /// + /// Converts (best-effort) an HTML string into an . This is a minimal + /// tolerant parser: it wraps content in a synthetic root, treats known void elements as + /// self-closing, and drops doctype/comments. It is not a full HTML5 parser. + /// + private static XmlDocument HtmlToXml(string html) + { + string xml = Normalize(html); + var doc = new XmlDocument { XmlResolver = null }; + doc.LoadXml("<__root__>" + xml + ""); + return doc; + } + + private static string Normalize(string html) + { + var sb = new System.Text.StringBuilder(html.Length + 16); + int i = 0; + int n = html.Length; + while (i < n) + { + char c = html[i]; + if (c != '<') + { + // Escape stray ampersands that are not part of an entity, and bare '>'. + if (c == '&' && !IsEntityStart(html, i)) + { + sb.Append("&"); + } + else + { + sb.Append(c); + } + i++; + continue; + } + + // Handle comments, doctype, CDATA, declarations: drop them. + if (html.AsSpan(i).StartsWith("", i + 4, StringComparison.Ordinal); + i = end < 0 ? n : end + 3; + continue; + } + if (i + 1 < n && (html[i + 1] == '!' || html[i + 1] == '?')) + { + int end = html.IndexOf('>', i); + i = end < 0 ? n : end + 1; + continue; + } + + int tagEnd = html.IndexOf('>', i); + if (tagEnd < 0) + { + // Unterminated tag; escape the '<' and continue. + sb.Append("<"); + i++; + continue; + } + + string tag = html.Substring(i, tagEnd - i + 1); + sb.Append(RewriteTag(tag)); + i = tagEnd + 1; + } + + return sb.ToString(); + } + + private static bool IsEntityStart(string html, int ampIndex) + { + int semi = html.IndexOf(';', ampIndex + 1); + if (semi < 0 || semi - ampIndex > 12) + { + return false; + } + for (int j = ampIndex + 1; j < semi; j++) + { + char c = html[j]; + if (!char.IsLetterOrDigit(c) && c != '#') + { + return false; + } + } + return semi > ampIndex + 1; + } + + private static string RewriteTag(string tag) + { + // Closing tag. + if (tag.StartsWith("", StringComparison.Ordinal); + if (VoidElements.Contains(name) && !alreadySelfClosing) + { + return tag.Substring(0, tag.Length - 1) + "/>"; + } + + return tag; + } +} diff --git a/src/Copybara.Core/Http/Auth/AuthInterceptor.cs b/src/Copybara.Core/Http/Auth/AuthInterceptor.cs new file mode 100644 index 000000000..d17c09573 --- /dev/null +++ b/src/Copybara.Core/Http/Auth/AuthInterceptor.cs @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using Copybara.Common; +using Starlark.Eval; + +namespace Copybara.Http.Auth; + +/// Interface for adding auth headers to requests. +public interface IAuthInterceptor : IStarlarkValue +{ + /// Self-description for all used credentials. + IReadOnlyList> DescribeCredentials(); + + /// + /// Returns an action that mutates an outgoing request to add authentication. + /// + /// The Java version returns a Google http-client + /// HttpExecuteInterceptor; here we return an + /// applied to the just before it is sent. + /// + /// If credential retrieval fails. + /// If credential issuing fails. + Action Interceptor(); +} diff --git a/src/Copybara.Core/Http/Auth/BearerInterceptor.cs b/src/Copybara.Core/Http/Auth/BearerInterceptor.cs new file mode 100644 index 000000000..f6f2c8942 --- /dev/null +++ b/src/Copybara.Core/Http/Auth/BearerInterceptor.cs @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Net.Http; +using System.Net.Http.Headers; +using Copybara.Common; +using Copybara.Credentials; + +namespace Copybara.Http.Auth; + +/// An interceptor for adding Bearer authentication to an HTTP request. +public class BearerInterceptor : IAuthInterceptor +{ + private readonly CredentialIssuer _issuer; + + public BearerInterceptor(CredentialIssuer issuer) + { + _issuer = Preconditions.CheckNotNull(issuer); + } + + public Action Interceptor() + { + string token = _issuer.Issue().ProvideSecret(); + return req => req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + + public IReadOnlyList> DescribeCredentials() => + ImmutableArray.Create(MultimapConversions.ToListMultimap(_issuer.Describe())); +} diff --git a/src/Copybara.Core/Http/Auth/MultimapConversions.cs b/src/Copybara.Core/Http/Auth/MultimapConversions.cs new file mode 100644 index 000000000..24e9bda6b --- /dev/null +++ b/src/Copybara.Core/Http/Auth/MultimapConversions.cs @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; + +namespace Copybara.Http; + +/// +/// Helpers to bridge the two multimap shapes used across the port. Credentials describe themselves +/// with (matching Guava's +/// ImmutableSetMultimap), while endpoints/config-item description use +/// . +/// +internal static class MultimapConversions +{ + public static ImmutableListMultimap ToListMultimap( + ImmutableSetMultimap setMultimap) + { + var builder = ImmutableListMultimap.CreateBuilder(); + foreach (var entry in setMultimap) + { + builder.Put(entry.Key, entry.Value); + } + + return builder.Build(); + } +} diff --git a/src/Copybara.Core/Http/Auth/UsernamePasswordInterceptor.cs b/src/Copybara.Core/Http/Auth/UsernamePasswordInterceptor.cs new file mode 100644 index 000000000..a5402679a --- /dev/null +++ b/src/Copybara.Core/Http/Auth/UsernamePasswordInterceptor.cs @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Copybara.Common; +using Copybara.Credentials; + +namespace Copybara.Http.Auth; + +/// Representation for authentication information for an http request. +public class UsernamePasswordInterceptor : IAuthInterceptor +{ + private readonly UsernamePasswordIssuer _issuer; + + public UsernamePasswordInterceptor(UsernamePasswordIssuer issuer) + { + _issuer = Preconditions.CheckNotNull(issuer); + } + + public Action Interceptor() + { + string un = _issuer.Username.Issue().ProvideSecret(); + string pwd = _issuer.Password.Issue().ProvideSecret(); + string encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{un}:{pwd}")); + return req => req.Headers.Authorization = new AuthenticationHeaderValue("Basic", encoded); + } + + public IReadOnlyList> DescribeCredentials() => + _issuer.DescribeCredentials() + .Select(MultimapConversions.ToListMultimap) + .ToList(); +} diff --git a/src/Copybara.Core/Http/Endpoint/HttpContentInterceptor.cs b/src/Copybara.Core/Http/Endpoint/HttpContentInterceptor.cs new file mode 100644 index 000000000..8445983be --- /dev/null +++ b/src/Copybara.Core/Http/Endpoint/HttpContentInterceptor.cs @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace Copybara.Http.Endpoint; + +/// +/// Intercepts HTTP content and resolves secrets. +/// +/// The Java version implements Google http-client's streaming HttpContent. Here we +/// materialize the wrapped content to a UTF-8 string, resolve any ${{issuer}} templates, +/// and expose the result as a new that preserves the original +/// content type headers. +/// +internal static class HttpContentInterceptor +{ + public static HttpContent Wrap(HttpContent content, HttpSecretInterceptor secretInterceptor) + { + string httpContent = content.ReadAsStringAsync().GetAwaiter().GetResult(); + string outputStr = secretInterceptor.ResolveStringSecrets(httpContent); + + var resolved = new ByteArrayContent(Encoding.UTF8.GetBytes(outputStr)); + // Preserve content-type and other content headers from the original content. + foreach (var header in content.Headers) + { + resolved.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return resolved; + } +} diff --git a/src/Copybara.Core/Http/Endpoint/HttpEndpoint.cs b/src/Copybara.Core/Http/Endpoint/HttpEndpoint.cs new file mode 100644 index 000000000..1ba3eb78f --- /dev/null +++ b/src/Copybara.Core/Http/Endpoint/HttpEndpoint.cs @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Net.Http; +using Copybara.Checks; +using Copybara.Common; +using Copybara.Config; +using Copybara.Credentials; +using Copybara.Exceptions; +using Copybara.Http.Auth; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Http.Endpoint; + +/// +/// Endpoint capable of making http requests. +/// +/// This endpoint is currently bound to a specific host, as a security restriction. +/// +[StarlarkBuiltin("http_endpoint", Doc = "Calls via HTTP.")] +public class HttpEndpoint : IEndpoint +{ + // Immutable map keeping the issuers as key value pairs. The key is used to identify the issuer + // before executing the http call. + private readonly ImmutableDictionary _issuers; + + private readonly ImmutableDictionary _hosts; + private readonly HttpClient _transport; + private readonly Console _console; + private readonly IChecker? _checker; + + /// Whether to automatically follow redirects, true by default. + private bool _followRedirects = true; + + public HttpEndpoint( + Console console, + HttpClient transport, + ImmutableDictionary hosts, + ImmutableDictionary issuers, + IChecker? checker) + { + _hosts = hosts; + _transport = transport; + _console = console; + _checker = checker; + _issuers = issuers; + } + + [StarlarkMethod("get", Doc = "Execute a get request")] + public HttpEndpointResponse Get( + [Param(Name = "url", Named = true, AllowedTypes = new[] { typeof(string) })] string url, + [Param( + Name = "headers", + Named = true, + Positional = false, + AllowedTypes = new[] { typeof(Dict) }, + DefaultValue = "{}", + Doc = "dict of http headers for the request")] + object headers, + [Param( + Name = "auth", + Named = true, + Positional = false, + DefaultValue = "False", + AllowedTypes = new[] { typeof(bool) })] + bool auth) => HandleRequest(url, "GET", headers, null, auth); + + [StarlarkMethod("post", Doc = "Execute a post request")] + public HttpEndpointResponse Post( + [Param(Name = "url", Named = true, AllowedTypes = new[] { typeof(string) })] string urlIn, + [Param( + Name = "headers", + Named = true, + Positional = false, + AllowedTypes = new[] { typeof(Dict) }, + DefaultValue = "{}", + Doc = "dict of http headers for the request")] + object headersIn, + [Param( + Name = "content", + Named = true, + Positional = false, + DefaultValue = "None", + AllowedTypes = new[] { typeof(IHttpEndpointBody), typeof(NoneType) })] + object? content, + [Param(Name = "auth", Named = true, Positional = false, DefaultValue = "False")] + bool auth) => HandleRequest(urlIn, "POST", headersIn, content, auth); + + [StarlarkMethod("delete", Doc = "Execute a delete request")] + public HttpEndpointResponse Delete( + [Param(Name = "url", Named = true, AllowedTypes = new[] { typeof(string) })] string urlIn, + [Param( + Name = "headers", + Named = true, + Positional = false, + AllowedTypes = new[] { typeof(Dict) }, + DefaultValue = "{}", + Doc = "dict of http headers for the request")] + object headersIn, + [Param( + Name = "auth", + Named = true, + Positional = false, + DefaultValue = "False", + AllowedTypes = new[] { typeof(bool) })] + bool auth) => HandleRequest(urlIn, "DELETE", headersIn, null, auth); + + private HttpEndpointResponse HandleRequest( + string urlIn, string method, object headersIn, object? endpointContentIn, bool auth) + { + var url = new Uri(urlIn); + ValidateUrl(url); + + var secretInterceptor = new HttpSecretInterceptor(_issuers); + var headersDict = SkylarkUtil.ConvertStringMap(headersIn, "headers"); + var headers = new List>(); + foreach (var e in headersDict) + { + string val = secretInterceptor.ResolveStringSecrets(e.Value); + headers.Add(new KeyValuePair(e.Key, val)); + } + + IHttpEndpointBody? endpointContent = + SkylarkUtil.ConvertFromNoneable(endpointContentIn, null); + HttpContent? content = null; + if (endpointContent != null) + { + content = HttpContentInterceptor.Wrap(endpointContent.GetContent(), secretInterceptor); + } + + IAuthInterceptor? creds = null; + if (auth) + { + if (!_hosts.TryGetValue(url.Host, out creds) || creds == null) + { + throw new EvalException( + $"Autentication was requested, but no creds provided for {url}"); + } + } + + var req = new HttpEndpointRequest( + url, method, headers, content, auth && creds != null ? creds : null); + + if (_checker != null) + { + _checker.DoCheck( + ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("url", url.ToString()), + new KeyValuePair( + "headers", string.Join(", ", headers.Select(h => $"{h.Key}: {h.Value}"))), + }), + _console); + endpointContent?.CheckContent(_checker, _console); + } + + var request = req.Build(); + // Redirect handling is configured on the transport's HttpClientHandler (see HttpOptions). + var response = _transport.SendAsync(request).GetAwaiter().GetResult(); + return new HttpEndpointResponse(response); + } + + public void ValidateUrl(Uri url) + { + ValidationException.CheckCondition( + _hosts.ContainsKey(url.Host), + "Illegal host: url host {0} matches none of endpoint hosts {{{1}}}", + url.Host, + string.Join(",", _hosts.Keys)); + } + + [StarlarkMethod("followRedirects", Doc = "Sets whether to follow redirects automatically")] + public void SetFollowRedirects( + [Param( + Name = "followRedirects", + Doc = "Whether to follow redirects automatically", + AllowedTypes = new[] { typeof(bool) })] + bool followRedirects) => _followRedirects = followRedirects; + + public ImmutableListMultimap Describe() + { + var builder = ImmutableListMultimap.CreateBuilder(); + builder.Put("type", "http_endpoint"); + builder.PutAll("host", _hosts.Keys); + return builder.Build(); + } + + public IReadOnlyList> DescribeCredentials() + { + var list = ImmutableArray.CreateBuilder>(); + + // Add credentials from auth interceptors for hosts. + foreach (var entry in _hosts) + { + if (entry.Value == null) + { + continue; + } + + foreach (var credEntry in entry.Value.DescribeCredentials()) + { + var describe = ImmutableListMultimap.CreateBuilder(); + describe.PutAll(credEntry); + describe.PutAll("host", new[] { entry.Key }); + list.Add(describe.Build()); + } + } + + // Add credentials from issuers used by secret interceptors. + foreach (var entry in _issuers) + { + var describe = ImmutableListMultimap.CreateBuilder(); + describe.Put("key", entry.Key); + foreach (var credEntry in entry.Value.Describe()) + { + describe.Put(credEntry.Key, credEntry.Value); + } + + list.Add(describe.Build()); + } + + return list.ToImmutable(); + } +} diff --git a/src/Copybara.Core/Http/Endpoint/HttpEndpointBody.cs b/src/Copybara.Core/Http/Endpoint/HttpEndpointBody.cs new file mode 100644 index 000000000..eda2503eb --- /dev/null +++ b/src/Copybara.Core/Http/Endpoint/HttpEndpointBody.cs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using Copybara.Checks; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Http.Endpoint; + +/// An object containing the content portion of an http request. +public interface IHttpEndpointBody +{ + /// + /// Returns the .NET for this body. Replaces the Google http-client + /// HttpContent used by the Java implementation. + /// + HttpContent GetContent(); + + // TODO(port): confirm IChecker / CheckerException names once Copybara.Checks lands. + void CheckContent(IChecker checker, Console console) => + throw new CheckerException("checker not implemented for this content type"); +} diff --git a/src/Copybara.Core/Http/Endpoint/HttpEndpointRequest.cs b/src/Copybara.Core/Http/Endpoint/HttpEndpointRequest.cs new file mode 100644 index 000000000..cfe484084 --- /dev/null +++ b/src/Copybara.Core/Http/Endpoint/HttpEndpointRequest.cs @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using System.Net.Http; +using Copybara.Http.Auth; +using Starlark.Eval; + +namespace Copybara.Http.Endpoint; + +/// +/// Holder for all the data required to make a request. It has a method that builds the underlying +/// . +/// +/// The Java version relies on Google http-client's HttpTransport/HttpRequest; +/// here requests are constructed as and executed via a +/// . +/// +public class HttpEndpointRequest : IStarlarkValue +{ + // Request parameters. + private readonly Uri _url; + private readonly HttpMethod _method; + private readonly IReadOnlyList> _headers; + private readonly HttpContent? _content; + private readonly IAuthInterceptor? _auth; + + private HttpRequestMessage? _request; + + public HttpEndpointRequest( + Uri url, + string method, + IReadOnlyList> headers, + HttpContent? content, + IAuthInterceptor? auth) + { + _url = url; + _method = new HttpMethod(method); + _headers = headers; + _content = content; + _auth = auth; + } + + /// If credential retrieval fails. + /// If credential issuing fails. + public HttpRequestMessage Build() + { + if (_request == null) + { + var request = new HttpRequestMessage(_method, _url); + if (_content != null) + { + request.Content = _content; + } + + foreach (var header in _headers) + { + if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value) + && request.Content != null) + { + request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + if (_auth != null) + { + _auth.Interceptor()(request); + } + + _request = request; + } + + return _request; + } +} diff --git a/src/Copybara.Core/Http/Endpoint/HttpEndpointResponse.cs b/src/Copybara.Core/Http/Endpoint/HttpEndpointResponse.cs new file mode 100644 index 000000000..1dda92be0 --- /dev/null +++ b/src/Copybara.Core/Http/Endpoint/HttpEndpointResponse.cs @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Http.Endpoint; + +/// Simple object to read an http response. +[StarlarkBuiltin("http_response", Doc = "A http response.")] +public class HttpEndpointResponse : IStarlarkValue +{ + private readonly HttpResponseMessage _response; + + public HttpEndpointResponse(HttpResponseMessage response) + { + _response = response; + } + + [StarlarkMethod("code", Doc = "http status code")] + public int GetStatusCode() => (int)_response.StatusCode; + + [StarlarkMethod("status", Doc = "http status message")] + public string GetStatusMessage() => _response.ReasonPhrase ?? _response.StatusCode.ToString(); + + [StarlarkMethod("contents_string", Doc = "response contents as string")] + public string ResponseAsString() => _response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + [StarlarkMethod( + "header", + Doc = "Returns the value of the response header specified by the field name")] + public IReadOnlyList ResponseHeader( + [Param(Name = "key", Named = true, AllowedTypes = new[] { typeof(string) })] string key) + { + var values = new List(); + if (_response.Headers.TryGetValues(key, out var headerValues)) + { + values.AddRange(headerValues); + } + + if (_response.Content.Headers.TryGetValues(key, out var contentValues)) + { + values.AddRange(contentValues); + } + + return values; + } + + [StarlarkMethod( + "download", + Doc = "Writes the content of the HTTP response into the given destination path")] + public void Download( + [Param(Name = "path", Doc = "The destination Path")] CheckoutPath path) + { + using var output = File.Create(path.FullPath()); + using var input = _response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + input.CopyTo(output); + } +} diff --git a/src/Copybara.Core/Http/Endpoint/HttpSecretInterceptor.cs b/src/Copybara.Core/Http/Endpoint/HttpSecretInterceptor.cs new file mode 100644 index 000000000..2d743804d --- /dev/null +++ b/src/Copybara.Core/Http/Endpoint/HttpSecretInterceptor.cs @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Copybara.Credentials; + +namespace Copybara.Http.Endpoint; + +/// HttpSecretInterceptor replaces secrets with their corresponding values. +internal class HttpSecretInterceptor +{ + private static readonly Regex Template = new(@"\$\{\{(.*?)\}\}"); + private static readonly Regex EncodedTemplate = new(@"\%24\%7B\%7B(.*?)\%7D\%7D"); + + private readonly ImmutableDictionary _issuers; + + public HttpSecretInterceptor(ImmutableDictionary issuers) + { + _issuers = issuers; + } + + public string ResolveStringSecrets(string value) + { + value = ReplaceMatchesWithSecrets(value, Template); + value = ReplaceMatchesWithSecrets(value, EncodedTemplate); + return value; + } + + private string ReplaceMatchesWithSecrets(string value, Regex pattern) + { + foreach (Match match in pattern.Matches(value)) + { + string issuerName = match.Groups[1].Value; + if (!_issuers.TryGetValue(issuerName, out var issuer)) + { + throw new ArgumentException($"Credential issuer {issuerName} is not found"); + } + + value = value.Replace(match.Groups[0].Value, issuer.Issue().ProvideSecret()); + } + + return value; + } +} diff --git a/src/Copybara.Core/Http/HttpModule.cs b/src/Copybara.Core/Http/HttpModule.cs new file mode 100644 index 000000000..e29c46d34 --- /dev/null +++ b/src/Copybara.Core/Http/HttpModule.cs @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using Copybara.Checks; +using Copybara.Config; +using Copybara.Credentials; +using Copybara.Exceptions; +using Copybara.Http.Auth; +using Copybara.Http.Endpoint; +using Copybara.Http.Json; +using Copybara.Http.Multipart; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; +using StarlarkList = Starlark.Eval.StarlarkList; + +namespace Copybara.Http; + +/// Starlark methods for working with the http endpoint. +[StarlarkBuiltin("http", Doc = "Module for working with http endpoints.")] +public class HttpModule : IStarlarkValue +{ + private readonly Console _console; + private readonly HttpOptions _options; + + public HttpModule(Console console, HttpOptions options) + { + _console = console; + _options = options; + } + + [StarlarkMethod("url_encode", Doc = "URL-encode the input string")] + public string UrlEncode( + [Param(Name = "input", Doc = "The string to be encoded.", + AllowedTypes = new[] { typeof(string) })] + string input) => Uri.EscapeDataString(input); + + [StarlarkMethod("trigger", Doc = "Trigger for http endpoint")] + public ITrigger Trigger( + [Param( + Name = "hosts", + Doc = "A list of hosts to allow HTTP traffic to.", + Named = true, + AllowedTypes = new[] { typeof(StarlarkList) }, + DefaultValue = "[]", + Positional = false)] + StarlarkList hosts, + [Param( + Name = "issuers", + Doc = "A dictionary of credential issuers.", + Named = true, + AllowedTypes = new[] { typeof(Dict), typeof(NoneType) }, + DefaultValue = "{}", + Positional = false)] + object? issuers, + [Param( + Name = "checker", + AllowedTypes = new[] { typeof(IChecker), typeof(NoneType) }, + DefaultValue = "None", + Doc = "A checker that will check calls made by the endpoint", + Named = true, + Positional = false)] + object? checkerIn) + { + var endpoint = new HttpEndpoint( + _console, + _options.GetTransport(), + BuildHostsMapWithAuthInterceptor(hosts), + BuildIssuersMap(issuers), + SkylarkUtil.ConvertFromNoneable(checkerIn, null)); + return new HttpTrigger(endpoint); + } + + [StarlarkMethod( + "endpoint", + Doc = "Endpoint that executes any sort of http request. Currently restricted" + + "to requests to specific hosts.")] + public EndpointProvider Endpoint( + [Param( + Name = "host", + Doc = "DEPRECATED. A single host to allow HTTP traffic to.", + Named = true, + AllowedTypes = new[] { typeof(string) }, + DefaultValue = "''", + Positional = false)] + string? host, + [Param( + Name = "checker", + AllowedTypes = new[] { typeof(IChecker), typeof(NoneType) }, + DefaultValue = "None", + Doc = "A checker that will check calls made by the endpoint", + Named = true, + Positional = false)] + object? checkerIn, + [Param( + Name = "hosts", + Doc = "A list of hosts to allow HTTP traffic to.", + Named = true, + AllowedTypes = new[] { typeof(StarlarkList) }, + DefaultValue = "[]", + Positional = false)] + StarlarkList hosts, + [Param( + Name = "issuers", + Doc = "A dictionaty of credential issuers.", + Named = true, + AllowedTypes = new[] { typeof(Dict), typeof(NoneType) }, + DefaultValue = "{}", + Positional = false)] + object? issuers) + { + IChecker? checker = SkylarkUtil.ConvertFromNoneable(checkerIn, null); + var h = BuildHostsMapBuilder(hosts); + if (!string.IsNullOrEmpty(host)) + { + h[host] = null; + } + + return EndpointProvider.Wrap( + new HttpEndpoint( + _console, + _options.GetTransport(), + h.ToImmutableDictionary(), + BuildIssuersMap(issuers), + checker)); + } + + private ImmutableDictionary BuildIssuersMap(object? issuers) + { + var issuersMap = ImmutableDictionary.CreateBuilder(); + if (issuers is Dict dict) + { + foreach (var entry in dict.Entries) + { + issuersMap[(string)entry.Key!] = (CredentialIssuer)entry.Value!; + } + } + + return issuersMap.ToImmutable(); + } + + private ImmutableDictionary BuildHostsMapWithAuthInterceptor( + StarlarkList hosts) => BuildHostsMapBuilder(hosts).ToImmutableDictionary(); + + private Dictionary BuildHostsMapBuilder(StarlarkList hosts) + { + var h = new Dictionary(); + foreach (object? o in hosts) + { + if (o is HostCredential withCred) + { + h[withCred.Host] = withCred.Creds; + } + else + { + h[(string)o!] = null; + } + } + + return h; + } + + [StarlarkMethod("urlencoded_form", Doc = "Creates a url-encoded form HTTP body.")] + public HttpEndpointUrlEncodedFormContent UrlEncodedFormContent( + [Param( + Name = "body", + Doc = "HTTP body object, property name will be used as key and value as value.", + AllowedTypes = new[] { typeof(Dict) }, + DefaultValue = "{}")] + object body) => new(body); + + [StarlarkMethod("multipart_form", Doc = "Creates a multipart form http body.")] + public HttpEndpointMultipartFormContent MultipartFormContent( + [Param( + Name = "parts", + Doc = "A list of form parts", + AllowedTypes = new[] { typeof(StarlarkList) }, + DefaultValue = "[]")] + StarlarkList partsIn) + { + var parts = Sequence.Cast(partsIn, "parts").ToList(); + return new HttpEndpointMultipartFormContent(parts); + } + + [StarlarkMethod("multipart_form_text", Doc = "Create a text/plain part for a multipart form payload")] + public IHttpEndpointFormPart MultipartFormTextField( + [Param(Name = "name", Doc = "The name of the form field.", + AllowedTypes = new[] { typeof(string) })] + string name, + [Param(Name = "text", Doc = "The form value of the field", + AllowedTypes = new[] { typeof(string) })] + string text) => new TextPart(name, text); + + [StarlarkMethod("multipart_form_file", Doc = "Create a file part for a multipart form payload.")] + public IHttpEndpointFormPart MultipartFormFileField( + [Param(Name = "name", Doc = "The name of the form field.", + AllowedTypes = new[] { typeof(string) })] + string name, + [Param(Name = "path", + Doc = "The checkout path pointing to the file to use as the field value.", + AllowedTypes = new[] { typeof(CheckoutPath) })] + CheckoutPath path, + [Param( + Name = "content_type", + Doc = "Content type header value for the form part. " + + "Defaults to application/octet-stream. \n" + + "https://www.w3.org/Protocols/rfc1341/4_Content-Type.html", + AllowedTypes = new[] { typeof(string) }, + Named = true, + Positional = false, + DefaultValue = "\"application/octet-stream\"")] + string contentType, + [Param( + Name = "filename", + Doc = "The filename that will be sent along with the data. " + + "Defaults to the filename of the path parameter. " + + "Sets the filename parameter in the content disposition " + + "header. \n" + + "https://www.w3.org/Protocols/HTTP/Issues/content-disposition.txt", + AllowedTypes = new[] { typeof(string), typeof(NoneType) }, + Named = true, + Positional = false, + DefaultValue = "None")] + object? filenameIn) + { + string? filename = SkylarkUtil.ConvertOptionalString(filenameIn); + return new FilePart(name, path.FullPath(), contentType, filename); + } + + [StarlarkMethod("json", Doc = "Creates a JSON HTTP body.")] + public HttpEndpointJsonContent JsonContent( + [Param( + Name = "body", + Doc = "HTTP body object, property name will be used as key and value as value.", + AllowedTypes = new[] { typeof(object) }, + DefaultValue = "{}")] + object? body) => new(body); + + [StarlarkMethod("host", Doc = "Wraps a host and potentially credentials for http auth.")] + public HostCredential Host( + [Param( + Name = "host", + Doc = "The host to be contacted.", + Named = true, + AllowedTypes = new[] { typeof(string) }, + Positional = false)] + string host, + [Param( + Name = "auth", + Doc = "Optional, an interceptor for providing credentials. Also accepts a " + + "username_password.", + Named = true, + DefaultValue = "None", + AllowedTypes = new[] + { + typeof(IAuthInterceptor), + typeof(UsernamePasswordIssuer), + typeof(NoneType), + }, + Positional = false)] + object? maybeCreds) + { + if (maybeCreds is UsernamePasswordIssuer upi) + { + maybeCreds = new UsernamePasswordInterceptor(upi); + } + + IAuthInterceptor? creds = SkylarkUtil.ConvertFromNoneable(maybeCreds, null); + return new HostCredential(host, creds); + } + + [StarlarkMethod("username_password_auth", Doc = "Authentication via username and password.")] + public UsernamePasswordInterceptor UsernamePasswordAuth( + [Param( + Name = "creds", + Doc = "The username and password credentials.", + Named = true, + AllowedTypes = new[] { typeof(UsernamePasswordIssuer) }, + Positional = false)] + UsernamePasswordIssuer creds) => new(creds); + + [StarlarkMethod("bearer_auth", Doc = "Authentication via a bearer token.")] + public BearerInterceptor BearerAuth( + [Param( + Name = "creds", + Doc = "The token credentials.", + Named = true, + AllowedTypes = new[] { typeof(CredentialIssuer) }, + Positional = false)] + CredentialIssuer creds) => new(creds); + + /// A host paired with optional auth credentials. + public sealed class HostCredential : IStarlarkValue + { + public HostCredential(string host, IAuthInterceptor? creds) + { + Host = host; + Creds = creds; + } + + public string Host { get; } + + public IAuthInterceptor? Creds { get; } + } +} diff --git a/src/Copybara.Core/Http/HttpOptions.cs b/src/Copybara.Core/Http/HttpOptions.cs new file mode 100644 index 000000000..dcb3ba6d9 --- /dev/null +++ b/src/Copybara.Core/Http/HttpOptions.cs @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; + +namespace Copybara.Http; + +/// +/// Options relating to the http endpoint. +/// +/// The Java version exposes a Google http-client HttpTransport; this port exposes a +/// (from System.Net.Http) as the transport. +/// +public class HttpOptions : IOption +{ + private HttpClient? _transport; + + /// If the transport cannot be created. + public virtual HttpClient GetTransport() + { + return _transport ??= new HttpClient(new HttpClientHandler + { + // Follow-redirect behavior is decided per HttpEndpoint request; enable auto-redirect on + // the handler and let the endpoint gate it. The default matches the Java default (true). + AllowAutoRedirect = true, + }); + } +} diff --git a/src/Copybara.Core/Http/HttpTrigger.cs b/src/Copybara.Core/Http/HttpTrigger.cs new file mode 100644 index 000000000..6f5917c2a --- /dev/null +++ b/src/Copybara.Core/Http/HttpTrigger.cs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; + +namespace Copybara.Http; + +/// HttpTrigger helps working with http origins. +public class HttpTrigger : ITrigger +{ + private readonly IEndpoint _endpoint; + + public HttpTrigger(IEndpoint endpoint) + { + _endpoint = Preconditions.CheckNotNull(endpoint); + } + + public IEndpoint GetEndpoint() => _endpoint; + + public ImmutableListMultimap Describe() => _endpoint.Describe(); +} diff --git a/src/Copybara.Core/Http/Json/HttpEndpointJsonContent.cs b/src/Copybara.Core/Http/Json/HttpEndpointJsonContent.cs new file mode 100644 index 000000000..ff1e3439b --- /dev/null +++ b/src/Copybara.Core/Http/Json/HttpEndpointJsonContent.cs @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using System.Text; +using Copybara.Http.Endpoint; +using Starlark.Eval; + +namespace Copybara.Http.Json; + +/// Constructs data for an HTTP request containing JSON data payload. +public class HttpEndpointJsonContent : IHttpEndpointBody, IStarlarkValue +{ + private readonly object? _data; + private HttpContent? _body; + + public HttpEndpointJsonContent(object? data) + { + _data = data; + } + + public HttpContent GetContent() + { + if (_body == null) + { + // The Java version uses gson; here we serialize Starlark values with a small + // recursive converter and produce an application/json body (System.Text.Json). + string json = StarlarkJson.Serialize(_data); + _body = new StringContent(json, Encoding.UTF8, "application/json"); + } + + return _body; + } +} diff --git a/src/Copybara.Core/Http/Json/StarlarkJson.cs b/src/Copybara.Core/Http/Json/StarlarkJson.cs new file mode 100644 index 000000000..036fb531f --- /dev/null +++ b/src/Copybara.Core/Http/Json/StarlarkJson.cs @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json; +using Starlark.Eval; + +namespace Copybara.Http.Json; + +/// +/// Serializes Starlark values to JSON. The Java implementation relies on the Google http-client +/// gson factory; this port walks the Starlark value graph and emits JSON via +/// System.Text.Json. +/// +internal static class StarlarkJson +{ + public static string Serialize(object? data) + { + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream)) + { + Write(writer, data); + } + + return System.Text.Encoding.UTF8.GetString(stream.ToArray()); + } + + private static void Write(Utf8JsonWriter writer, object? value) + { + switch (value) + { + case null: + case NoneType: + writer.WriteNullValue(); + break; + case bool b: + writer.WriteBooleanValue(b); + break; + case string s: + writer.WriteStringValue(s); + break; + case StarlarkInt i: + writer.WriteNumberValue(i.ToBigInteger() is var big + && big >= long.MinValue && big <= long.MaxValue + ? (decimal)(long)big + : (decimal)big); + break; + case StarlarkFloat f: + writer.WriteNumberValue(f.ToDouble()); + break; + case Dict dict: + writer.WriteStartObject(); + foreach (var entry in dict.Entries) + { + writer.WritePropertyName(entry.Key?.ToString() ?? "null"); + Write(writer, entry.Value); + } + + writer.WriteEndObject(); + break; + case System.Collections.IEnumerable seq: + writer.WriteStartArray(); + foreach (var item in seq) + { + Write(writer, item); + } + + writer.WriteEndArray(); + break; + default: + writer.WriteStringValue(value.ToString()); + break; + } + } +} diff --git a/src/Copybara.Core/Http/Multipart/FilePart.cs b/src/Copybara.Core/Http/Multipart/FilePart.cs new file mode 100644 index 000000000..1977f1e7c --- /dev/null +++ b/src/Copybara.Core/Http/Multipart/FilePart.cs @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using System.Net.Http.Headers; +using Copybara.Checks; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Http.Multipart; + +/// Represents a file field in a multipart http form payload. +public class FilePart : IHttpEndpointFormPart, IStarlarkValue +{ + private readonly string _name; + private readonly string _filePath; + private readonly string _contentType; + private readonly string? _filename; + + public FilePart(string name, string filePath, string contentType, string? filename) + { + _name = name; + _filePath = filePath; + _contentType = contentType; + _filename = filename; + } + + public void AddToContent(MultipartFormDataContent content) + { + var part = new StreamContent(File.OpenRead(_filePath)); + part.Headers.ContentType = new MediaTypeHeaderValue(_contentType); + IHttpEndpointFormPart.SetContentDispositionHeader(part, _name, _filename); + content.Add(part); + } + + public void CheckPart(IChecker checker, Console console) + { + checker.DoCheck(_filePath, console); + } +} diff --git a/src/Copybara.Core/Http/Multipart/HttpEndpointFormPart.cs b/src/Copybara.Core/Http/Multipart/HttpEndpointFormPart.cs new file mode 100644 index 000000000..aae11ebf3 --- /dev/null +++ b/src/Copybara.Core/Http/Multipart/HttpEndpointFormPart.cs @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using System.Net.Http.Headers; +using Copybara.Checks; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Http.Multipart; + +/// Represents a single part of a multipart form data request. +public interface IHttpEndpointFormPart +{ + void AddToContent(MultipartFormDataContent content); + + void CheckPart(IChecker checker, Console console); + + /// + /// Sets the content-disposition header for a form part on the given content, mirroring the Java + /// helper. The .NET emits form-data automatically; + /// this applies the name (and optional filename) parameters. + /// + static void SetContentDispositionHeader(HttpContent content, string name, string? filename) + { + var disposition = new ContentDispositionHeaderValue("form-data") + { + Name = $"\"{name}\"", + }; + if (filename != null) + { + disposition.FileName = $"\"{filename}\""; + } + + content.Headers.ContentDisposition = disposition; + } +} diff --git a/src/Copybara.Core/Http/Multipart/HttpEndpointMultipartFormContent.cs b/src/Copybara.Core/Http/Multipart/HttpEndpointMultipartFormContent.cs new file mode 100644 index 000000000..c138d76e6 --- /dev/null +++ b/src/Copybara.Core/Http/Multipart/HttpEndpointMultipartFormContent.cs @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using Copybara.Checks; +using Copybara.Http.Endpoint; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Http.Multipart; + +/// Constructs data for an http request containing a multipart form data payload. +public class HttpEndpointMultipartFormContent : IHttpEndpointBody, IStarlarkValue +{ + private readonly IReadOnlyList _parts; + private HttpContent? _body; + + public HttpEndpointMultipartFormContent(IReadOnlyList parts) + { + _parts = parts; + } + + public HttpContent GetContent() + { + if (_body == null) + { + var content = new MultipartFormDataContent(Guid.NewGuid().ToString()); + foreach (var part in _parts) + { + part.AddToContent(content); + } + + _body = content; + } + + return _body; + } + + public void CheckContent(IChecker checker, Console console) + { + foreach (var part in _parts) + { + part.CheckPart(checker, console); + } + } +} diff --git a/src/Copybara.Core/Http/Multipart/HttpEndpointUrlEncodedFormContent.cs b/src/Copybara.Core/Http/Multipart/HttpEndpointUrlEncodedFormContent.cs new file mode 100644 index 000000000..25c14e630 --- /dev/null +++ b/src/Copybara.Core/Http/Multipart/HttpEndpointUrlEncodedFormContent.cs @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using Copybara.Http.Endpoint; +using Starlark.Eval; + +namespace Copybara.Http.Multipart; + +/// Constructs data for an HTTP request containing a urlencoded form data payload. +public class HttpEndpointUrlEncodedFormContent : IHttpEndpointBody, IStarlarkValue +{ + private readonly object? _data; + private HttpContent? _body; + + public HttpEndpointUrlEncodedFormContent(object? data) + { + _data = data; + } + + public HttpContent GetContent() + { + if (_body == null) + { + var pairs = new List>(); + if (_data is Dict dict) + { + foreach (var entry in dict.Entries) + { + pairs.Add(new KeyValuePair( + entry.Key?.ToString() ?? string.Empty, + entry.Value?.ToString() ?? string.Empty)); + } + } + + _body = new FormUrlEncodedContent(pairs); + } + + return _body; + } +} diff --git a/src/Copybara.Core/Http/Multipart/TextPart.cs b/src/Copybara.Core/Http/Multipart/TextPart.cs new file mode 100644 index 000000000..5947b5fa4 --- /dev/null +++ b/src/Copybara.Core/Http/Multipart/TextPart.cs @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Net.Http; +using Copybara.Checks; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Http.Multipart; + +/// Represents a text field in a multipart http form payload. +public class TextPart : IHttpEndpointFormPart, IStarlarkValue +{ + private readonly string _name; + private readonly string _text; + + public TextPart(string name, string text) + { + _name = name; + _text = text; + } + + public void AddToContent(MultipartFormDataContent content) + { + var part = new StringContent(_text); + part.Headers.ContentType = null; + IHttpEndpointFormPart.SetContentDispositionHeader(part, _name, null); + content.Add(part); + } + + public void CheckPart(IChecker checker, Console console) + { + checker.DoCheck( + ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("name", _name), + new KeyValuePair("text", _text), + }), + console); + } +} diff --git a/src/Copybara.Core/Json/GsonParserUtil.cs b/src/Copybara.Core/Json/GsonParserUtil.cs new file mode 100644 index 000000000..8cd38fb0e --- /dev/null +++ b/src/Copybara.Core/Json/GsonParserUtil.cs @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; + +namespace Copybara.Json; + +/// +/// Utility class for parsing remote API JSON responses, with special handling if a malformed JSON +/// document is encountered. +/// +/// +/// NOTE(port): upstream uses google-http-client + gson. This port uses BCL +/// and . When a parse error occurs +/// we surface the raw content in the exception, as upstream does. +/// +public static class GsonParserUtil +{ + /// Gerrit's "magic prefix line" that guards JSON responses against XSSI. + public const string GsonNoExecutePrefix = ")]}'\n"; + + /// Options mirroring gson's lenient behavior reasonably closely. + private static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + /// + /// Parses an , with special handling if malformed JSON is + /// encountered. Returns null for empty (no-content) responses. + /// + /// if the JSON can't be parsed to the given type. + public static async Task ParseHttpResponseAsync( + HttpResponseMessage response, + bool stripNoExecutePrefix) + { + if (await IsResponseEmptyAsync(response).ConfigureAwait(false)) + { + return default; + } + + byte[] bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + Encoding charset = GetCharset(response); + + return ParseBytes(bytes, charset, stripNoExecutePrefix); + } + + private static async Task IsResponseEmptyAsync(HttpResponseMessage response) + { + if (response.StatusCode == HttpStatusCode.NoContent) + { + return true; + } + + // Peek at the content length; if unknown, buffer and check. + if (response.Content.Headers.ContentLength is { } len) + { + return len == 0; + } + + byte[] bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + return bytes.Length == 0; + } + + private static Encoding GetCharset(HttpResponseMessage response) + { + string? charsetName = response.Content.Headers.ContentType?.CharSet; + if (!string.IsNullOrEmpty(charsetName)) + { + try + { + return Encoding.GetEncoding(charsetName); + } + catch (ArgumentException) + { + // Fall through to UTF-8. + } + } + + return Encoding.UTF8; + } + + /// + /// Parses a string, with special handling if malformed JSON is encountered. + /// + /// if the JSON can't be parsed to the given type. + public static T? ParseString(string @string, bool stripNoExecutePrefix) + { + var charset = Encoding.UTF8; + return ParseBytes(charset.GetBytes(@string), charset, stripNoExecutePrefix); + } + + /// + /// Parses a byte array, with special handling if malformed JSON is encountered. + /// + /// if the JSON can't be parsed to the given type. + public static T? ParseBytes(byte[] bytes, Encoding charset, bool stripNoExecutePrefix) + { + ReadOnlySpan span = bytes; + + if (stripNoExecutePrefix) + { + byte[] prefix = charset.GetBytes(GsonNoExecutePrefix); + if (span.Length >= prefix.Length && span.Slice(0, prefix.Length).SequenceEqual(prefix)) + { + span = span.Slice(prefix.Length); + } + } + + try + { + string json = charset.GetString(span); + return JsonSerializer.Deserialize(json, Options); + } + catch (Exception e) when (e is JsonException or ArgumentException or NotSupportedException) + { + throw new ArgumentException( + string.Format( + "Cannot parse content as type {0}.\nContent: {1}\n", + typeof(T), charset.GetString(bytes)), + e); + } + } +} diff --git a/src/Copybara.Core/Re2/Re2Module.cs b/src/Copybara.Core/Re2/Re2Module.cs new file mode 100644 index 000000000..9aee47de3 --- /dev/null +++ b/src/Copybara.Core/Re2/Re2Module.cs @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Re2; + +/// Regex functions to work with re2 like regexes in Starlark. +/// +/// NOTE(port): upstream uses re2j; this port uses , +/// an accepted deviation. +/// +[StarlarkBuiltin("re2", Doc = "Set of functions to work with regexes in Copybara.")] +public sealed class Re2Module : IStarlarkValue +{ + [StarlarkMethod("compile", Doc = "Create a regex pattern")] + public StarlarkPattern Compile( + [Param(Name = "regex")] string regex) + { + try + { + return new StarlarkPattern(new Regex(regex)); + } + catch (ArgumentException e) + { + throw new EvalException($"Unable to parse regex '{regex}'.", e); + } + } + + [StarlarkMethod( + "quote", + Doc = "Quote a string to be matched literally if used within a regex pattern")] + public string Quote( + [Param(Name = "string")] string @string) => + Regex.Escape(@string); +} diff --git a/src/Copybara.Core/Re2/StarlarkMatcher.cs b/src/Copybara.Core/Re2/StarlarkMatcher.cs new file mode 100644 index 000000000..0c3c7ab99 --- /dev/null +++ b/src/Copybara.Core/Re2/StarlarkMatcher.cs @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Starlark.Annot; +using Starlark.Eval; + +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Re2; + +/// A Starlark wrapper of a regex matcher, mirroring re2j's Matcher. +/// +/// NOTE(port): re2j's Matcher is stateful. This port emulates that state on top of +/// : attempts a full-input +/// match, scans for the next match, and the query methods (group, +/// start, end) operate on the most recent successful match. +/// +[StarlarkBuiltin( + "re2_matcher", + Doc = "A RE2 regex pattern matcher object to perform regexes in Starlark")] +public sealed class StarlarkMatcher : IStarlarkValue +{ + private readonly Regex _pattern; + private readonly string _input; + private Match? _match; + + public StarlarkMatcher(Regex pattern, string input) + { + _pattern = pattern; + _input = input; + } + + [StarlarkMethod("matches", Doc = "Return true if the string matches the regex pattern.")] + public bool Matches() + { + var m = _pattern.Match(_input); + if (m.Success && m.Index == 0 && m.Length == _input.Length) + { + _match = m; + return true; + } + _match = null; + return false; + } + + [StarlarkMethod( + "find", + Doc = "Return true if the string matches the regex pattern.")] + public bool Find( + [Param(Name = "start", Doc = "The input position where the search begins", Named = true, + AllowedTypes = new[] { typeof(StarlarkInt), typeof(NoneType) }, + DefaultValue = "None")] + object start) + { + Match m; + if (StarlarkRt.IsNullOrNone(start)) + { + int from = _match is { Success: true } + ? (_match.Length == 0 ? _match.Index + 1 : _match.Index + _match.Length) + : 0; + m = from <= _input.Length ? _pattern.Match(_input, from) : Match.Empty; + } + else + { + int from = ((StarlarkInt)start).ToInt("start"); + m = _pattern.Match(_input, from); + } + + _match = m.Success ? m : null; + return m.Success; + } + + [StarlarkMethod( + "start", + Doc = "Return the start position of a matching group")] + public int Start( + [Param(Name = "group", Named = true, + AllowedTypes = new[] { typeof(StarlarkInt), typeof(string) }, + DefaultValue = "0")] + object group) + { + var g = RequireGroup(group, "start()"); + return g.Success ? g.Index : -1; + } + + [StarlarkMethod( + "group", + Doc = "Return a matching group")] + public string Group( + [Param(Name = "group", Named = true, + AllowedTypes = new[] { typeof(StarlarkInt), typeof(string) }, + DefaultValue = "0")] + object group) + { + var g = RequireGroup(group, "group()"); + return g.Success ? g.Value : ""; + } + + [StarlarkMethod( + "end", + Doc = "Return the end position of a matching group")] + public int End( + [Param(Name = "group", Named = true, + AllowedTypes = new[] { typeof(StarlarkInt), typeof(string) }, + DefaultValue = "0")] + object group) + { + var g = RequireGroup(group, "end()"); + return g.Success ? g.Index + g.Length : -1; + } + + [StarlarkMethod("group_count", Doc = "Return the number of groups found for a match")] + public int GroupCount() => + // Java's groupCount does not count the whole-match group 0. + _pattern.GetGroupNumbers().Length - 1; + + [StarlarkMethod( + "replace_all", + Doc = "Replace all instances matching the regex")] + public string ReplaceAll( + [Param(Name = "replacement", Named = true, DefaultValue = "0")] string replacement) => + _pattern.Replace(_input, ConvertReplacement(replacement)); + + [StarlarkMethod( + "replace_first", + Doc = "Replace the first instance matching the regex")] + public string ReplaceFirst( + [Param(Name = "replacement", Named = true, DefaultValue = "0")] string replacement) => + _pattern.Replace(_input, ConvertReplacement(replacement), 1); + + private Group RequireGroup(object group, string method) + { + if (_match is not { } match) + { + throw new EvalException( + $"Call to {method} is not allowed before calling matches()"); + } + + try + { + return group is string name + ? match.Groups[name] + : match.Groups[((StarlarkInt)group).ToInt("group")]; + } + catch (Exception e) when (e is ArgumentException or IndexOutOfRangeException) + { + throw new EvalException($"Invalid group for {method}", e); + } + } + + // NOTE(port): re2j / Java use $N and ${name} for group references, which matches .NET's + // substitution syntax, so replacement templates are passed through unchanged. + private static string ConvertReplacement(string replacement) => replacement; +} diff --git a/src/Copybara.Core/Re2/StarlarkPattern.cs b/src/Copybara.Core/Re2/StarlarkPattern.cs new file mode 100644 index 000000000..e6822fd2f --- /dev/null +++ b/src/Copybara.Core/Re2/StarlarkPattern.cs @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Re2; + +/// A Starlark wrapper of a regex pattern. +/// +/// NOTE(port): upstream uses re2j (RE2 semantics). This port backs regexes with +/// , an accepted deviation. Most patterns behave +/// identically, but RE2-specific behaviors (e.g. linear-time guarantees, some syntax) may diverge. +/// +[StarlarkBuiltin( + "re2_pattern", + Doc = "A RE2 regex pattern object to perform regexes in Starlark")] +public sealed class StarlarkPattern : IStarlarkValue +{ + private readonly Regex _pattern; + + public StarlarkPattern(Regex pattern) + { + _pattern = pattern; + } + + /// The underlying compiled pattern. + public Regex Pattern => _pattern; + + [StarlarkMethod( + "matches", + Doc = "Return true if the string matches the regex pattern")] + public bool Matches( + [Param(Name = "input", Named = true)] string input) + { + // Java's Pattern.matches requires the whole input to match the pattern. + var m = _pattern.Match(input); + return m.Success && m.Index == 0 && m.Length == input.Length; + } + + [StarlarkMethod( + "matcher", + Doc = "Return a Matcher for the given input.")] + public StarlarkMatcher Matcher( + [Param(Name = "input", Named = true)] string input) => + new(_pattern, input); +} diff --git a/src/Copybara.Core/RemoteFile/Extract/ExtractType.cs b/src/Copybara.Core/RemoteFile/Extract/ExtractType.cs new file mode 100644 index 000000000..040afb466 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/Extract/ExtractType.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.RemoteFile.Extract; + +/// Supported archive types. +public enum ExtractType +{ + JAR, + ZIP, + TAR, + TAR_GZ, + TAR_XZ, + TAR_BZ2, +} diff --git a/src/Copybara.Core/RemoteFile/Extract/ExtractUtil.cs b/src/Copybara.Core/RemoteFile/Extract/ExtractUtil.cs new file mode 100644 index 000000000..8ee6a9c75 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/Extract/ExtractUtil.cs @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Formats.Tar; +using System.IO.Compression; +using Copybara.Exceptions; +using Copybara.Util; + +namespace Copybara.RemoteFile.Extract; + +/// +/// A utility to extract a compressed archive to a target folder. Accepts a to +/// filter out which files should be copied. +/// +/// Upstream uses commons-compress. This port uses the .NET in-box archive readers: +/// for zip/jar, for tar (with +/// for tar.gz). XZ and BZip2 are not supported by the BCL and are left as +/// TODO(port). +/// +public static class ExtractUtil +{ + /// Helper to read an archive from a stream. + /// + /// + public static void ExtractArchive( + Stream contents, string targetPath, ExtractType type, Glob? fileFilter) + { + string root = PathOps.Normalize(Path.GetFullPath(targetPath)); + IPathMatcher? rootedFilter = fileFilter?.RelativeTo(root); + + switch (type) + { + case ExtractType.JAR: + case ExtractType.ZIP: + ExtractZip(contents, root, rootedFilter); + break; + case ExtractType.TAR: + ExtractTar(contents, root, rootedFilter); + break; + case ExtractType.TAR_GZ: + using (var gz = new GZipStream(contents, CompressionMode.Decompress, leaveOpen: true)) + { + ExtractTar(gz, root, rootedFilter); + } + break; + case ExtractType.TAR_XZ: + // TODO(port): XZ decompression is not available in the .NET BCL. A third-party + // package (e.g. SharpCompress or XZ.NET) would be required to support TAR_XZ. + throw new ValidationException( + "TAR_XZ archives are not yet supported in the .NET port (no in-box XZ codec)."); + case ExtractType.TAR_BZ2: + // TODO(port): BZip2 decompression is not available in the .NET BCL. A third-party + // package (e.g. SharpCompress) would be required to support TAR_BZ2. + throw new ValidationException( + "TAR_BZ2 archives are not yet supported in the .NET port (no in-box BZip2 codec)."); + default: + throw new ValidationException( + $"Failed to get archive input stream for file type: {type}"); + } + } + + private static void ExtractZip(Stream contents, string root, IPathMatcher? rootedFilter) + { + using var zip = new ZipArchive(contents, ZipArchiveMode.Read, leaveOpen: true); + foreach (var entry in zip.Entries) + { + // A directory entry in zip has an empty name / a trailing slash and zero length. + bool isDirectory = entry.Name.Length == 0 || entry.FullName.EndsWith('/'); + if (!TryResolveEntry(entry.FullName, root, isDirectory, rootedFilter, out string resolvedPath)) + { + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(resolvedPath)!); + using var source = entry.Open(); + using var dest = File.Create(resolvedPath); + source.CopyTo(dest); + } + } + + private static void ExtractTar(Stream contents, string root, IPathMatcher? rootedFilter) + { + using var reader = new TarReader(contents, leaveOpen: true); + while (reader.GetNextEntry() is { } entry) + { + bool isDirectory = entry.EntryType is TarEntryType.Directory; + if (!TryResolveEntry(entry.Name, root, isDirectory, rootedFilter, out string resolvedPath)) + { + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(resolvedPath)!); + if (entry.DataStream is { } data) + { + using var dest = File.Create(resolvedPath); + data.CopyTo(dest); + } + } + } + + /// + /// Resolves an archive entry name against , applying the zip-slip check + /// and the file filter. Returns false if the entry should be skipped. + /// + /// if the entry escapes the target dir. + private static bool TryResolveEntry( + string entryName, string root, bool isDirectory, IPathMatcher? rootedFilter, + out string resolvedPath) + { + resolvedPath = PathOps.Normalize(PathOps.Resolve(root, entryName)); + + // Security check: Prevent Zip Slip vulnerability + if (!PathOps.StartsWith(resolvedPath, root)) + { + throw new IOException("Zip entry is outside of the target dir: " + entryName); + } + + if ((rootedFilter != null && !rootedFilter.Matches(resolvedPath)) || isDirectory) + { + return false; + } + return true; + } +} diff --git a/src/Copybara.Core/RemoteFile/GclientHttpStreamFactory.cs b/src/Copybara.Core/RemoteFile/GclientHttpStreamFactory.cs new file mode 100644 index 000000000..fb5525078 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/GclientHttpStreamFactory.cs @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using Copybara.Common; +using Copybara.Http.Auth; + +namespace Copybara.RemoteFile; + +/// +/// An backed by . +/// +/// Upstream wraps the Google GHttp Client; this port uses the .NET +/// to perform the GET and return the response content +/// stream. +/// +public class GclientHttpStreamFactory : IHttpStreamFactory +{ + private readonly HttpClient _httpClient; + + public GclientHttpStreamFactory(TimeSpan timeout) + : this(NewHttpClient(timeout)) + { + } + + public GclientHttpStreamFactory(HttpClient httpClient) + { + _httpClient = Preconditions.CheckNotNull(httpClient); + } + + private static HttpClient NewHttpClient(TimeSpan timeout) + { + var handler = new HttpClientHandler + { + AllowAutoRedirect = true, + }; + return new HttpClient(handler) { Timeout = timeout }; + } + + public Stream Open(Uri url, IAuthInterceptor? auth, ImmutableListMultimap headers) + { + using var request = BuildHttpRequest(url, auth, headers); + HttpResponseMessage response = + _httpClient.Send(request, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + return response.Content.ReadAsStream(); + } + + /// Constructs an object. + /// The URL to send the request to. + /// The authentication to use. + /// The headers to set in the request. + /// + /// + protected HttpRequestMessage BuildHttpRequest( + Uri url, IAuthInterceptor? auth, ImmutableListMultimap headers) + { + var req = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var key in headers.Keys) + { + foreach (string value in headers.Get(key)) + { + req.Headers.TryAddWithoutValidation(key, value); + } + } + auth?.Interceptor()(req); + return req; + } +} diff --git a/src/Copybara.Core/RemoteFile/GithubArchive.cs b/src/Copybara.Core/RemoteFile/GithubArchive.cs new file mode 100644 index 000000000..1b1e74a82 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/GithubArchive.cs @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Http.Auth; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.RemoteFile; + +/// A tarball for a given ref, downloaded from GitHub. +[StarlarkBuiltin( + "remote_http_file.GitHubArchive", + Documented = false, + Doc = "A GitHub archive that can be downloaded at the given revision. Only exposes the SHA256 " + + "hash of the archive.")] +public class GithubArchive : RemoteHttpFile +{ + private readonly string _project; + private readonly ArchiveType _fileType; + + public GithubArchive( + string project, + string reference, + ArchiveType fileType, + IHttpStreamFactory transport, + Profiler.Profiler profiler, + Console console, + IAuthInterceptor? auth) + : base(reference, transport, console, profiler, auth) + { + _project = Preconditions.CheckNotNull(project); + _fileType = fileType; + } + + protected override Uri GetRemote() + { + try + { + // This is somewhat limited and does not support private repos. We can use + // https://developer.github.com/v3/repos/contents/#get-archive-link if a use case for + // private repos comes up. + return new Uri(string.Format( + "https://github.com/{0}/archive/{1}.{2}", + _project, Reference, _fileType.Extension())); + } + catch (UriFormatException e) + { + throw new ValidationException( + $"Error assembling URL for archive of {_project} at {Reference}", e); + } + } + + protected override Stream GetSink() => Stream.Null; + + /// We only need the hash of archives as we do not allow introspecting them. + public enum ArchiveType + { + TARBALL, + ZIP, + } +} + +internal static class ArchiveTypeExtensions +{ + public static string Extension(this GithubArchive.ArchiveType type) => + type switch + { + GithubArchive.ArchiveType.TARBALL => "tar.gz", + GithubArchive.ArchiveType.ZIP => "zip", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; +} diff --git a/src/Copybara.Core/RemoteFile/IHttpStreamFactory.cs b/src/Copybara.Core/RemoteFile/IHttpStreamFactory.cs new file mode 100644 index 000000000..fba490ed7 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/IHttpStreamFactory.cs @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Http.Auth; + +namespace Copybara.RemoteFile; + +/// Interface for opening a URL for downloading a file. +public interface IHttpStreamFactory +{ + /// Open the referenced URL and return the stream to the contents. + /// The URL to open. + /// The interceptor to use for authentication. If null, no authentication is used. + /// + /// + /// + Stream Open(Uri url, IAuthInterceptor? auth) => + Open(url, auth, ImmutableListMultimap.Empty); + + /// Open the referenced URL and return the stream to the contents. + /// The URL to open. + /// The interceptor to use for authentication. If null, no authentication is used. + /// The headers to send in the HTTP request. + /// + /// + /// + Stream Open(Uri url, IAuthInterceptor? auth, ImmutableListMultimap headers); +} diff --git a/src/Copybara.Core/RemoteFile/IRemoteArchiveVersionSelector.cs b/src/Copybara.Core/RemoteFile/IRemoteArchiveVersionSelector.cs new file mode 100644 index 000000000..129c2db61 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/IRemoteArchiveVersionSelector.cs @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.RemoteFile; + +/// +/// An interface for version selecting logic to use with a object. +/// Individual implementations should be exposed as StarlarkBuiltin objects. +/// +public interface IRemoteArchiveVersionSelector +{ + /// + RemoteArchiveVersion ConstructUrl( + string baseUrl, string? requestedRef, RemoteFileOptions remoteFileOptions); +} diff --git a/src/Copybara.Core/RemoteFile/RemoteArchiveOrigin.cs b/src/Copybara.Core/RemoteFile/RemoteArchiveOrigin.cs new file mode 100644 index 000000000..f849b08c6 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/RemoteArchiveOrigin.cs @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Http.Auth; +using Copybara.RemoteFile.Extract; +using Copybara.Revision; +using Copybara.TemplateToken; +using Copybara.Util; +using Copybara.Version; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.RemoteFile; + +/// An class for remote files. +public class RemoteArchiveOrigin : IOrigin +{ + private const string LabelNameConst = "RemoteArchiveOrigin"; + + private readonly Author _author; + private readonly string _message; + private readonly GeneralOptions _generalOptions; + private readonly RemoteFileOptions _remoteFileOptions; + private readonly string _archiveSourceUrl; + private readonly IVersionList? _versionList; + private readonly IVersionSelector? _versionSelector; + private readonly RemoteFileType _remoteFileType; + private readonly IVersionResolver? _versionResolver; + private readonly IAuthInterceptor? _auth; + + public RemoteArchiveOrigin( + Author author, + string message, + GeneralOptions generalOptions, + RemoteFileOptions remoteFileOptions, + RemoteFileType remoteFileType, + string archiveSourceUrl, + IVersionList? versionList, + IVersionSelector? versionSelector, + IVersionResolver? versionResolver, + IAuthInterceptor? auth) + { + _remoteFileType = remoteFileType; + _author = author; + _message = message; + _generalOptions = generalOptions; + _remoteFileOptions = remoteFileOptions; + _archiveSourceUrl = archiveSourceUrl; + _versionList = versionList; + _versionSelector = versionSelector; + _versionResolver = versionResolver; + _auth = auth; + } + + /// + private string ResolveUrlTemplate(string url, string version) => + new LabelTemplate(url).Resolve( + label => + { + if (label.Equals("VERSION") + || label.Equals("CONTEXT_REFERENCE") + || label.Equals("COPYBARA_CONTEXT_REFERENCE")) + { + return version; + } + throw new ArgumentException( + string.Format( + "Archive source templates only support '${{VERSION}}', '${{CONTEXT_REFERENCE}}'" + + " or '${{COPYBARA_CONTEXT_REFERENCE}}' labels, but found '{0}'", + label)); + }); + + private string? GetUrlAssemblyStrategy(string label) + { + try + { + return ResolveUrlTemplate(_archiveSourceUrl, label); + } + catch (LabelTemplate.LabelNotFoundException) + { + return null; + } + } + + /// This is used to resolve new refs. + /// the version to target. If left null/empty, we will deduce intended + /// version from what was supplied to the constructor. + /// + /// + public RemoteArchiveRevision Resolve(string? reference) + { + bool canUseResolverOnCliRef = + _generalOptions.IsVersionSelectorUseCliRef() || _generalOptions.IsForced(); + _generalOptions + .GetConsole() + .WarnFmtIf( + !canUseResolverOnCliRef + && !string.IsNullOrEmpty(reference) + && _versionResolver != null, + "Resolve version ref for '{0}' was detected, but will not apply the supplied resolver." + + " Consider setting --force or --use-version-selector-ref to true.", + reference!); + + // It's a versionless import. + if (_versionList == null || _versionSelector == null) + { + string url = _archiveSourceUrl; + if (!string.IsNullOrEmpty(reference)) + { + try + { + url = ResolveUrlTemplate(_archiveSourceUrl, reference); + } + catch (Exception e) when (e is LabelTemplate.LabelNotFoundException or ArgumentException) + { + throw new ValidationException( + string.Format( + "Could not resolve archive URL template {0} with error '{1}' and the cause" + + " (if any) was '{2}'", + _archiveSourceUrl, e.Message, e.InnerException)); + } + } + return new RemoteArchiveRevision(new RemoteArchiveVersion(url, reference)); + } + + try + { + string version = + _versionSelector.Select(_versionList, reference, _generalOptions.GetConsole()) + ?? throw new ValidationException("Version selector returned no results."); + + if (canUseResolverOnCliRef + && !string.IsNullOrEmpty(reference) + && _versionResolver != null) + { + return (RemoteArchiveRevision)_versionResolver.Resolve(version, GetUrlAssemblyStrategy); + } + var remoteArchiveVersion = + new RemoteArchiveVersion(ResolveUrlTemplate(_archiveSourceUrl, version), version); + return new RemoteArchiveRevision(remoteArchiveVersion); + } + catch (Exception e) + when (e is LabelTemplate.LabelNotFoundException or ArgumentException or ValidationException) + { + throw new ValidationException( + string.Format( + "Could not resolve archive URL template {0} with error '{1}' and the cause (if any)" + + " was '{2}'", + _archiveSourceUrl, e.Message, e.InnerException)); + } + } + + /// This is used to resolve the baseline. + /// + /// + public RemoteArchiveRevision ResolveLastRev(string reference) + { + Preconditions.CheckState( + !string.IsNullOrEmpty(reference), + "Last migrated revision reference must not be null or empty."); + if (_versionResolver != null) + { + return (RemoteArchiveRevision)_versionResolver.Resolve(reference, GetUrlAssemblyStrategy); + } + + _generalOptions + .GetConsole() + .WarnFmt( + "No version resolver was supplied, will attempt to resolve baseline version by url" + + " template."); + string fullUrl = + GetUrlAssemblyStrategy(reference) + ?? throw new ValidationException( + string.Format( + "Could not construct remote archive version from url='{0}' and ref='{1}'", + _archiveSourceUrl, reference)); + return new RemoteArchiveRevision(new RemoteArchiveVersion(fullUrl, reference)); + } + + public IOrigin.IReader NewReader( + Glob originFiles, Authoring.Authoring authoring) => + new Reader(this, originFiles); + + public string GetLabelName() => LabelNameConst; + + public string GetTypeName() => "remotefiles.origin"; + + public ImmutableListMultimap Describe(Glob? originFiles) + { + var builder = ImmutableListMultimap.CreateBuilder(); + builder.Put("type", GetTypeName()); + builder.Put("url", _archiveSourceUrl); // the unresolved url + if (originFiles != null) + { + builder.PutAll("root", originFiles.Roots()); + } + return builder.Build(); + } + + public IReadOnlyList> DescribeCredentials() + { + var credentials = ImmutableArray.CreateBuilder>(); + if (_versionList != null) + { + credentials.AddRange(_versionList.DescribeCredentials()); + } + if (_versionResolver != null) + { + credentials.AddRange(_versionResolver.DescribeCredentials()); + } + if (_auth != null) + { + credentials.AddRange(_auth.DescribeCredentials()); + } + return credentials.ToImmutable(); + } + + private sealed class Reader : IOrigin.IReader + { + private readonly RemoteArchiveOrigin _origin; + private readonly Glob _originFiles; + + public Reader(RemoteArchiveOrigin origin, Glob originFiles) + { + _origin = origin; + _originFiles = originFiles; + } + + private void WriteArchiveAsIs(RemoteArchiveRevision reference, string workdir, Stream returned) + { + string url = reference.GetUrl()!; + string filename = url[(_origin._archiveSourceUrl.LastIndexOf('/') + 1)..]; + using Stream sink = File.Create(PathOps.Resolve(workdir, filename)); + returned.CopyTo(sink); + } + + /// + public void Checkout(RemoteArchiveRevision reference, string checkoutDir) + { + try + { + // TODO(joshgoldman): Add richer ref object and ability to restrict download by + // host/url + var url = new Uri(Preconditions.CheckNotNull(reference.GetUrl())); + IHttpStreamFactory transport = _origin._remoteFileOptions.GetTransport(); + using (_origin._generalOptions.Profiler().Start("remote_file_" + url)) + using (Stream returned = transport.Open(url, _origin._auth)) + { + if (_origin._remoteFileType == RemoteFileType.AS_IS) + { + WriteArchiveAsIs(reference, checkoutDir, returned); + } + else + { + ExtractUtil.ExtractArchive( + returned, + checkoutDir, + RemoteFileTypeExtensions.ToExtractType(_origin._remoteFileType), + _originFiles); + } + } + } + catch (IOException e) + { + throw new ValidationException( + string.Format( + "Could not checkout archive file at {0}: \n{1}", reference.GetUrl(), e.Message)); + } + } + + /// + public Origin.ChangesResponse Changes( + RemoteArchiveRevision? fromRef, RemoteArchiveRevision toRef) + { + var console = _origin._generalOptions.GetConsole(); + if (_origin._versionSelector == null) + { + return Origin.ChangesResponse.ForChanges( + ImmutableArray.Create(Change(toRef))); + } + if (fromRef == null) + { + console.WarnFmt( + "The baseline revision could not be detected, not performing downgrade" + + " validation."); + return Origin.ChangesResponse.ForChanges( + ImmutableArray.Create(Change(toRef))); + } + if (string.IsNullOrEmpty(fromRef.FixedReference()) + || string.IsNullOrEmpty(toRef.FixedReference())) + { + console.WarnFmt( + "Either the baseline ref[{0}] or the incoming ref[{1}] form as a fixed ref were" + + " not known, not performing downgrade validation.", + fromRef.FixedReference()!, toRef.FixedReference()!); + return Origin.ChangesResponse.ForChanges( + ImmutableArray.Create(Change(toRef))); + } + + try + { + string? selectedVersion = + _origin._versionSelector.Select( + new SetVersionList( + ImmutableHashSet.Create(toRef.FixedReference()!, fromRef.FixedReference()!)), + requestedRef: null, + console); + + if (selectedVersion != null && selectedVersion.Equals(fromRef.FixedReference())) + { + console.WarnFmt( + "The incoming ref [{0}] is not newer than the baseline ref [{1}]. " + + "The change response will have no changes generated.", + toRef.FixedReference()!, fromRef.FixedReference()!); + return Origin.ChangesResponse.NoChanges( + Origin.EmptyReason.ToIsAncestor); + } + } + catch (ValidationException e) + { + console.WarnFmt( + "An error has occurred while validating the order of changes between {0} and {1}:" + + " '{2}'. Defaulting to a changelist with only the incoming ref.", + fromRef.FixedReference()!, toRef.FixedReference()!, e.Message); + } + return Origin.ChangesResponse.ForChanges( + ImmutableArray.Create(Change(toRef))); + } + + /// + public Change Change(RemoteArchiveRevision reference) => + new( + reference, + _origin._author, + _origin._message, + reference.ReadTimestamp() ?? DateTimeOffset.Now, + ImmutableListMultimap.Empty); + + /// + public void VisitChanges(RemoteArchiveRevision? start, IChangesVisitor visitor) + { + RemoteArchiveRevision reference = start!; + var change = new Change( + reference, + _origin._author, + _origin._message, + reference.ReadTimestamp() ?? DateTimeOffset.Now, + ImmutableListMultimap.Empty); + visitor.Visit(change); + } + } +} diff --git a/src/Copybara.Core/RemoteFile/RemoteArchiveRevision.cs b/src/Copybara.Core/RemoteFile/RemoteArchiveRevision.cs new file mode 100644 index 000000000..cb173219b --- /dev/null +++ b/src/Copybara.Core/RemoteFile/RemoteArchiveRevision.cs @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Revision; + +namespace Copybara.RemoteFile; + +/// A for a remote file. +public class RemoteArchiveRevision : IRevision +{ + private const string ArchiveVersionLabel = "ARCHIVE_VERSION"; + private const string ArchiveFullUrlLabel = "ARCHIVE_FULL_URL "; + + internal readonly RemoteArchiveVersion Version; + + public RemoteArchiveRevision(RemoteArchiveVersion version) + { + Version = version; + } + + public string? GetUrl() => Version.GetFullUrl(); + + public DateTimeOffset? ReadTimestamp() => null; + + public string AsString() => + !string.IsNullOrEmpty(Version.GetVersion()) + ? Version.GetVersion()! + : Version.GetFullUrl(); + + public string? ContextReference() => Version.GetVersion(); + + public string? FixedReference() => AsString(); + + public ImmutableListMultimap AssociatedLabels() + { + var builder = ImmutableListMultimap.CreateBuilder(); + builder.Put(ArchiveVersionLabel, Version.GetVersion() ?? ""); + builder.Put(ArchiveFullUrlLabel, GetUrl() ?? ""); + return builder.Build(); + } +} diff --git a/src/Copybara.Core/RemoteFile/RemoteArchiveVersion.cs b/src/Copybara.Core/RemoteFile/RemoteArchiveVersion.cs new file mode 100644 index 000000000..636bbe0a4 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/RemoteArchiveVersion.cs @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.RemoteFile; + +/// A class to represent a version for RemoteArchive endpoints. +public sealed class RemoteArchiveVersion +{ + private readonly string _fullUrl; + private readonly string? _version; + + public RemoteArchiveVersion(string fullUrl, string? version) + { + _version = version; + _fullUrl = fullUrl; + } + + public string GetFullUrl() => _fullUrl; + + public string? GetVersion() => _version; +} diff --git a/src/Copybara.Core/RemoteFile/RemoteFileModule.cs b/src/Copybara.Core/RemoteFile/RemoteFileModule.cs new file mode 100644 index 000000000..68f8678ec --- /dev/null +++ b/src/Copybara.Core/RemoteFile/RemoteFileModule.cs @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Config; +using Copybara.Exceptions; +using Copybara.Http.Auth; +using Copybara.Version; +using Starlark.Annot; +using Starlark.Eval; +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.RemoteFile; + +/// Module for helpers to load files from a source other than the origin. Use with caution. +[StarlarkBuiltin("remotefiles", Doc = "Functions to access remote files not in either repo.")] +public class RemoteFileModule : ILabelsAwareModule, IStarlarkValue +{ + protected readonly Options Options; + + public RemoteFileModule(Options options) + { + Options = Preconditions.CheckNotNull(options); + } + + [StarlarkMethod( + "github_archive", + Doc = "A tarball for a specific SHA1 on GitHub. Experimental.", + Documented = false)] + public GithubArchive GitHubTarball( + [Param( + Name = "project", + Named = true, + DefaultValue = "[]", + Doc = "The GitHub project from which to load the file, e.g. google/copybara")] + string project, + [Param( + Name = "revision", + Named = true, + DefaultValue = "[]", + Doc = "The revision to download from the project, typically a commit SHA1.")] + string revision, + [Param( + Name = "type", + Named = true, + DefaultValue = "'TARBALL'", + Doc = "Archive type to download, options are 'TARBALL' or 'ZIP'.")] + string type, + [Param( + Name = "auth", + Doc = "Optional, an interceptor for providing credentials.", + Named = true, + DefaultValue = "None", + Positional = false, + AllowedTypes = new[] { typeof(IAuthInterceptor), typeof(NoneType) })] + object auth) + { + var generalOptions = Options.Get(); + var remoteFileOptions = Options.Get(); + try + { + if (!Enum.TryParse(type, out var archiveType) + || !Enum.IsDefined(archiveType)) + { + throw StarlarkRt.Errorf( + "Unsupported archive type: '{0}'. Supported values: {1}", + type, string.Join(", ", Enum.GetNames())); + } + return new GithubArchive( + project, + revision, + archiveType, + remoteFileOptions.GetTransport(), + generalOptions.Profiler(), + generalOptions.GetConsole(), + SkylarkUtil.ConvertFromNoneable(auth, null)); + } + catch (ValidationException e) + { + throw StarlarkRt.Errorf("Error setting up remote http file: {0}", e.Message); + } + } + + [StarlarkMethod("origin", Doc = "Defines a remote file origin.")] + public RemoteArchiveOrigin RemoteArchiveOrigin( + [Param( + Name = "author", + DefaultValue = "'Copybara '", + Doc = "Author to attribute the change to", + Named = true, + AllowedTypes = new[] { typeof(string) })] + string author, + [Param( + Name = "message", + DefaultValue = "'Placeholder message'", + Doc = "Message to attach to the change", + Named = true, + AllowedTypes = new[] { typeof(string) })] + string message, + [Param( + Name = "unpack_method", + DefaultValue = "'AS_IS'", + Doc = + "The method by which to unpack the remote file. Currently 'ZIP', 'TAR', 'TAR_GZ'," + + " 'TAR_XZ', 'TAR_BZ2', and 'AS_IS' are supported.", + Named = true, + AllowedTypes = new[] { typeof(string) })] + string fileType, + [Param( + Name = "archive_source", + Named = true, + Doc = + "Template or literal URL to download archive from. Optionally you can use" + + " ${VERSION} in your URL string as placeholder for later resolved versions" + + " during origin checkout. E.g." + + " 'https://proxy.golang.org/mymodule/@v/${VERSION}.zip'", + DefaultValue = "''", + AllowedTypes = new[] { typeof(string) })] + string archiveSourceUrl, + [Param( + Name = "version_list", + Named = true, + DefaultValue = "None", + Doc = "Version list to select versions on. Omit to create a versionless origin.", + AllowedTypes = new[] { typeof(IVersionList), typeof(NoneType) })] + object versionList, + [Param( + Name = "origin_version_selector", + Named = true, + DefaultValue = "None", + Doc = "Version selector used to select on version_list. Omit to create a versionless origin.", + AllowedTypes = new[] { typeof(IVersionSelector), typeof(NoneType) })] + object versionSelector, + [Param( + Name = "version_resolver", + Named = true, + DefaultValue = "None", + Doc = + "Version resolvers are used to resolve refs to specific versions. Primarily used" + + " when command line refs are provided and accompanied by the '--force' or" + + " '--version-selector-use-cli-ref' flag.", + AllowedTypes = new[] { typeof(IVersionResolver), typeof(NoneType) })] + object versionResolver, + [Param( + Name = "auth", + Doc = "Optional, an interceptor for providing credentials.", + Named = true, + DefaultValue = "None", + Positional = false, + AllowedTypes = new[] { typeof(IAuthInterceptor), typeof(NoneType) })] + object auth) + { + var generalOptions = Options.Get(); + var remoteFileOptions = Options.Get(); + RemoteFileType remoteFileType = + SkylarkUtil.StringToEnum("unpack_method", fileType); + return new RemoteArchiveOrigin( + Author.Parse(author), + message, + generalOptions, + remoteFileOptions, + remoteFileType, + archiveSourceUrl, + SkylarkUtil.ConvertFromNoneable(versionList, null), + SkylarkUtil.ConvertFromNoneable(versionSelector, null), + SkylarkUtil.ConvertFromNoneable(versionResolver, null), + SkylarkUtil.ConvertFromNoneable(auth, null)); + } +} diff --git a/src/Copybara.Core/RemoteFile/RemoteFileOptions.cs b/src/Copybara.Core/RemoteFile/RemoteFileOptions.cs new file mode 100644 index 000000000..ec4571031 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/RemoteFileOptions.cs @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.RemoteFile; + +/// Options for loading files from a source other than the origin. Use with caution. +public class RemoteFileOptions : IOption +{ + [Flag( + "--remote-http-files-connection-timeout", + "Timeout for the fetch operation, e.g. 30s.")] + protected TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromMinutes(2); + + private readonly Lazy _transport; + + public RemoteFileOptions() + { + _transport = new Lazy( + () => new GclientHttpStreamFactory(ConnectionTimeout)); + } + + /// + public IHttpStreamFactory GetTransport() => _transport.Value; +} diff --git a/src/Copybara.Core/RemoteFile/RemoteFileType.cs b/src/Copybara.Core/RemoteFile/RemoteFileType.cs new file mode 100644 index 000000000..5ebfc409d --- /dev/null +++ b/src/Copybara.Core/RemoteFile/RemoteFileType.cs @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Copybara.RemoteFile.Extract; + +namespace Copybara.RemoteFile; + +/// +/// Remote file types supported by Copybara archive import. Must be kept in the same ordinal order as +/// . +/// +public enum RemoteFileType +{ + JAR, + ZIP, + TAR, + TAR_GZ, + TAR_XZ, + TAR_BZ2, + AS_IS, +} + +public static class RemoteFileTypeExtensions +{ + /// + public static ExtractType ToExtractType(RemoteFileType type) + { + ValidationException.CheckCondition( + type != RemoteFileType.AS_IS, "Cannot convert an as is file type to an extract type"); + return (ExtractType)(int)type; + } +} diff --git a/src/Copybara.Core/RemoteFile/RemoteHttpFile.cs b/src/Copybara.Core/RemoteFile/RemoteHttpFile.cs new file mode 100644 index 000000000..8694a4800 --- /dev/null +++ b/src/Copybara.Core/RemoteFile/RemoteHttpFile.cs @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Security.Cryptography; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Http.Auth; +using Copybara.Profiler; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.RemoteFile; + +/// A Starlark construct to download remote files via HTTP. +[StarlarkBuiltin( + "remote_http_file", + Documented = false, + Doc = "A file loaded via http(s). This is experimental.")] +public abstract class RemoteHttpFile : IStarlarkValue +{ + protected readonly string Reference; + private readonly IHttpStreamFactory _transport; + private readonly Console _console; + protected readonly Profiler.Profiler Profiler; + private readonly IAuthInterceptor? _auth; + + protected string? Sha256; + protected bool Downloaded; + + protected RemoteHttpFile( + string reference, + IHttpStreamFactory transport, + Console console, + Profiler.Profiler profiler, + IAuthInterceptor? auth) + { + Reference = Preconditions.CheckNotNull(reference); + _transport = Preconditions.CheckNotNull(transport); + _console = Preconditions.CheckNotNull(console); + Profiler = Preconditions.CheckNotNull(profiler); + _auth = auth; + } + + /// Obtain the URL to download the file from. + /// + protected abstract Uri GetRemote(); + + /// Sink that receives the downloaded files. + /// + protected abstract Stream GetSink(); + + /// + /// + protected void Download() + { + lock (this) + { + if (Downloaded) + { + return; + } + Uri remote = GetRemote(); + try + { + _console.ProgressFmt("Fetching {0}", remote); + using Stream sink = GetSink(); + using var digest = SHA256.Create(); + using (Profiler.Start("remote_file_" + remote)) + { + using (Stream source = _transport.Open(remote, _auth)) + using (var digestStream = new CryptoStream(sink, digest, CryptoStreamMode.Write, leaveOpen: true)) + { + source.CopyTo(digestStream); + digestStream.FlushFinalBlock(); + } + Sha256 = Convert.ToHexStringLower(digest.Hash!); + Downloaded = true; + } + } + catch (IOException e) + { + throw new RepoException($"Error downloading {remote}", e); + } + } + } + + [StarlarkMethod("sha256", Documented = false, Doc = "Sha256 of the file.")] + public string GetSha256() + { + Download(); + return Sha256!; + } +} diff --git a/src/Copybara.Core/Toml/TomlContent.cs b/src/Copybara.Core/Toml/TomlContent.cs new file mode 100644 index 000000000..74c272533 --- /dev/null +++ b/src/Copybara.Core/Toml/TomlContent.cs @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; + +using StarlarkRt = Starlark.Eval.Starlark; + +namespace Copybara.Toml; + +/// Represents parsed TOML content. +[StarlarkBuiltin("TomlContent", Doc = "Object containing parsed TOML values.")] +public sealed class TomlContent : IStarlarkValue +{ + private readonly IReadOnlyDictionary _parsedToml; + + internal TomlContent(IReadOnlyDictionary parsedToml) + { + _parsedToml = parsedToml; + } + + [StarlarkMethod( + "get", + Doc = "Retrieve the value from the parsed TOML for the given key. " + + "If the key is not defined, this will return None.")] + public object? Get( + [Param(Name = "key", Doc = "The dotted key expression", Named = true, + AllowedTypes = new[] { typeof(string) })] + string key) + { + try + { + return ConvertToStarlarkValue(Lookup(key)); + } + catch (Exception e) when (e is ArgumentException or InvalidCastException) + { + throw new EvalException( + $"There was an error retrieving the value for the given key {key}", e); + } + } + + [StarlarkMethod( + "get_or_default", + Doc = "Retrieve the value from the parsed TOML for the given key. " + + "If the key is not defined, this will return the default value.")] + public object? GetOrDefault( + [Param(Name = "key", Doc = "The dotted key expression", Named = true, + AllowedTypes = new[] { typeof(string) })] + string key, + [Param(Name = "default", Doc = "The default value to return if the key isn't found.", + Named = true)] + object? defaultValue) + { + try + { + object? value = ConvertToStarlarkValue(Lookup(key)); + if (Equals(value, StarlarkRt.None)) + { + return defaultValue; + } + return value; + } + catch (Exception e) when (e is ArgumentException or InvalidCastException) + { + throw new EvalException( + $"There was an error retrieving the value for the given key {key}", e); + } + } + + /// Resolves a dotted key expression against the parsed tree. + private object? Lookup(string dottedKey) + { + object? node = _parsedToml; + foreach (string part in dottedKey.Split('.')) + { + if (node is IReadOnlyDictionary dict && dict.TryGetValue(part, out var v)) + { + node = v; + } + else + { + return null; + } + } + return node; + } + + /// + /// Converts a parsed TOML value to an object that can be cast to a Starlark value. + /// + /// + /// Return type is object because strings and booleans are valid Starlark values despite + /// not implementing . + /// + private object ConvertToStarlarkValue(object? value) + { + switch (value) + { + case null: + return StarlarkRt.None; + case DateTimeOffset dto: + return new StarlarkDateTimeModule.StarlarkDateTime( + dto.ToUnixTimeSeconds(), "UTC"); + case IReadOnlyList array: + { + var builder = new List(array.Count); + foreach (object? item in array) + { + builder.Add(ConvertToStarlarkValue(item)); + } + return StarlarkList.ImmutableCopyOf(builder); + } + case IReadOnlyDictionary table: + { + var entries = new List>(table.Count); + foreach (var entry in table) + { + entries.Add(new KeyValuePair( + entry.Key, ConvertToStarlarkValue(entry.Value))); + } + return Dict.ImmutableCopyOf(entries); + } + case long l: + return StarlarkInt.Of(l); + case int i: + return StarlarkInt.Of(i); + default: + // string, bool, double are valid Starlark values as-is. + return value; + } + } +} diff --git a/src/Copybara.Core/Toml/TomlModule.cs b/src/Copybara.Core/Toml/TomlModule.cs new file mode 100644 index 000000000..9caeeb847 --- /dev/null +++ b/src/Copybara.Core/Toml/TomlModule.cs @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Toml; + +/// Module for parsing TOML in Starlark. +/// +/// NOTE(port): upstream uses the tomlj library. This port uses a small hand-rolled TOML reader (see +/// ); see that type for supported features and gaps. +/// +[StarlarkBuiltin("toml", Doc = "Module for parsing TOML in Copybara.")] +public sealed class TomlModule : IStarlarkValue +{ + [StarlarkMethod("parse", Doc = "Parse the TOML content. Returns a toml object.")] + public TomlContent Parse( + [Param(Name = "content", Doc = "TOML content to be parsed", Named = true, + AllowedTypes = new[] { typeof(string) })] + string tomlContent) + { + var result = TomlParser.Parse(tomlContent); + + ValidationException.CheckCondition( + !result.HasErrors, + "There were errors parsing the TOML string. Errors: {0}", + string.Join(", ", result.Errors)); + + return new TomlContent(result.Root); + } +} diff --git a/src/Copybara.Core/Toml/TomlParser.cs b/src/Copybara.Core/Toml/TomlParser.cs new file mode 100644 index 000000000..88cabb147 --- /dev/null +++ b/src/Copybara.Core/Toml/TomlParser.cs @@ -0,0 +1,755 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Globalization; +using System.Text; + +namespace Copybara.Toml; + +/// +/// A minimal, hand-rolled TOML parser sufficient for Copybara's Starlark toml.parse surface. +/// +/// +/// NOTE(port): upstream uses the tomlj library. This is a small, from-scratch reader that produces +/// a nested dictionary tree of plain .NET values (, , +/// , , , of +/// values, and nested Dictionary<string, object>). +/// +/// SUPPORTED: comments; bare/quoted keys; dotted keys; standard tables [a.b]; array-of-tables +/// [[a.b]]; basic and literal strings (single-line); multi-line basic and literal strings; +/// integers (with _ separators, hex/oct/bin prefixes); floats (incl. inf/nan/exponents); +/// booleans; offset date-times; arrays (nested, multi-line); inline tables. +/// +/// GAPS (TODO): full TOML date/time coverage is limited to offset date-times parseable by +/// — local date-time / local date / local time are returned as raw +/// strings. Strict duplicate-key / table-redefinition validation is best-effort. Unicode escape +/// edge cases beyond \uXXXX/\UXXXXXXXX are not exhaustively validated. +/// +internal static class TomlParser +{ + public sealed class TomlParseResult + { + public Dictionary Root { get; } = new(); + public List Errors { get; } = new(); + public bool HasErrors => Errors.Count > 0; + } + + public static TomlParseResult Parse(string content) + { + var result = new TomlParseResult(); + try + { + new Impl(content, result).Run(); + } + catch (TomlParseException e) + { + result.Errors.Add(e.Message); + } + return result; + } + + private sealed class TomlParseException(string message) : Exception(message); + + private sealed class Impl(string text, TomlParseResult result) + { + private readonly string _text = text; + private int _pos; + private int _line = 1; + // The table into which bare key/value pairs are currently written. + private Dictionary _current = result.Root; + + public void Run() + { + while (true) + { + SkipWhitespaceAndComments(); + if (Eof) + { + return; + } + + char c = Peek(); + if (c == '[') + { + ParseTableHeader(); + } + else + { + ParseKeyValue(); + } + SkipToLineEndTrivia(); + } + } + + private bool Eof => _pos >= _text.Length; + private char Peek() => _text[_pos]; + private char PeekAt(int off) => _pos + off < _text.Length ? _text[_pos + off] : '\0'; + + private char Next() + { + char c = _text[_pos++]; + if (c == '\n') + { + _line++; + } + return c; + } + + private TomlParseException Err(string msg) => + new($"line {_line}: {msg}"); + + private void SkipWhitespaceAndComments() + { + while (!Eof) + { + char c = Peek(); + if (c is ' ' or '\t' or '\r' or '\n') + { + Next(); + } + else if (c == '#') + { + while (!Eof && Peek() != '\n') + { + Next(); + } + } + else + { + break; + } + } + } + + private void SkipInlineWhitespace() + { + while (!Eof && (Peek() == ' ' || Peek() == '\t')) + { + Next(); + } + } + + // After a value, expect optional whitespace, optional comment, then newline or EOF. + private void SkipToLineEndTrivia() + { + SkipInlineWhitespace(); + if (Eof) + { + return; + } + char c = Peek(); + if (c == '#') + { + while (!Eof && Peek() != '\n') + { + Next(); + } + } + else if (c == '\r') + { + Next(); + } + else if (c != '\n') + { + throw Err($"unexpected trailing content '{c}'"); + } + if (!Eof && Peek() == '\n') + { + Next(); + } + } + + private void ParseTableHeader() + { + Next(); // '[' + bool arrayOfTables = !Eof && Peek() == '['; + if (arrayOfTables) + { + Next(); + } + + SkipInlineWhitespace(); + var keys = ParseKeyPath(); + SkipInlineWhitespace(); + + if (Eof || Next() != ']') + { + throw Err("expected ']' after table header"); + } + if (arrayOfTables) + { + if (Eof || Next() != ']') + { + throw Err("expected ']]' after array-of-tables header"); + } + } + + _current = arrayOfTables + ? DescendArrayOfTables(result.Root, keys) + : DescendTable(result.Root, keys); + } + + private Dictionary DescendTable( + Dictionary root, List keys) + { + var node = root; + for (int i = 0; i < keys.Count; i++) + { + string key = keys[i]; + if (!node.TryGetValue(key, out object? existing)) + { + var child = new Dictionary(); + node[key] = child; + node = child; + } + else if (existing is Dictionary dict) + { + node = dict; + } + else if (existing is List list && list.Count > 0 + && list[^1] is Dictionary last) + { + node = last; + } + else + { + throw Err($"key '{key}' is already defined with a non-table value"); + } + } + return node; + } + + private Dictionary DescendArrayOfTables( + Dictionary root, List keys) + { + var node = root; + for (int i = 0; i < keys.Count - 1; i++) + { + node = DescendTable(node, new List { keys[i] }); + } + + string lastKey = keys[^1]; + if (!node.TryGetValue(lastKey, out object? existing)) + { + var list = new List(); + node[lastKey] = list; + var entry = new Dictionary(); + list.Add(entry); + return entry; + } + if (existing is List arr) + { + var entry = new Dictionary(); + arr.Add(entry); + return entry; + } + throw Err($"key '{lastKey}' is already defined and is not an array of tables"); + } + + private void ParseKeyValue() + { + var keys = ParseKeyPath(); + SkipInlineWhitespace(); + if (Eof || Next() != '=') + { + throw Err("expected '=' in key/value pair"); + } + SkipInlineWhitespace(); + object? value = ParseValue(); + + // Place the value, descending dotted keys into (implicit) tables. + var node = _current; + for (int i = 0; i < keys.Count - 1; i++) + { + string k = keys[i]; + if (!node.TryGetValue(k, out object? existing)) + { + var child = new Dictionary(); + node[k] = child; + node = child; + } + else if (existing is Dictionary dict) + { + node = dict; + } + else + { + throw Err($"key '{k}' is already defined with a non-table value"); + } + } + node[keys[^1]] = value; + } + + private List ParseKeyPath() + { + var keys = new List(); + while (true) + { + SkipInlineWhitespace(); + keys.Add(ParseSingleKey()); + SkipInlineWhitespace(); + if (!Eof && Peek() == '.') + { + Next(); + continue; + } + break; + } + return keys; + } + + private string ParseSingleKey() + { + if (Eof) + { + throw Err("expected a key"); + } + char c = Peek(); + if (c == '"') + { + return ParseBasicString(); + } + if (c == '\'') + { + return ParseLiteralString(); + } + + var sb = new StringBuilder(); + while (!Eof) + { + c = Peek(); + if (char.IsLetterOrDigit(c) || c == '_' || c == '-') + { + sb.Append(Next()); + } + else + { + break; + } + } + if (sb.Length == 0) + { + throw Err($"invalid bare key starting with '{c}'"); + } + return sb.ToString(); + } + + private object? ParseValue() + { + if (Eof) + { + throw Err("expected a value"); + } + char c = Peek(); + switch (c) + { + case '"': + return PeekAt(1) == '"' && PeekAt(2) == '"' + ? ParseMultilineBasicString() + : ParseBasicString(); + case '\'': + return PeekAt(1) == '\'' && PeekAt(2) == '\'' + ? ParseMultilineLiteralString() + : ParseLiteralString(); + case '[': + return ParseArray(); + case '{': + return ParseInlineTable(); + case 't': + case 'f': + return ParseBool(); + default: + return ParseNumberOrDate(); + } + } + + private string ParseBasicString() + { + Next(); // opening quote + var sb = new StringBuilder(); + while (true) + { + if (Eof) + { + throw Err("unterminated string"); + } + char c = Next(); + if (c == '"') + { + return sb.ToString(); + } + if (c == '\\') + { + sb.Append(ReadEscape()); + } + else if (c == '\n') + { + throw Err("newline in single-line string"); + } + else + { + sb.Append(c); + } + } + } + + private string ParseMultilineBasicString() + { + Next(); Next(); Next(); // """ + // A newline immediately following the opening delimiter is trimmed. + if (!Eof && Peek() == '\r') + { + Next(); + } + if (!Eof && Peek() == '\n') + { + Next(); + } + + var sb = new StringBuilder(); + while (true) + { + if (Eof) + { + throw Err("unterminated multi-line string"); + } + if (Peek() == '"' && PeekAt(1) == '"' && PeekAt(2) == '"') + { + Next(); Next(); Next(); + return sb.ToString(); + } + char c = Next(); + if (c == '\\') + { + // Line-ending backslash: trim trailing whitespace/newlines. + int save = _pos; + SkipInlineWhitespace(); + if (!Eof && (Peek() == '\n' || Peek() == '\r')) + { + while (!Eof && (Peek() is ' ' or '\t' or '\r' or '\n')) + { + Next(); + } + continue; + } + _pos = save; + sb.Append(ReadEscape()); + } + else + { + sb.Append(c); + } + } + } + + private string ParseLiteralString() + { + Next(); // opening ' + var sb = new StringBuilder(); + while (true) + { + if (Eof) + { + throw Err("unterminated literal string"); + } + char c = Next(); + if (c == '\'') + { + return sb.ToString(); + } + if (c == '\n') + { + throw Err("newline in single-line literal string"); + } + sb.Append(c); + } + } + + private string ParseMultilineLiteralString() + { + Next(); Next(); Next(); // ''' + if (!Eof && Peek() == '\r') + { + Next(); + } + if (!Eof && Peek() == '\n') + { + Next(); + } + var sb = new StringBuilder(); + while (true) + { + if (Eof) + { + throw Err("unterminated multi-line literal string"); + } + if (Peek() == '\'' && PeekAt(1) == '\'' && PeekAt(2) == '\'') + { + Next(); Next(); Next(); + return sb.ToString(); + } + sb.Append(Next()); + } + } + + private string ReadEscape() + { + if (Eof) + { + throw Err("unterminated escape sequence"); + } + char c = Next(); + switch (c) + { + case 'b': return "\b"; + case 't': return "\t"; + case 'n': return "\n"; + case 'f': return "\f"; + case 'r': return "\r"; + case '"': return "\""; + case '\\': return "\\"; + case 'u': return ReadUnicode(4); + case 'U': return ReadUnicode(8); + default: + throw Err($"invalid escape sequence '\\{c}'"); + } + } + + private string ReadUnicode(int digits) + { + if (_pos + digits > _text.Length) + { + throw Err("invalid unicode escape"); + } + string hex = _text.Substring(_pos, digits); + for (int i = 0; i < digits; i++) + { + Next(); + } + if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out int code)) + { + throw Err($"invalid unicode escape '\\{(digits == 4 ? 'u' : 'U')}{hex}'"); + } + return char.ConvertFromUtf32(code); + } + + private bool ParseBool() + { + if (_text.AsSpan(_pos).StartsWith("true")) + { + _pos += 4; + return true; + } + if (_text.AsSpan(_pos).StartsWith("false")) + { + _pos += 5; + return false; + } + throw Err("invalid boolean value"); + } + + private object? ParseNumberOrDate() + { + int start = _pos; + while (!Eof) + { + char c = Peek(); + if (c is ',' or ']' or '}' or '\n' or '\r' or '#') + { + break; + } + Next(); + } + string token = _text.Substring(start, _pos - start).Trim(); + if (token.Length == 0) + { + throw Err("expected a value"); + } + return InterpretScalar(token); + } + + private object InterpretScalar(string token) + { + // Date-time: contains a '-' or ':' pattern suggestive of a timestamp. + if (LooksLikeDateTime(token) && + DateTimeOffset.TryParse(token, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dto)) + { + return dto; + } + + switch (token) + { + case "inf": + case "+inf": + return double.PositiveInfinity; + case "-inf": + return double.NegativeInfinity; + case "nan": + case "+nan": + case "-nan": + return double.NaN; + } + + string noUnderscore = token.Replace("_", ""); + + // Prefixed integers. + if (noUnderscore.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + return Convert.ToInt64(noUnderscore[2..], 16); + } + if (noUnderscore.StartsWith("0o", StringComparison.OrdinalIgnoreCase)) + { + return Convert.ToInt64(noUnderscore[2..], 8); + } + if (noUnderscore.StartsWith("0b", StringComparison.OrdinalIgnoreCase)) + { + return Convert.ToInt64(noUnderscore[2..], 2); + } + + bool isFloat = noUnderscore.Contains('.') + || noUnderscore.Contains('e', StringComparison.OrdinalIgnoreCase); + if (!isFloat && long.TryParse(noUnderscore, NumberStyles.AllowLeadingSign, + CultureInfo.InvariantCulture, out long l)) + { + return l; + } + if (double.TryParse(noUnderscore, NumberStyles.Float, CultureInfo.InvariantCulture, + out double d)) + { + return d; + } + + // Fall back: this may be a local date/time that DateTimeOffset couldn't parse. + if (LooksLikeDateTime(token)) + { + return token; // GAP: local date/time returned as raw string. + } + + throw Err($"could not parse value '{token}'"); + } + + private static bool LooksLikeDateTime(string token) + { + // e.g. 1979-05-27T07:32:00Z, 1979-05-27, 07:32:00 + if (token.Length < 5) + { + return false; + } + bool hasDate = token.Length >= 10 && token[4] == '-' && token[7] == '-'; + bool hasTime = token.Contains(':'); + return hasDate || hasTime; + } + + private List ParseArray() + { + Next(); // '[' + var list = new List(); + while (true) + { + SkipWhitespaceAndComments(); + if (Eof) + { + throw Err("unterminated array"); + } + if (Peek() == ']') + { + Next(); + return list; + } + list.Add(ParseValue()); + SkipWhitespaceAndComments(); + if (Eof) + { + throw Err("unterminated array"); + } + char c = Peek(); + if (c == ',') + { + Next(); + } + else if (c == ']') + { + Next(); + return list; + } + else + { + throw Err($"expected ',' or ']' in array, got '{c}'"); + } + } + } + + private Dictionary ParseInlineTable() + { + Next(); // '{' + var table = new Dictionary(); + SkipInlineWhitespace(); + if (!Eof && Peek() == '}') + { + Next(); + return table; + } + while (true) + { + SkipInlineWhitespace(); + var keys = ParseKeyPath(); + SkipInlineWhitespace(); + if (Eof || Next() != '=') + { + throw Err("expected '=' in inline table"); + } + SkipInlineWhitespace(); + object? value = ParseValue(); + + var node = table; + for (int i = 0; i < keys.Count - 1; i++) + { + string k = keys[i]; + if (node.TryGetValue(k, out object? ex) + && ex is Dictionary d) + { + node = d; + } + else + { + var child = new Dictionary(); + node[k] = child; + node = child; + } + } + node[keys[^1]] = value; + + SkipInlineWhitespace(); + if (Eof) + { + throw Err("unterminated inline table"); + } + char c = Next(); + if (c == ',') + { + continue; + } + if (c == '}') + { + return table; + } + throw Err($"expected ',' or '}}' in inline table, got '{c}'"); + } + } + } +} diff --git a/src/Copybara.Core/Xml/XmlModule.cs b/src/Copybara.Core/Xml/XmlModule.cs new file mode 100644 index 000000000..276552cb3 --- /dev/null +++ b/src/Copybara.Core/Xml/XmlModule.cs @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Xml; +using System.Xml.XPath; +using Copybara.Config; +using Copybara.Exceptions; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Xml; + +/// Set of functions to work with XML in Copybara. +/// +/// NOTE(port): upstream uses javax.xml + javax.xml.xpath. This port uses the in-box +/// / APIs. +/// +[StarlarkBuiltin("xml", Doc = "Set of functions to work with XML in Copybara.")] +public sealed class XmlModule : IStarlarkValue +{ + [StarlarkMethod("xpath", Doc = "Run an xpath expression")] + public object Compile( + [Param(Name = "content", Doc = "The XML content", Named = true)] + string xmlContent, + [Param(Name = "expression", Doc = "XPath expression", Named = true)] + string expression, + [Param(Name = "type", Named = true, + Doc = "The type of the return value, see http://www.w3.org/TR/xpath" + + "for more details. For now we support STRING, BOOLEAN & NUMBER.")] + string type) + { + XmlDocument xmlDocument = new() { XmlResolver = null }; + try + { + xmlDocument.LoadXml(xmlContent); + + var navigator = xmlDocument.CreateNavigator()!; + var xpathType = SkylarkUtil.StringToEnum("type", type); + var expr = navigator.Compile(expression); + + return xpathType switch + { + XPathTypes.STRING => Evaluate(navigator, expr, XPathResultType.String), + XPathTypes.NUMBER => Evaluate(navigator, expr, XPathResultType.Number), + XPathTypes.BOOLEAN => Evaluate(navigator, expr, XPathResultType.Boolean), + _ => throw new ValidationException($"Unsupported xpath type: {type}"), + }; + } + catch (Exception e) when (e is XmlException or XPathException or EvalException) + { + throw new ValidationException("Error parsing XML", e); + } + } + + private static object Evaluate(XPathNavigator navigator, XPathExpression expr, + XPathResultType returnType) + { + expr.SetContext(new XmlNamespaceManager(new NameTable())); + object result = navigator.Evaluate(expr); + + // Match javax.xml.xpath coercion semantics for STRING/NUMBER/BOOLEAN result types. + return returnType switch + { + XPathResultType.String => Convert.ToString( + result, System.Globalization.CultureInfo.InvariantCulture) ?? "", + XPathResultType.Number => Convert.ToDouble( + result, System.Globalization.CultureInfo.InvariantCulture), + XPathResultType.Boolean => Convert.ToBoolean(result), + _ => result, + }; + } + + private enum XPathTypes + { + STRING, + NUMBER, + BOOLEAN, + } +} From d44ce11b7622e828ef47e151dcf5e58be170c3cb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 09:22:13 +0000 Subject: [PATCH 12/15] Port git origin/dest base, GitHub/Gerrit/GitLab API clients, Mercurial, language modules Wave C (5 agents + consolidation). Whole solution builds 0/0; 15 tests pass. - git/ origin/destination base: GitOrigin, GitDestination, GitDestinationReader, ChangeReader, GitVisitorUtil, Mirror, GitIntegrateChanges, options, write hooks. - git/github/api/ (~50 files): GitHubApi + transport (HttpClient) + entity DTOs with byte-identical JSON property names (gson @Key -> [JsonPropertyName]). - git/gerritapi/ (30 files): GerritApi client, XSSI-prefix-aware parsing, nano timestamp handling. - git/gitlab/api/ (18 files): GitLabApi client + entities. - hg/ (Mercurial): HgRepository (shells to hg via CommandRunner), origin/dest/module. - go/, rust/, python/, tsjs/ (npm): version-resolution modules over HttpClient. Consolidation fixes: namespace/class name clash on GerritApi.NotifyType (global:: qualification), ambiguous record copy-ctor in ListProjectMergeRequestParams, Tuple alias in PythonModule. Provider classes (GitHubPrOrigin/GerritOrigin/GitLabMrOrigin etc.) and GitModule remain for the next wave. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JKh2mEFxE6wgBgzDjpQV6f --- TODO.md | 22 +- src/Copybara.Common/ImmutableListMultimap.cs | 4 + src/Copybara.Core/Checks/ApiChecker.cs | 77 ++ .../Git/AddExcludedFilesToIndex.cs | 200 ++++ src/Copybara.Core/Git/ChangeReader.cs | 345 ++++++ .../Git/CredentialFileHandler.cs | 213 ++++ src/Copybara.Core/Git/EventTrigger.cs | 60 ++ .../Git/FuzzyClosestVersionSelector.cs | 45 + .../Git/GerritApi/AbandonInput.cs | 49 + .../Git/GerritApi/AccountInfo.cs | 115 ++ src/Copybara.Core/Git/GerritApi/ActionInfo.cs | 74 ++ .../Git/GerritApi/ApprovalInfo.cs | 64 ++ src/Copybara.Core/Git/GerritApi/ChangeInfo.cs | 286 +++++ .../Git/GerritApi/ChangeMessageInfo.cs | 112 ++ .../Git/GerritApi/ChangeStatus.cs | 28 + .../Git/GerritApi/ChangesQuery.cs | 94 ++ src/Copybara.Core/Git/GerritApi/CommitInfo.cs | 99 ++ .../Git/GerritApi/DeleteReviewerInput.cs | 36 + .../Git/GerritApi/DeleteVoteInput.cs | 36 + src/Copybara.Core/Git/GerritApi/Empty.cs | 24 + src/Copybara.Core/Git/GerritApi/FetchInfo.cs | 48 + src/Copybara.Core/Git/GerritApi/GerritApi.cs | 250 +++++ .../Git/GerritApi/GerritApiException.cs | 100 ++ .../Git/GerritApi/GerritApiTransportImpl.cs | 179 ++++ .../GerritApiTransportWithChecker.cs | 86 ++ .../Git/GerritApi/GerritApiUtil.cs | 46 + .../Git/GerritApi/GerritEventType.cs | 28 + .../Git/GerritApi/GetChangeInput.cs | 69 ++ .../Git/GerritApi/GitPersonInfo.cs | 67 ++ .../Git/GerritApi/IGerritApiTransport.cs | 47 + .../Git/GerritApi/IncludeResult.cs | 43 + src/Copybara.Core/Git/GerritApi/LabelInfo.cs | 143 +++ src/Copybara.Core/Git/GerritApi/NotifyType.cs | 42 + .../Git/GerritApi/ParentCommitInfo.cs | 53 + .../Git/GerritApi/ProjectAccessInfo.cs | 36 + .../Git/GerritApi/ProjectInfo.cs | 67 ++ .../Git/GerritApi/ProjectInput.cs | 24 + .../Git/GerritApi/RestoreInput.cs | 37 + .../Git/GerritApi/ReviewResult.cs | 73 ++ .../Git/GerritApi/ReviewerInput.cs | 35 + .../Git/GerritApi/RevisionInfo.cs | 116 ++ .../Git/GerritApi/SetReviewInput.cs | 138 +++ .../Git/GerritApi/SubmitInput.cs | 36 + .../SubmitRequirementExpressionInfo.cs | 69 ++ .../SubmitRequirementExpressionStatus.cs | 26 + .../Git/GerritApi/SubmitRequirementInput.cs | 57 + .../GerritApi/SubmitRequirementResultInfo.cs | 71 ++ .../SubmitRequirementResultStatus.cs | 28 + src/Copybara.Core/Git/GitDestination.cs | 995 ++++++++++++++++++ .../Git/GitDestinationOptions.cs | 173 +++ src/Copybara.Core/Git/GitDestinationReader.cs | 104 ++ .../GitHub/Api/AbstractGitHubApiTransport.cs | 311 ++++++ .../Git/GitHub/Api/AddAssignees.cs | 41 + src/Copybara.Core/Git/GitHub/Api/AddLabels.cs | 44 + .../Git/GitHub/Api/AuthorAssociation.cs | 33 + src/Copybara.Core/Git/GitHub/Api/CheckRun.cs | 209 ++++ src/Copybara.Core/Git/GitHub/Api/CheckRuns.cs | 63 ++ .../Git/GitHub/Api/CheckSuite.cs | 75 ++ .../Git/GitHub/Api/CheckSuites.cs | 54 + .../Git/GitHub/Api/ClientError.cs | 99 ++ .../Git/GitHub/Api/CombinedStatus.cs | 65 ++ .../Git/GitHub/Api/CommentBody.cs | 39 + src/Copybara.Core/Git/GitHub/Api/Commit.cs | 84 ++ .../Git/GitHub/Api/CommitHistoryResponse.cs | 228 ++++ .../Git/GitHub/Api/CreatePullRequest.cs | 69 ++ .../Git/GitHub/Api/CreateReleaseRequest.cs | 141 +++ .../Git/GitHub/Api/CreateStatusRequest.cs | 57 + src/Copybara.Core/Git/GitHub/Api/GitHubApi.cs | 674 ++++++++++++ .../Git/GitHub/Api/GitHubApiException.cs | 111 ++ .../Git/GitHub/Api/GitHubApiJson.cs | 49 + .../Git/GitHub/Api/GitHubApiTransportImpl.cs | 62 ++ .../Api/GitHubApiTransportWithChecker.cs | 57 + src/Copybara.Core/Git/GitHub/Api/GitHubApp.cs | 46 + .../Git/GitHub/Api/GitHubCommit.cs | 75 ++ .../Git/GitHub/Api/GitHubEventType.cs | 67 ++ .../Git/GitHub/Api/GitHubGraphQLApi.cs | 174 +++ .../Git/GitHub/Api/IGitHubApiTransport.cs | 54 + .../Git/GitHub/Api/Installation.cs | 52 + .../Git/GitHub/Api/Installations.cs | 57 + src/Copybara.Core/Git/GitHub/Api/Issue.cs | 70 ++ .../Git/GitHub/Api/IssueComment.cs | 73 ++ .../Api/IssuesAndPullRequestsSearchResults.cs | 52 + src/Copybara.Core/Git/GitHub/Api/Label.cs | 37 + .../Git/GitHub/Api/Organization.cs | 41 + src/Copybara.Core/Git/GitHub/Api/Output.cs | 51 + .../Git/GitHub/Api/PaginatedList.cs | 142 +++ .../GitHub/Api/PaginatedListJsonConverter.cs | 66 ++ .../Git/GitHub/Api/PaginatedPayload.cs | 39 + .../Git/GitHub/Api/PullRequest.cs | 85 ++ .../Git/GitHub/Api/PullRequestComment.cs | 106 ++ .../Git/GitHub/Api/PullRequestOrIssue.cs | 97 ++ src/Copybara.Core/Git/GitHub/Api/Ref.cs | 66 ++ src/Copybara.Core/Git/GitHub/Api/Release.cs | 50 + .../Git/GitHub/Api/Repository.cs | 51 + src/Copybara.Core/Git/GitHub/Api/Review.cs | 77 ++ src/Copybara.Core/Git/GitHub/Api/Revision.cs | 58 + src/Copybara.Core/Git/GitHub/Api/Status.cs | 131 +++ .../Git/GitHub/Api/UpdatePullRequest.cs | 64 ++ .../Git/GitHub/Api/UpdateReferenceRequest.cs | 43 + src/Copybara.Core/Git/GitHub/Api/User.cs | 50 + .../Git/GitHub/Api/UserPermissionLevel.cs | 58 + src/Copybara.Core/Git/GitIntegrateChanges.cs | 458 ++++++++ .../Git/GitLab/Api/Entities/Commit.cs | 30 + .../Api/Entities/CreateMergeRequestParams.cs | 38 + .../Api/Entities/ExternalStatusCheck.cs | 76 ++ .../GitLab/Api/Entities/GitLabApiEntity.cs | 22 + .../GitLab/Api/Entities/GitLabApiParams.cs | 82 ++ .../Entities/ListProjectMergeRequestParams.cs | 38 + .../GitLab/Api/Entities/ListUsersParams.cs | 27 + .../Git/GitLab/Api/Entities/MergeRequest.cs | 198 ++++ .../GitLab/Api/Entities/PaginatedPageList.cs | 117 ++ .../Git/GitLab/Api/Entities/Project.cs | 42 + .../Entities/SetExternalStatusCheckParams.cs | 29 + .../SetExternalStatusCheckResponse.cs | 54 + .../Api/Entities/UpdateMergeRequestParams.cs | 47 + .../Git/GitLab/Api/Entities/User.cs | 42 + src/Copybara.Core/Git/GitLab/Api/GitLabApi.cs | 181 ++++ .../Git/GitLab/Api/GitLabApiException.cs | 45 + .../Git/GitLab/Api/GitLabApiTransport.cs | 56 + .../Git/GitLab/Api/GitLabApiTransportImpl.cs | 238 +++++ src/Copybara.Core/Git/GitMirrorContext.cs | 513 +++++++++ src/Copybara.Core/Git/GitMirrorOptions.cs | 27 + src/Copybara.Core/Git/GitOptions.cs | 230 ++++ src/Copybara.Core/Git/GitOrigin.cs | 819 ++++++++++++++ src/Copybara.Core/Git/GitOriginOptions.cs | 101 ++ src/Copybara.Core/Git/GitRepositoryHook.cs | 41 + src/Copybara.Core/Git/GitVisitorUtil.cs | 91 ++ src/Copybara.Core/Git/Mirror.cs | 307 ++++++ src/Copybara.Core/Go/GoModule.cs | 102 ++ src/Copybara.Core/Go/GoProxyVersionList.cs | 209 ++++ .../Go/GoProxyVersionResolver.cs | 134 +++ src/Copybara.Core/Go/GoVersionObject.cs | 96 ++ src/Copybara.Core/Go/PseudoVersionSelector.cs | 43 + src/Copybara.Core/Hg/ChangeReader.cs | 171 +++ src/Copybara.Core/Hg/HgDestination.cs | 345 ++++++ src/Copybara.Core/Hg/HgModule.cs | 62 ++ src/Copybara.Core/Hg/HgOptions.cs | 74 ++ src/Copybara.Core/Hg/HgOrigin.cs | 315 ++++++ src/Copybara.Core/Hg/HgOriginOptions.cs | 85 ++ src/Copybara.Core/Hg/HgRepository.cs | 391 +++++++ src/Copybara.Core/Hg/HgRevision.cs | 61 ++ src/Copybara.Core/Hg/HgVisitorUtil.cs | 96 ++ src/Copybara.Core/Python/PackageMetadata.cs | 122 +++ src/Copybara.Core/Python/PythonModule.cs | 44 + .../Rust/ComparisonRustVersionRequirement.cs | 100 ++ .../Rust/DefaultRustVersionRequirement.cs | 103 ++ .../Rust/EpochRustVersionRequirement.cs | 96 ++ .../Rust/MultipleRustVersionRequirement.cs | 78 ++ .../Rust/RustCratesIoVersionList.cs | 188 ++++ .../Rust/RustCratesIoVersionResolver.cs | 103 ++ .../Rust/RustCratesIoVersionSelector.cs | 68 ++ src/Copybara.Core/Rust/RustModule.cs | 155 +++ .../Rust/RustRegistryVersionObject.cs | 114 ++ .../Rust/RustVersionRequirement.cs | 241 +++++ .../Rust/TildeRustVersionRequirement.cs | 81 ++ .../Rust/WildcardRustVersionRequirement.cs | 87 ++ src/Copybara.Core/TsJs/Npm/NpmDistInfo.cs | 35 + src/Copybara.Core/TsJs/Npm/NpmModule.cs | 99 ++ .../TsJs/Npm/NpmPackageIdentifier.cs | 66 ++ src/Copybara.Core/TsJs/Npm/NpmVersionInfo.cs | 46 + src/Copybara.Core/TsJs/Npm/NpmVersionList.cs | 121 +++ .../TsJs/Npm/NpmVersionListResponseObject.cs | 50 + .../TsJs/Npm/NpmVersionResolver.cs | 103 ++ 163 files changed, 18089 insertions(+), 10 deletions(-) create mode 100644 src/Copybara.Core/Checks/ApiChecker.cs create mode 100644 src/Copybara.Core/Git/AddExcludedFilesToIndex.cs create mode 100644 src/Copybara.Core/Git/ChangeReader.cs create mode 100644 src/Copybara.Core/Git/CredentialFileHandler.cs create mode 100644 src/Copybara.Core/Git/EventTrigger.cs create mode 100644 src/Copybara.Core/Git/FuzzyClosestVersionSelector.cs create mode 100644 src/Copybara.Core/Git/GerritApi/AbandonInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/AccountInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ActionInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ApprovalInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ChangeInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ChangeMessageInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ChangeStatus.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ChangesQuery.cs create mode 100644 src/Copybara.Core/Git/GerritApi/CommitInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/DeleteReviewerInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/DeleteVoteInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/Empty.cs create mode 100644 src/Copybara.Core/Git/GerritApi/FetchInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/GerritApi.cs create mode 100644 src/Copybara.Core/Git/GerritApi/GerritApiException.cs create mode 100644 src/Copybara.Core/Git/GerritApi/GerritApiTransportImpl.cs create mode 100644 src/Copybara.Core/Git/GerritApi/GerritApiTransportWithChecker.cs create mode 100644 src/Copybara.Core/Git/GerritApi/GerritApiUtil.cs create mode 100644 src/Copybara.Core/Git/GerritApi/GerritEventType.cs create mode 100644 src/Copybara.Core/Git/GerritApi/GetChangeInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/GitPersonInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/IGerritApiTransport.cs create mode 100644 src/Copybara.Core/Git/GerritApi/IncludeResult.cs create mode 100644 src/Copybara.Core/Git/GerritApi/LabelInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/NotifyType.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ParentCommitInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ProjectAccessInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ProjectInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ProjectInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/RestoreInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ReviewResult.cs create mode 100644 src/Copybara.Core/Git/GerritApi/ReviewerInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/RevisionInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/SetReviewInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/SubmitInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/SubmitRequirementExpressionInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/SubmitRequirementExpressionStatus.cs create mode 100644 src/Copybara.Core/Git/GerritApi/SubmitRequirementInput.cs create mode 100644 src/Copybara.Core/Git/GerritApi/SubmitRequirementResultInfo.cs create mode 100644 src/Copybara.Core/Git/GerritApi/SubmitRequirementResultStatus.cs create mode 100644 src/Copybara.Core/Git/GitDestination.cs create mode 100644 src/Copybara.Core/Git/GitDestinationOptions.cs create mode 100644 src/Copybara.Core/Git/GitDestinationReader.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/AbstractGitHubApiTransport.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/AddAssignees.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/AddLabels.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/AuthorAssociation.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CheckRun.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CheckRuns.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CheckSuite.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CheckSuites.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/ClientError.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CombinedStatus.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CommentBody.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Commit.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CommitHistoryResponse.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CreatePullRequest.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CreateReleaseRequest.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/CreateStatusRequest.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/GitHubApi.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/GitHubApiException.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/GitHubApiJson.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/GitHubApiTransportImpl.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/GitHubApiTransportWithChecker.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/GitHubApp.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/GitHubCommit.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/GitHubEventType.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/GitHubGraphQLApi.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/IGitHubApiTransport.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Installation.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Installations.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Issue.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/IssueComment.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/IssuesAndPullRequestsSearchResults.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Label.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Organization.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Output.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/PaginatedList.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/PaginatedListJsonConverter.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/PaginatedPayload.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/PullRequest.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/PullRequestComment.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/PullRequestOrIssue.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Ref.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Release.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Repository.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Review.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Revision.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/Status.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/UpdatePullRequest.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/UpdateReferenceRequest.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/User.cs create mode 100644 src/Copybara.Core/Git/GitHub/Api/UserPermissionLevel.cs create mode 100644 src/Copybara.Core/Git/GitIntegrateChanges.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/Commit.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/CreateMergeRequestParams.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/ExternalStatusCheck.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/GitLabApiEntity.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/GitLabApiParams.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/ListProjectMergeRequestParams.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/ListUsersParams.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/MergeRequest.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/PaginatedPageList.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/Project.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/SetExternalStatusCheckParams.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/SetExternalStatusCheckResponse.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/UpdateMergeRequestParams.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/Entities/User.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/GitLabApi.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/GitLabApiException.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/GitLabApiTransport.cs create mode 100644 src/Copybara.Core/Git/GitLab/Api/GitLabApiTransportImpl.cs create mode 100644 src/Copybara.Core/Git/GitMirrorContext.cs create mode 100644 src/Copybara.Core/Git/GitMirrorOptions.cs create mode 100644 src/Copybara.Core/Git/GitOptions.cs create mode 100644 src/Copybara.Core/Git/GitOrigin.cs create mode 100644 src/Copybara.Core/Git/GitOriginOptions.cs create mode 100644 src/Copybara.Core/Git/GitRepositoryHook.cs create mode 100644 src/Copybara.Core/Git/GitVisitorUtil.cs create mode 100644 src/Copybara.Core/Git/Mirror.cs create mode 100644 src/Copybara.Core/Go/GoModule.cs create mode 100644 src/Copybara.Core/Go/GoProxyVersionList.cs create mode 100644 src/Copybara.Core/Go/GoProxyVersionResolver.cs create mode 100644 src/Copybara.Core/Go/GoVersionObject.cs create mode 100644 src/Copybara.Core/Go/PseudoVersionSelector.cs create mode 100644 src/Copybara.Core/Hg/ChangeReader.cs create mode 100644 src/Copybara.Core/Hg/HgDestination.cs create mode 100644 src/Copybara.Core/Hg/HgModule.cs create mode 100644 src/Copybara.Core/Hg/HgOptions.cs create mode 100644 src/Copybara.Core/Hg/HgOrigin.cs create mode 100644 src/Copybara.Core/Hg/HgOriginOptions.cs create mode 100644 src/Copybara.Core/Hg/HgRepository.cs create mode 100644 src/Copybara.Core/Hg/HgRevision.cs create mode 100644 src/Copybara.Core/Hg/HgVisitorUtil.cs create mode 100644 src/Copybara.Core/Python/PackageMetadata.cs create mode 100644 src/Copybara.Core/Python/PythonModule.cs create mode 100644 src/Copybara.Core/Rust/ComparisonRustVersionRequirement.cs create mode 100644 src/Copybara.Core/Rust/DefaultRustVersionRequirement.cs create mode 100644 src/Copybara.Core/Rust/EpochRustVersionRequirement.cs create mode 100644 src/Copybara.Core/Rust/MultipleRustVersionRequirement.cs create mode 100644 src/Copybara.Core/Rust/RustCratesIoVersionList.cs create mode 100644 src/Copybara.Core/Rust/RustCratesIoVersionResolver.cs create mode 100644 src/Copybara.Core/Rust/RustCratesIoVersionSelector.cs create mode 100644 src/Copybara.Core/Rust/RustModule.cs create mode 100644 src/Copybara.Core/Rust/RustRegistryVersionObject.cs create mode 100644 src/Copybara.Core/Rust/RustVersionRequirement.cs create mode 100644 src/Copybara.Core/Rust/TildeRustVersionRequirement.cs create mode 100644 src/Copybara.Core/Rust/WildcardRustVersionRequirement.cs create mode 100644 src/Copybara.Core/TsJs/Npm/NpmDistInfo.cs create mode 100644 src/Copybara.Core/TsJs/Npm/NpmModule.cs create mode 100644 src/Copybara.Core/TsJs/Npm/NpmPackageIdentifier.cs create mode 100644 src/Copybara.Core/TsJs/Npm/NpmVersionInfo.cs create mode 100644 src/Copybara.Core/TsJs/Npm/NpmVersionList.cs create mode 100644 src/Copybara.Core/TsJs/Npm/NpmVersionListResponseObject.cs create mode 100644 src/Copybara.Core/TsJs/Npm/NpmVersionResolver.cs diff --git a/TODO.md b/TODO.md index 92bfee3a6..aa9c75163 100644 --- a/TODO.md +++ b/TODO.md @@ -107,13 +107,14 @@ Largest single module (~175 files). Port in slices: to upstream which shells out), `GitRevision`, `GitRepoType`, `GitEnvironment`, `GitCredential`, `Refspec`, `FetchResult`, `MergeResult`, `IntegrateLabel`, `SameGitTree`, exceptions. -- ⬜ Origin/Destination: `GitOrigin`, `GitDestination`, `GitModule`, - `GitMirror`, `ChangeReader`, `GitVisitorUtil`, options, writer hooks. -- ⬜ GitHub: `github/api` client (System.Text.Json + HttpClient), - `GitHubOrigin`, `GitHubPrDestination`, `GitHubPrOrigin`, `github/util`. -- ⬜ Gerrit: `gerritapi` client, `GerritOrigin`, `GerritDestination`. -- ⬜ GitLab: `gitlab/api`, origin/destination. -- ✅ `version/` resolvers. +- ✅ Origin/Destination base: `GitOrigin`, `GitDestination`, `GitDestinationReader`, + `ChangeReader`, `GitVisitorUtil`, `Mirror`, `GitIntegrateChanges`, options, write hooks. +- ⬜ `GitModule` (the `git` Starlark module, ~3677 LOC — registers all git.* factories). +- ✅ GitHub `github/api` client (System.Text.Json + HttpClient, ~50 files). + ⬜ GitHub providers: `GitHubPrOrigin`/`GitHubPrDestination`/`GitHubOrigin`/`github/util`. +- ✅ Gerrit `gerritapi` client (30 files). ⬜ Gerrit providers: `GerritOrigin`/`GerritDestination`/`GerritEndpoint`. +- ✅ GitLab `gitlab/api` client (18 files). ⬜ GitLab providers: `GitLabMrOrigin`/`GitLabMrDestination`. +- ✅ `version/` resolvers. ✅ `hg/` (Mercurial). ## Phase 6 — Other origins/destinations & modules @@ -122,9 +123,10 @@ Largest single module (~175 files). Port in slices: `http/` (HttpClient), `format/` (buildifier), `buildozer/`, `toml/`, `json/`, `xml/`, `html/`, `re2/`, `credentials/`, `approval/`, `action/`, `treestate/`, `monitor/`, `effect/`, `checks/` (minimal stub). -- ⬜ `hg/` (Mercurial), `go/`, `rust/`, `python/`, `tsjs/`, `feedback/`, - `checks/` (full), `regenerate/`, `onboard/`, `configgen/`, `doc/` (reference - doc generator), `transform/patch/`. +- ✅ `hg/` (Mercurial), `go/`, `rust/`, `python/`, `tsjs/` (npm). +- ⬜ `feedback/`, `checks/` (full), `regenerate/`, `onboard/`, `configgen/`, + `doc/` (reference doc generator), `transform/patch/`, GitHub/Gerrit/GitLab + provider classes, `GitModule`. ## Phase 7 — CLI (`src/Copybara.Cli`) diff --git a/src/Copybara.Common/ImmutableListMultimap.cs b/src/Copybara.Common/ImmutableListMultimap.cs index 1abf62e2a..1941f9c1c 100644 --- a/src/Copybara.Common/ImmutableListMultimap.cs +++ b/src/Copybara.Common/ImmutableListMultimap.cs @@ -44,6 +44,10 @@ private ImmutableListMultimap( public static ImmutableListMultimap Empty => EmptyInstance; + /// Creates a multimap with a single key/value entry (Guava parity for of). + public static ImmutableListMultimap Of(TKey key, TValue value) => + CreateBuilder().Put(key, value).Build(); + /// All values associated with in insertion order (empty if none). public ImmutableArray this[TKey key] => _index.TryGetValue(key, out var values) ? values : ImmutableArray.Empty; diff --git a/src/Copybara.Core/Checks/ApiChecker.cs b/src/Copybara.Core/Checks/ApiChecker.cs new file mode 100644 index 000000000..9487c3a4a --- /dev/null +++ b/src/Copybara.Core/Checks/ApiChecker.cs @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Checks; + +/// +/// A checker for API clients that delegates on a and provides convenience +/// methods for checking one or more pairs of field names and values, plus error handling. +/// +public class ApiChecker +{ + private readonly IChecker _checker; + private readonly Console _console; + + public ApiChecker(IChecker checker, Console console) + { + _checker = Preconditions.CheckNotNull(checker); + _console = Preconditions.CheckNotNull(console); + } + + /// Performs a check on the given request field. + /// + public void Check(string field, object value) => + DoCheck(ImmutableDictionary.CreateRange(new[] + { + KeyValuePair.Create(field, value.ToString()!), + })); + + /// Performs a check on the given request fields. + /// + public void Check(string field1, object value1, string field2, object value2) => + DoCheck(ImmutableDictionary.CreateRange(new[] + { + KeyValuePair.Create(field1, value1.ToString()!), + KeyValuePair.Create(field2, value2.ToString()!), + })); + + /// Performs a check on the given request fields. + /// + public void Check( + string field1, object value1, string field2, object value2, string field3, object value3) => + DoCheck(ImmutableDictionary.CreateRange(new[] + { + KeyValuePair.Create(field1, value1.ToString()!), + KeyValuePair.Create(field2, value2.ToString()!), + KeyValuePair.Create(field3, value3.ToString()!), + })); + + private void DoCheck(ImmutableDictionary data) + { + try + { + _checker.DoCheck(data, _console); + } + catch (IOException e) + { + throw new InvalidOperationException("Error running checker", e); + } + } +} diff --git a/src/Copybara.Core/Git/AddExcludedFilesToIndex.cs b/src/Copybara.Core/Git/AddExcludedFilesToIndex.cs new file mode 100644 index 000000000..5db263cc3 --- /dev/null +++ b/src/Copybara.Core/Git/AddExcludedFilesToIndex.cs @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Util; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Git; + +/// +/// A walker which adds all files not matching a glob to the index of a Git repo using +/// git add. Port of com.google.copybara.git.AddExcludedFilesToIndex. +/// +internal sealed class AddExcludedFilesToIndex +{ + private static readonly Regex SubmoduleStatusPrefix = new("^-[0-9a-f]{40,64} ", RegexOptions.Compiled); + + private readonly GitRepository _repo; + private readonly IPathMatcher _pathMatcher; + private readonly string _workTree; + private List? _addBackSubmodules; + + // Relative paths (using '/' separators) to add back to the index. Sorted for stable batching. + private readonly SortedSet _toExclude = new(StringComparer.Ordinal); + + internal AddExcludedFilesToIndex(GitRepository repo, IPathMatcher pathMatcher) + { + _repo = Preconditions.CheckNotNull(repo); + _workTree = Preconditions.CheckNotNull(repo.GetWorkTree()); + _pathMatcher = Preconditions.CheckNotNull(pathMatcher); + } + + internal void Prepare(string workdir) + { + var included = new HashSet(StringComparer.Ordinal); + var prevExcluded = new List(); + IReadOnlyList head; + try + { + head = _repo.LsTree(_repo.ResolveReference("HEAD"), null, true, true); + } + catch (CannotResolveRevisionException) + { + // Destination is empty. Nothing to revert. + return; + } + foreach (var treeElement in head) + { + string relative = Normalize(treeElement.Path); + if (_pathMatcher.Matches(Path.Combine(_workTree, treeElement.Path))) + { + AddPathAndParents(included, relative); + } + else + { + prevExcluded.Add(relative); + if (IsHidden(relative)) + { + // File is not included but 'git add dir' doesn't work for 'dir/.file'. + AddPathAndParents(included, Parent(relative)); + } + } + } + + foreach (var file in EnumerateRelativeFiles(workdir)) + { + AddPathAndParents(included, file); + } + + foreach (var path in prevExcluded) + { + var segments = path.Split('/'); + string search = ""; + foreach (var segment in segments) + { + search = search.Length == 0 ? segment : search + "/" + segment; + if (search == path) + { + _toExclude.Add(search); + break; + } + if (!included.Contains(search)) + { + _toExclude.Add(search); + break; + } + } + } + } + + private static void AddPathAndParents(HashSet included, string? path) + { + while (!string.IsNullOrEmpty(path) && !included.Contains(path)) + { + Preconditions.CheckArgument(!Path.IsPathRooted(path)); + included.Add(path); + path = Parent(path); + } + } + + /// + /// Finds and records the path of all submodules. This should be called when they are not staged + /// for deletion. + /// + internal void FindSubmodules(Console console) + { + _addBackSubmodules = new List(); + + string submoduleStatus = _repo.SimpleCommand("submodule", "status").GetStdout(); + foreach (var line in submoduleStatus.Split('\n')) + { + if (line.Length == 0) + { + continue; + } + string submoduleName = SubmoduleStatusPrefix.Replace(line, "", 1); + if (submoduleName == line) + { + console.Warn("Cannot parse line from 'git submodule status': " + line); + continue; + } + if (!_pathMatcher.Matches(Path.Combine(_workTree, submoduleName))) + { + _addBackSubmodules.Add(submoduleName); + } + } + } + + /// Adds all the excluded files and submodules. + internal void Add() + { + int size = 0; + var current = new List(); + foreach (var path in _toExclude) + { + current.Add(path); + size += path.Length; + // Split the executions in chunks of 6K. 8K triggers arg max in some systems. + if (size > 6 * 1024) + { + _repo.Add().Force().Files(current).Run(); + current = new List(); + size = 0; + } + } + if (current.Count != 0) + { + _repo.Add().Force().Files(current).Run(); + } + + foreach (var addBackSubmodule in _addBackSubmodules ?? new List()) + { + _repo.SimpleCommand("reset", "--", "--quiet", addBackSubmodule); + _repo.Add().Force().Files(new[] { addBackSubmodule }).Run(); + } + } + + private IEnumerable EnumerateRelativeFiles(string workdir) + { + if (!Directory.Exists(workdir)) + { + yield break; + } + foreach (var file in Directory.EnumerateFiles( + workdir, "*", SearchOption.AllDirectories)) + { + yield return Normalize(Path.GetRelativePath(workdir, file)); + } + } + + private static string Normalize(string path) => path.Replace('\\', '/'); + + private static string? Parent(string path) + { + int idx = path.LastIndexOf('/'); + return idx < 0 ? null : path.Substring(0, idx); + } + + private static bool IsHidden(string relativePath) + { + int idx = relativePath.LastIndexOf('/'); + string name = idx < 0 ? relativePath : relativePath.Substring(idx + 1); + return name.StartsWith('.'); + } +} diff --git a/src/Copybara.Core/Git/ChangeReader.cs b/src/Copybara.Core/Git/ChangeReader.cs new file mode 100644 index 000000000..1fe0d6548 --- /dev/null +++ b/src/Copybara.Core/Git/ChangeReader.cs @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Util; +using Console = Copybara.Util.Console.Console; +using AuthoringT = Copybara.Authoring.Authoring; +using Author = Copybara.Authoring.Author; + +namespace Copybara.Git; + +/// +/// Utility class to introspect the log of a Git repository. Port of +/// com.google.copybara.git.ChangeReader. +/// +internal sealed class ChangeReader +{ + internal const string BranchCommitLogHeading = "-- Branch commit log --"; + + private readonly AuthoringT? _authoring; + private readonly GitRepository _repository; + private readonly int _limit; + private readonly ImmutableArray _roots; + private readonly bool _includeBranchCommitLogs; + private readonly string? _url; + private readonly bool _firstParent; + private readonly bool _partialFetch; + private readonly bool _topoOrder; + private readonly int _skip; + private readonly int _batchSize; + private readonly string? _grepString; + + private ChangeReader( + AuthoringT? authoring, + GitRepository repository, + int limit, + IEnumerable roots, + bool includeBranchCommitLogs, + string? url, + bool firstParent, + bool partialFetch, + bool topoOrder, + int skip, + int batchSize, + string? grepString) + { + _authoring = authoring; + _repository = Preconditions.CheckNotNull(repository, "repository"); + _limit = limit; + _roots = roots.ToImmutableArray(); + _includeBranchCommitLogs = includeBranchCommitLogs; + _url = url; + _firstParent = firstParent; + _partialFetch = partialFetch; + _topoOrder = topoOrder; + _skip = skip; + _batchSize = batchSize; + _grepString = grepString; + } + + internal IReadOnlyList> Run(GitRevision rev) => + Run( + null, + rev, + historyIsNonLinear: false, + ImmutableDictionary>.Empty); + + /// + /// Computes a list of changes from the refExpression and propagates labels contained in + /// onto the resulting change list. + /// + internal IReadOnlyList> Run( + GitRevision? fromRev, + GitRevision toRev, + bool historyIsNonLinear, + IReadOnlyDictionary> labels) + { + string refExpression = + fromRev == null || historyIsNonLinear + ? toRev.GetHash() + : fromRev.GetHash() + ".." + toRev.GetHash(); + GitRepository.LogCmd logCmd = + _repository.Log(refExpression).FirstParent(_firstParent).TopoOrder(_topoOrder); + if (_limit != -1) + { + logCmd = logCmd.WithLimit(_limit); + } + if (_skip > 0) + { + logCmd = logCmd.WithSkip(_skip); + } + if (_batchSize > 0) + { + logCmd = logCmd.WithBatchSize(_batchSize); + } + if (_grepString != null) + { + logCmd = logCmd.Grep(_grepString); + } + if (_partialFetch && _roots.Contains("")) + { + throw new ValidationException( + "Config error: partial_fetch feature is not compatible with fetching the whole" + + " repo."); + } + if (_partialFetch) + { + logCmd = logCmd.WithPaths(_roots); + } + + // Log command does not filter by roots here because of how git log works. Some commits (e.g. + // fake merges) might not include the files in the log, and filtering here would return + // incorrect results. We do filter later on the changes to match the actual glob. + return ParseChanges( + logCmd.IncludeFiles(true).IncludeMergeDiff(true).Run(), labels, toRev); + } + + private string BranchCommitLog(GitRevision @ref, IReadOnlyList parents) + { + if (parents.Count <= 1) + { + // Not a merge commit, so don't bother showing full log of branch commits. + return ""; + } + if (!_includeBranchCommitLogs) + { + return ""; + } + + IReadOnlyList entries = + _repository + .Log(parents[0].GetHash() + ".." + @ref.GetHash()) + .WithPaths(Glob.IsEmptyRoot(_roots) ? ImmutableArray.Empty : _roots) + .FirstParent(false) + .Run(); + + if (entries.Count == 0) + { + return ""; + } + // Remove the merge commit. Since we already have that in the body. + entries = entries.Skip(1).ToList(); + + var sb = new StringBuilder("\n").Append(BranchCommitLogHeading).Append('\n'); + bool first = true; + foreach (var e in entries) + { + if (!first) + { + sb.Append('\n'); + } + sb.Append("commit ") + .Append(e.Commit.GetHash()) + .Append('\n') + .Append("Author: ") + .Append(FilterAuthor(e.Author)) + .Append('\n') + .Append("Date: ") + .Append(e.AuthorDate) + .Append('\n') + .Append('\n') + .Append(" ") + .Append((e.Body ?? "").Replace("\n", " \n")); + first = false; + } + return sb.ToString(); + } + + private IReadOnlyList> ParseChanges( + IReadOnlyList logEntries, + IReadOnlyDictionary> labels, + GitRevision toRev) + { + var result = new List>(); + GitRevision? last = null; + foreach (var e in logEntries) + { + // Keep the first commit if repeated (merge commits). + if (last != null && last.Equals(e.Commit)) + { + continue; + } + last = e.Commit; + ImmutableListMultimap labelsToCopy = + labels.TryGetValue(e.Commit.GetHash(), out var found) + ? found + : ImmutableListMultimap.Empty; + // Carry over the context reference to the corresponding change in the list. + if (last.GetHash() == toRev.GetHash() && toRev.ContextReference() != null) + { + last = last.WithContextReference(toRev.ContextReference()!); + } + result.Add( + new Change( + (_url != null ? last.WithUrl(_url) : last).WithLabels(labelsToCopy), + FilterAuthor(e.Author), + (e.Body ?? "") + BranchCommitLog(last, e.Parents), + e.AuthorDate, + ChangeMessage.ParseAllAsLabels(e.Body ?? "").LabelsAsMultimap(), + e.Files, + e.Parents.Count > 1, + e.Parents.ToImmutableArray())); + } + result.Reverse(); + return result; + } + + private Author FilterAuthor(Author author) => + _authoring == null || _authoring.UseAuthor(author.Email) + ? author + : _authoring.GetDefaultAuthor(); + + /// Builder for . + internal sealed class Builder + { + private AuthoringT? _authoring; + private readonly GitRepository _repository; + private int _limit = -1; + private ImmutableArray _roots = ImmutableArray.Create(""); + private bool _includeBranchCommitLogs; + private string? _url; + private bool _firstParent; + private bool _topoOrder; + private bool _partialFetch; + private int _skip; + private int _batchSize; + private string? _grepString; + + internal static Builder ForDestination(GitRepository repository, Console console) => + new(repository, console); + + internal static Builder ForOrigin( + AuthoringT authoring, GitRepository repository, Console console) => + new Builder(repository, console).SetAuthoring(authoring); + + private Builder(GitRepository repository, Console console) + { + _repository = Preconditions.CheckNotNull(repository, "repository"); + Preconditions.CheckNotNull(console, "console"); + } + + internal Builder SetLimit(int limit) + { + Preconditions.CheckArgument(limit > 0); + _limit = limit; + return this; + } + + internal Builder SetSkip(int skip) + { + Preconditions.CheckArgument(skip >= 0); + _skip = skip; + return this; + } + + internal Builder SetBatchSize(int batchSize) + { + Preconditions.CheckArgument(batchSize >= 0); + _batchSize = batchSize; + return this; + } + + private Builder SetAuthoring(AuthoringT authoring) + { + _authoring = Preconditions.CheckNotNull(authoring, "authoring"); + return this; + } + + internal Builder SetPartialFetch(bool partialFetch) + { + _partialFetch = partialFetch; + return this; + } + + internal Builder SetFirstParent(bool firstParent) + { + _firstParent = firstParent; + return this; + } + + internal Builder SetTopoOrder(bool topoOrder) + { + _topoOrder = topoOrder; + return this; + } + + internal Builder SetIncludeBranchCommitLogs(bool includeBranchCommitLogs) + { + _includeBranchCommitLogs = includeBranchCommitLogs; + return this; + } + + internal Builder SetUrl(string? url) + { + _url = url; + return this; + } + + /// Only return commits that match the given paths in the Git log command. + internal Builder SetRoots(IEnumerable roots) + { + _roots = roots.ToImmutableArray(); + return this; + } + + /// Grep for the given pattern in the Git log command. + internal Builder Grep(string grepString) + { + _grepString = grepString; + return this; + } + + internal ChangeReader Build() => + new( + _authoring, + _repository, + _limit, + _roots, + _includeBranchCommitLogs, + _url, + _firstParent, + _partialFetch, + _topoOrder, + _skip, + _batchSize, + _grepString); + } +} diff --git a/src/Copybara.Core/Git/CredentialFileHandler.cs b/src/Copybara.Core/Git/CredentialFileHandler.cs new file mode 100644 index 000000000..f806da811 --- /dev/null +++ b/src/Copybara.Core/Git/CredentialFileHandler.cs @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2024 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; +using Copybara.Common; +using Copybara.Credentials; +using Copybara.Exceptions; + +namespace Copybara.Git; + +/// +/// Holder to handle https access tokens for Git Repos. Port of +/// com.google.copybara.git.CredentialFileHandler. +/// +public class CredentialFileHandler +{ + private static readonly object FileLock = new(); + + private readonly string _scheme; + private readonly string _host; + private readonly string _path; + private readonly CredentialIssuer _username; + private readonly CredentialIssuer _password; + private Credential? _currentPassword; + private Credential? _currentUsername; + private readonly object _sync = new(); + + private readonly bool _enabled; + + public CredentialFileHandler( + string scheme, + string host, + string path, + CredentialIssuer username, + CredentialIssuer password, + bool enabled) + { + _scheme = Preconditions.CheckNotNull(scheme); + _host = Preconditions.CheckNotNull(host); + _path = Preconditions.CheckNotNull(path); + _username = Preconditions.CheckNotNull(username); + _password = Preconditions.CheckNotNull(password); + _enabled = enabled; + } + + public CredentialFileHandler( + string host, string path, CredentialIssuer username, CredentialIssuer password, bool enable) + : this("https", host, path, username, password, enable) + { + } + + public CredentialFileHandler( + string host, string path, CredentialIssuer username, CredentialIssuer password) + : this("https", host, path, username, password, true) + { + } + + public CredentialFileHandler( + string scheme, string host, string path, CredentialIssuer username, CredentialIssuer password) + : this(scheme, host, path, username, password, true) + { + } + + /// Obtain a token for the username field from the username Issuer. + public string GetUsername() => GetUsernameCred().ProvideSecret(); + + /// Obtain a token for the password field from the password Issuer. + public string GetPassword() => GetPasswordCred().ProvideSecret(); + + private Credential GetPasswordCred() + { + lock (_sync) + { + if (_currentPassword == null || !_currentPassword.Valid()) + { + _currentPassword = _password.Issue(); + } + return _currentPassword; + } + } + + private Credential GetUsernameCred() + { + lock (_sync) + { + if (_currentUsername == null || !_currentUsername.Valid()) + { + _currentUsername = _username.Issue(); + } + return _currentUsername; + } + } + + public void Install(GitRepository repo, string credentialHelper) + { + if (!_enabled) + { + return; + } + repo.ReplaceLocalConfigField("credential", "useHttpPath", "true"); + WriteTokenToCredFile(credentialHelper); + repo.WithCredentialHelper("store --file=" + credentialHelper); + } + + /// + /// Writes an entry for the token into the given creds file. If the token has expired, calling + /// this again will update the token. + /// + public void WriteTokenToCredFile(string file) + { + lock (FileLock) + { + try + { + var lines = new List(); + if (File.Exists(file)) + { + lines.AddRange(File.ReadAllLines(file)); + } + string entry; + Regex pattern; + try + { + entry = GetCredentialEntry(GetPasswordCred().ProvideSecret()); + pattern = new Regex( + "^" + + Regex.Escape(GetCredentialEntry("PASSWORD_PLACEHOLDER")) + .Replace("PASSWORD_PLACEHOLDER", "[^@]+") + + "$"); + } + catch (Exception e) + when (e is CredentialRetrievalException or CredentialIssuingException) + { + throw new RepoException("Issue minting token", e); + } + bool missing = true; + for (int i = 0; i < lines.Count; i++) + { + string line = lines[i]; + if (line == entry) + { + return; + } + if (pattern.IsMatch(line)) + { + lines[i] = entry; + missing = false; + } + } + if (missing) + { + lines.Add(entry); + } + File.WriteAllText( + file, + string.Join("\n", lines.Where(s => !string.IsNullOrEmpty(s))) + "\n"); + } + catch (IOException e) + { + throw new RepoException($"Error writing access token for {_host}/{_path}", e); + } + } + } + + private string GetCredentialEntry(string pw) => + $"{_scheme}://{Uri.EscapeDataString(GetUsername())}:{Uri.EscapeDataString(pw)}@{_host}/{_path}"; + + /// + /// Helper to print a cred files without exposing tokens. Do not use this output for anything but + /// debugging. + /// + public string GetScrubbedFileContentForDebug(string file) + { + if (!File.Exists(file)) + { + return ""; + } + try + { + return ScrubCredential(File.ReadAllText(file)); + } + catch (IOException e) + { + return ""; + } + } + + private static string ScrubCredential(string line) => + Regex.Replace( + line, "([^:\\n]+://[^:\\n]+):[^@\\n]+(@[^\\n]*)", "$1:$2"); + + public override string ToString() => + $"CredentialFileHandler{{host={_host}, path={_path}, password={_password.Describe()}," + + $" username={_username.Describe()}}}"; + + public IReadOnlyList> DescribeCredentials() => + ImmutableArray.Create(_username.Describe(), _password.Describe()); +} diff --git a/src/Copybara.Core/Git/EventTrigger.cs b/src/Copybara.Core/Git/EventTrigger.cs new file mode 100644 index 000000000..78b5bf81c --- /dev/null +++ b/src/Copybara.Core/Git/EventTrigger.cs @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Git.GitHub.Api; + +namespace Copybara.Git; + +/// +/// A simple pair to express GitHub Events with arbitrary subtypes (Status, CheckRun). Port of +/// com.google.copybara.git.EventTrigger. +/// +public sealed class EventTrigger +{ + private readonly GitHubEventType _type; + private readonly ImmutableHashSet _subtypes; + + private EventTrigger(GitHubEventType type, ImmutableHashSet subtypes) + { + _type = type; + _subtypes = subtypes; + } + + public GitHubEventType Type() => _type; + + public IReadOnlySet Subtypes() => _subtypes; + + public static EventTrigger Create(GitHubEventType type, IEnumerable subtypes) => + new(type, subtypes.ToImmutableHashSet()); + + public override bool Equals(object? o) => + o is EventTrigger other && _type == other._type && _subtypes.SetEquals(other._subtypes); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(_type); + foreach (var s in _subtypes.OrderBy(x => x, StringComparer.Ordinal)) + { + hash.Add(s); + } + return hash.ToHashCode(); + } + + public override string ToString() => + $"EventTrigger{{type={_type}, subtypes=[{string.Join(", ", _subtypes)}]}}"; +} diff --git a/src/Copybara.Core/Git/FuzzyClosestVersionSelector.cs b/src/Copybara.Core/Git/FuzzyClosestVersionSelector.cs new file mode 100644 index 000000000..0c13cdcbb --- /dev/null +++ b/src/Copybara.Core/Git/FuzzyClosestVersionSelector.cs @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Git; + +/// +/// A VersionSelector that heuristically tries to match a version to a git tag. This is best effort +/// and only recommended for testing. Port of +/// com.google.copybara.git.FuzzyClosestVersionSelector. +/// +/// The upstream implementation composes an OrderedVersionSelector over a +/// TagVersionList. Those git/version helper types are owned by a peer port; until they land, +/// this falls back to the documented behavior of returning the requested ref. +/// +public class FuzzyClosestVersionSelector +{ + public string SelectVersion(string? requestedRef, GitRepository repo, string url, Console console) + { + ValidationException.CheckCondition( + !string.IsNullOrEmpty(requestedRef), + "Fuzzy version finding requires a ref to be explicitly specified"); + + // TODO(peer): Wire up OrderedVersionSelector over the git TagVersionList once the + // git/version helper types (TagVersionList, RequestedShaVersionSelector) are ported. Until + // then, degrade to returning the requested ref, which is the documented best-effort + // fallback for this selector. + return requestedRef!; + } +} diff --git a/src/Copybara.Core/Git/GerritApi/AbandonInput.cs b/src/Copybara.Core/Git/GerritApi/AbandonInput.cs new file mode 100644 index 000000000..300dd4c20 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/AbandonInput.cs @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#abandon-input +/// +/// NotifyInfo (notify_details) not included for now. +/// +public class AbandonInput : IStarlarkPrintableValue +{ + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("notify")] + public string? Notify { get; set; } + + private AbandonInput(string? message, NotifyType? notify) + { + Message = message; + Notify = notify?.ToWireValue(); + } + + public static AbandonInput Create(string? message, NotifyType? notify) => + new(message, notify); + + public static AbandonInput CreateWithoutComment() => new(null, null); + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => $"AbandonInput{{message={Message}, notify={Notify}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/AccountInfo.cs b/src/Copybara.Core/Git/GerritApi/AccountInfo.cs new file mode 100644 index 000000000..4366967e1 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/AccountInfo.cs @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-info +[StarlarkBuiltin("gerritapi.AccountInfo", Doc = "Gerrit account information.")] +public class AccountInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("_account_id")] + public long AccountId { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } + + [JsonPropertyName("secondary_emails")] + public IReadOnlyList? SecondaryEmails { get; set; } + + [JsonPropertyName("username")] + public string? Username { get; set; } + + public AccountInfo() + { + } + + public AccountInfo(long accountId, string? email) + { + AccountId = accountId; + Email = email; + } + + public long GetAccountId() => AccountId; + + [StarlarkMethod( + "account_id", + Doc = "The numeric ID of the account.", + StructField = true)] + public string GetAccountIdAsString() => AccountId.ToString(CultureInfo.InvariantCulture); + + [StarlarkMethod( + "name", + Doc = + "The full name of the user.\n" + + "Only set if detailed account information is requested.\n" + + "See option DETAILED_ACCOUNTS for change queries\n" + + "and option DETAILS for account queries.", + StructField = true, + AllowReturnNones = true)] + public string? GetName() => Name; + + [StarlarkMethod( + "email", + Doc = + "The email address the user prefers to be contacted through.\n" + + "Only set if detailed account information is requested.\n" + + "See option DETAILED_ACCOUNTS for change queries\n" + + "and options DETAILS and ALL_EMAILS for account queries.", + StructField = true, + AllowReturnNones = true)] + public string? GetEmail() => Email; + + [StarlarkMethod( + "secondary_emails", + Doc = + "A list of the secondary email addresses of the user.\n" + + "Only set for account queries when the ALL_EMAILS option or the suggest " + + "parameter is set.\n" + + "Secondary emails are only included if the calling user has the Modify Account, " + + "and hence is allowed to see secondary emails of other users.", + StructField = true)] + public IReadOnlyList GetSecondaryEmails() => + SecondaryEmails is null ? ImmutableArray.Empty : SecondaryEmails.ToImmutableArray(); + + [StarlarkMethod( + "username", + Doc = + "The username of the user.\n" + + "Only set if detailed account information is requested.\n" + + "See option DETAILED_ACCOUNTS for change queries\n" + + "and option DETAILS for account queries.", + StructField = true, + AllowReturnNones = true)] + public string? GetUsername() => Username; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"AccountInfo{{accountId={AccountId}, name={Name}, email={Email}, " + + $"secondaryEmails={FormatList(SecondaryEmails)}, username={Username}}}"; + + private protected static string FormatList(IEnumerable? items) => + items is null ? "null" : "[" + string.Join(", ", items) + "]"; +} diff --git a/src/Copybara.Core/Git/GerritApi/ActionInfo.cs b/src/Copybara.Core/Git/GerritApi/ActionInfo.cs new file mode 100644 index 000000000..377367751 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ActionInfo.cs @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#action-info +[StarlarkBuiltin("gerritapi.getActionInfo", Doc = "Gerrit actions information.")] +public class ActionInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("method")] + public string? Method { get; set; } + + [JsonPropertyName("label")] + public string? Label { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + public ActionInfo() + { + } + + public ActionInfo(string method, string label, string title, bool enabled) + { + Method = method; + Label = label; + Title = title; + Enabled = enabled; + } + + public string? GetMethod() => Method; + + [StarlarkMethod( + "label", + Doc = "Short title to display to a user describing the action", + StructField = true, + AllowReturnNones = true)] + public string? GetLabel() => Label; + + public string? GetTitle() => Title; + + [StarlarkMethod( + "enabled", + Doc = + "If true the action is permitted at this time and the caller is likely " + + "allowed to execute it.", + StructField = true)] + public bool GetEnabled() => Enabled; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"ActionInfo{{method={Method}, label={Label}, title={Title}, enabled={Enabled}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/ApprovalInfo.cs b/src/Copybara.Core/Git/GerritApi/ApprovalInfo.cs new file mode 100644 index 000000000..37b448cb1 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ApprovalInfo.cs @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; + +namespace Copybara.Git.GerritApi; + +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#approval-info +[StarlarkBuiltin("gerritapi.ApprovalInfo", Doc = "Gerrit approval information.")] +public class ApprovalInfo : AccountInfo +{ + [JsonPropertyName("value")] + public int Value { get; set; } + + [JsonPropertyName("date")] + public string? Date { get; set; } + + public ApprovalInfo() + { + } + + public ApprovalInfo(long accountId, string? email, int value) + : base(accountId, email) + { + Value = value; + } + + [StarlarkMethod( + "value", + Doc = + "The vote that the user has given for the label. If present and zero, the user " + + "is permitted to vote on the label. If absent, the user is not permitted to vote " + + "on that label.", + StructField = true)] + public int GetValue() => Value; + + public DateTimeOffset GetDate() => GerritApiUtil.ParseTimestamp(Date!); + + [StarlarkMethod( + "date", + Doc = "The time and date describing when the approval was made.", + StructField = true, + AllowReturnNones = true)] + public string? GetDateForSkylark() => Date; + + public override string ToString() => + $"ApprovalInfo{{accountId={AccountId}, name={Name}, email={Email}, " + + $"secondaryEmails={FormatList(SecondaryEmails)}, username={Username}, " + + $"value={Value}, date={Date}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/ChangeInfo.cs b/src/Copybara.Core/Git/GerritApi/ChangeInfo.cs new file mode 100644 index 000000000..a011d9605 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ChangeInfo.cs @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info +[StarlarkBuiltin("gerritapi.ChangeInfo", Doc = "Gerrit change information.")] +public class ChangeInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("triplet_id")] + public string? TripletId { get; set; } + + [JsonPropertyName("project")] + public string? Project { get; set; } + + [JsonPropertyName("branch")] + public string? Branch { get; set; } + + [JsonPropertyName("topic")] + public string? Topic { get; set; } + + [JsonPropertyName("change_id")] + public string? ChangeIdField { get; set; } + + [JsonPropertyName("subject")] + public string? Subject { get; set; } + + [JsonPropertyName("status")] + public string? StatusString { get; set; } + + [JsonPropertyName("created")] + public string? Created { get; set; } + + [JsonPropertyName("updated")] + public string? Updated { get; set; } + + [JsonPropertyName("submitted")] + public string? Submitted { get; set; } + + [JsonPropertyName("submittable")] + public bool Submittable { get; set; } + + [JsonPropertyName("work_in_progress")] + public bool WorkInProgress { get; set; } + + [JsonPropertyName("_number")] + public long Number { get; set; } + + [JsonPropertyName("owner")] + public AccountInfo? Owner { get; set; } + + [JsonPropertyName("submit_requirements")] + public IReadOnlyList? SubmitRequirementsField { get; set; } + + [JsonPropertyName("labels")] + public IReadOnlyDictionary? LabelsField { get; set; } + + [JsonPropertyName("messages")] + public IReadOnlyList? MessagesField { get; set; } + + [JsonPropertyName("current_revision")] + public string? CurrentRevision { get; set; } + + [JsonPropertyName("revisions")] + public IReadOnlyDictionary? AllRevisionsField { get; set; } + + [JsonPropertyName("_more_changes")] + public bool MoreChanges { get; set; } + + [JsonPropertyName("reviewers")] + public IReadOnlyDictionary>? ReviewersField { get; set; } + + [StarlarkMethod( + "id", + Doc = + "The ID of the change in the format \"`~~`\", where " + + "'project', 'branch' and 'Change-Id' are URL encoded. For 'branch' the " + + "refs/heads/ prefix is omitted.", + StructField = true, + AllowReturnNones = true)] + public string? GetId() => Id; + + [StarlarkMethod( + "triplet_id", + Doc = + "The ID of the change in the format \"'~~'\", where 'project'" + + " and 'branch' are URL encoded. For 'branch' the refs/heads/ prefix is omitted.", + StructField = true, + AllowReturnNones = true)] + public string? GetTripletId() => TripletId; + + [StarlarkMethod( + "project", + Doc = "The name of the project.", + StructField = true, + AllowReturnNones = true)] + public string? GetProject() => Project; + + [StarlarkMethod( + "branch", + Doc = "The name of the target branch.\nThe refs/heads/ prefix is omitted.", + StructField = true, + AllowReturnNones = true)] + public string? GetBranch() => Branch; + + [StarlarkMethod( + "topic", + Doc = "The topic to which this change belongs.", + StructField = true, + AllowReturnNones = true)] + public string? GetTopic() => Topic; + + [StarlarkMethod( + "change_id", + Doc = "The Change-Id of the change.", + StructField = true, + AllowReturnNones = true)] + public string? GetChangeId() => ChangeIdField; + + [StarlarkMethod( + "subject", + Doc = "The subject of the change (header line of the commit message).", + StructField = true, + AllowReturnNones = true)] + public string? GetSubject() => Subject; + + public ChangeStatus GetStatus() => Enum.Parse(StatusString!); + + [StarlarkMethod( + "status", + Doc = "The status of the change (NEW, MERGED, ABANDONED).", + StructField = true, + AllowReturnNones = true)] + public string? GetStatusAsString() => StatusString; + + public DateTimeOffset GetCreated() => GerritApiUtil.ParseTimestamp(Created!); + + [StarlarkMethod( + "created", + Doc = "The timestamp of when the change was created.", + StructField = true, + AllowReturnNones = true)] + public string? GetCreatedForSkylark() => Created; + + public DateTimeOffset GetUpdated() => GerritApiUtil.ParseTimestamp(Updated!); + + [StarlarkMethod( + "updated", + Doc = "The timestamp of when the change was last updated.", + StructField = true, + AllowReturnNones = true)] + public string? GetUpdatedForSkylark() => Updated; + + public DateTimeOffset GetSubmitted() => GerritApiUtil.ParseTimestamp(Submitted!); + + [StarlarkMethod( + "submitted", + Doc = "The timestamp of when the change was submitted.", + StructField = true, + AllowReturnNones = true)] + public string? GetSubmittedForSkylark() => Submitted; + + [StarlarkMethod( + "submittable", + Doc = + "Whether the change has been approved by the project submit rules. Only set if " + + "requested via additional field SUBMITTABLE.", + StructField = true)] + public bool IsSubmittable() => Submittable; + + [StarlarkMethod( + "work_in_progress", + Doc = "Whether the change is marked as \"Work in progress\".", + StructField = true)] + public bool IsWorkInProgress() => WorkInProgress; + + public long GetNumber() => Number; + + [StarlarkMethod("number", Doc = "The legacy numeric ID of the change.", StructField = true)] + public string GetNumberAsString() => Number.ToString(CultureInfo.InvariantCulture); + + [StarlarkMethod( + "owner", + Doc = "The owner of the change as an AccountInfo entity.", + StructField = true, + AllowReturnNones = true)] + public AccountInfo? GetOwner() => Owner; + + public IReadOnlyList GetSubmitRequirements() => + SubmitRequirementsField is null + ? ImmutableArray.Empty + : SubmitRequirementsField.ToImmutableArray(); + + [StarlarkMethod( + "submit_requirements", + Doc = "A list of the evaluated submit requirements for the change.", + StructField = true)] + public IReadOnlyList GetSubmitRequirementsForSkylark() => + GetSubmitRequirements(); + + public IReadOnlyDictionary GetLabels() => + LabelsField is null ? ImmutableDictionary.Empty : LabelsField.ToImmutableDictionary(); + + [StarlarkMethod( + "labels", + Doc = + "The labels of the change as a map that maps the label names to LabelInfo entries.\n" + + "Only set if labels or detailed labels are requested.", + StructField = true)] + public IReadOnlyDictionary GetLabelsForSkylark() => GetLabels(); + + public IReadOnlyList GetMessages() => + MessagesField is null ? ImmutableArray.Empty : MessagesField.ToImmutableArray(); + + [StarlarkMethod( + "messages", + Doc = + "Messages associated with the change as a list of ChangeMessageInfo entities.\n" + + "Only set if messages are requested.", + StructField = true)] + public IReadOnlyList GetMessagesForSkylark() => GetMessages(); + + [StarlarkMethod( + "current_revision", + Doc = + "The commit ID of the current patch set of this change.\n" + + "Only set if the current revision is requested or if all revisions are requested.", + StructField = true, + AllowReturnNones = true)] + public string? GetCurrentRevision() => CurrentRevision; + + public IReadOnlyDictionary GetAllRevisions() => + AllRevisionsField is null + ? ImmutableDictionary.Empty + : AllRevisionsField.ToImmutableDictionary(); + + [StarlarkMethod( + "revisions", + Doc = + "All patch sets of this change as a map that maps the commit ID of the patch set to a " + + "RevisionInfo entity.\n" + + "Only set if the current revision is requested (in which case it will only contain " + + "a key for the current revision) or if all revisions are requested.", + StructField = true)] + public IReadOnlyDictionary GetAllRevisionsForSkylark() => GetAllRevisions(); + + public IReadOnlyDictionary> GetReviewers() => + ReviewersField is null + ? ImmutableDictionary>.Empty + : ReviewersField.ToImmutableDictionary(); + + public bool IsMoreChanges() => MoreChanges; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"ChangeInfo{{id={Id}, project={Project}, branch={Branch}, topic={Topic}, " + + $"changeId={ChangeIdField}, subject={Subject}, status={StatusString}, created={Created}, " + + $"updated={Updated}, submitted={Submitted}, submittable={Submittable}, " + + $"work_in_progress={WorkInProgress}, number={Number}, owner={Owner}, " + + $"submitRequirements={SubmitRequirementsField}, labels={LabelsField}, " + + $"messages={MessagesField}, currentRevision={CurrentRevision}, " + + $"allRevisions={AllRevisionsField}, moreChanges={MoreChanges}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/ChangeMessageInfo.cs b/src/Copybara.Core/Git/GerritApi/ChangeMessageInfo.cs new file mode 100644 index 000000000..7358ec336 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ChangeMessageInfo.cs @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-message-info +/// +[StarlarkBuiltin("gerritapi.ChangeMessageInfo", Doc = "Gerrit change message information.")] +public class ChangeMessageInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("author")] + public AccountInfo? Author { get; set; } + + [JsonPropertyName("real_author")] + public AccountInfo? RealAuthor { get; set; } + + [JsonPropertyName("date")] + public string? Date { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("tag")] + public string? Tag { get; set; } + + [JsonPropertyName("_revision_number")] + public int RevisionNumber { get; set; } + + [StarlarkMethod( + "id", + Doc = "The ID of the message.", + StructField = true, + AllowReturnNones = true)] + public string? GetId() => Id; + + [StarlarkMethod( + "author", + Doc = + "Author of the message as an AccountInfo entity.\n" + + "Unset if written by the Gerrit system.", + StructField = true, + AllowReturnNones = true)] + public AccountInfo? GetAuthor() => Author; + + [StarlarkMethod( + "real_author", + Doc = + "Real author of the message as an AccountInfo entity.\n" + + "Set if the message was posted on behalf of another user.", + StructField = true, + AllowReturnNones = true)] + public AccountInfo? GetRealAuthor() => RealAuthor; + + public DateTimeOffset GetDate() => GerritApiUtil.ParseTimestamp(Date!); + + [StarlarkMethod( + "date", + Doc = "The timestamp of when this identity was constructed.", + StructField = true, + AllowReturnNones = true)] + public string? GetDateForSkylark() => Date; + + [StarlarkMethod( + "message", + Doc = "The text left by the user.", + StructField = true, + AllowReturnNones = true)] + public string? GetMessage() => Message; + + [StarlarkMethod( + "tag", + Doc = + "Value of the tag field from ReviewInput set while posting the review. " + + "NOTE: To apply different tags on on different votes/comments multiple " + + "invocations of the REST call are required.", + StructField = true, + AllowReturnNones = true)] + public string? GetTag() => Tag; + + [StarlarkMethod( + "revision_number", + Doc = "Which patchset (if any) generated this message.", + StructField = true)] + public int GetRevisionNumber() => RevisionNumber; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"ChangeMessageInfo{{id={Id}, author={Author}, realAuthor={RealAuthor}, date={Date}, " + + $"message={Message}, tag={Tag}, revisionNumber={RevisionNumber}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/ChangeStatus.cs b/src/Copybara.Core/Git/GerritApi/ChangeStatus.cs new file mode 100644 index 000000000..2363ca1fc --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ChangeStatus.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Git.GerritApi; + +/// +/// Change status for Changes as defined in +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info +/// +public enum ChangeStatus +{ + NEW, + MERGED, + ABANDONED, +} diff --git a/src/Copybara.Core/Git/GerritApi/ChangesQuery.cs b/src/Copybara.Core/Git/GerritApi/ChangesQuery.cs new file mode 100644 index 000000000..c708f3e71 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ChangesQuery.cs @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Globalization; +using System.Text; +using Copybara.Common; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// +/// An object that represents the input parameters for a changes query: +/// +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes +/// +[Starlark.Annot.StarlarkBuiltin( + "gerritapi.ChangesQuery", + Doc = + "Input for listing Gerrit changes. See " + + "https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes")] +public class ChangesQuery : IStarlarkPrintableValue +{ + private readonly string _query; + private readonly IReadOnlySet _include; + private readonly int? _limit; + private readonly int? _start; + + public ChangesQuery(string query) + { + _query = query; + _include = ImmutableHashSet.Empty; + _limit = null; + _start = null; + } + + private ChangesQuery(string query, IReadOnlySet include, int? limit, int? start) + { + _query = Preconditions.CheckNotNull(query); + _include = Preconditions.CheckNotNull(include); + _limit = limit; + _start = start; + } + + public ChangesQuery WithStart(int start) => new(_query, _include, _limit, start); + + public ChangesQuery WithLimit(int limit) => new(_query, _include, limit, _start); + + public ChangesQuery WithInclude(IEnumerable include) => + new(_query, include.ToImmutableHashSet(), _limit, _start); + + public string AsUrlParams() + { + var sb = new StringBuilder("q=").Append(Escape(_query)); + foreach (var includeResult in _include) + { + sb.Append("&o=").Append(includeResult); + } + + if (_limit != null) + { + sb.Append("&n=").Append(_limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (_start != null) + { + sb.Append("&S=").Append(_start.Value.ToString(CultureInfo.InvariantCulture)); + } + + return sb.ToString(); + } + + private static string Escape(string query) => Uri.EscapeDataString(query); + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"ChangesQuery{{query={_query}, include=[{string.Join(", ", _include)}], " + + $"limit={_limit?.ToString(CultureInfo.InvariantCulture) ?? "null"}, " + + $"start={_start?.ToString(CultureInfo.InvariantCulture) ?? "null"}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/CommitInfo.cs b/src/Copybara.Core/Git/GerritApi/CommitInfo.cs new file mode 100644 index 000000000..5c9589a5a --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/CommitInfo.cs @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#commit-info +[StarlarkBuiltin("gerritapi.CommitInfo", Doc = "Gerrit commit information.")] +public class CommitInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("commit")] + public string? Commit { get; set; } + + [JsonPropertyName("parents")] + public IReadOnlyList? Parents { get; set; } + + [JsonPropertyName("author")] + public GitPersonInfo? Author { get; set; } + + [JsonPropertyName("committer")] + public GitPersonInfo? Committer { get; set; } + + [JsonPropertyName("subject")] + public string? Subject { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [StarlarkMethod( + "commit", + Doc = + "The commit ID. Not set if included in a RevisionInfo entity that is contained " + + "in a map which has the commit ID as key.", + StructField = true, + AllowReturnNones = true)] + public string? GetCommit() => Commit; + + public IReadOnlyList GetParents() => + Parents is null ? ImmutableArray.Empty : Parents.ToImmutableArray(); + + [StarlarkMethod( + "parents", + Doc = + "The parent commits of this commit as a list of CommitInfo entities. " + + "In each parent only the commit and subject fields are populated.", + StructField = true)] + public IReadOnlyList GetMessagesForSkylark() => GetParents(); + + [StarlarkMethod( + "author", + Doc = "The author of the commit as a GitPersonInfo entity.", + StructField = true, + AllowReturnNones = true)] + public GitPersonInfo? GetAuthor() => Author; + + [StarlarkMethod( + "committer", + Doc = "The committer of the commit as a GitPersonInfo entity.", + StructField = true, + AllowReturnNones = true)] + public GitPersonInfo? GetCommitter() => Committer; + + [StarlarkMethod( + "subject", + Doc = "The subject of the commit (header line of the commit message).", + StructField = true, + AllowReturnNones = true)] + public string? GetSubject() => Subject; + + [StarlarkMethod( + "message", + Doc = "The commit message.", + StructField = true, + AllowReturnNones = true)] + public string? GetMessage() => Message; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"CommitInfo{{commit={Commit}, parents={Parents}, author={Author}, " + + $"committer={Committer}, subject={Subject}, message={Message}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/DeleteReviewerInput.cs b/src/Copybara.Core/Git/GerritApi/DeleteReviewerInput.cs new file mode 100644 index 000000000..c5154cbd5 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/DeleteReviewerInput.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; + +namespace Copybara.Git.GerritApi; + +/// +/// See +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-reviewer-input +/// +/// NotifyInfo (notify_details) not included for now. +/// +public class DeleteReviewerInput +{ + [JsonPropertyName("notify")] + public string? Notify { get; set; } + + public DeleteReviewerInput(NotifyType? notify) + { + Notify = notify?.ToWireValue(); + } +} diff --git a/src/Copybara.Core/Git/GerritApi/DeleteVoteInput.cs b/src/Copybara.Core/Git/GerritApi/DeleteVoteInput.cs new file mode 100644 index 000000000..015fe3856 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/DeleteVoteInput.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; + +namespace Copybara.Git.GerritApi; + +/// +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-vote-input +/// request json. +/// NotifyInfo (notify_details) not included for now. +/// label not included for now since it matches the label in the URL. +/// +public class DeleteVoteInput +{ + [JsonPropertyName("notify")] + public string? Notify { get; set; } + + public DeleteVoteInput(NotifyType? notify) + { + Notify = notify?.ToWireValue(); + } +} diff --git a/src/Copybara.Core/Git/GerritApi/Empty.cs b/src/Copybara.Core/Git/GerritApi/Empty.cs new file mode 100644 index 000000000..897071be8 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/Empty.cs @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Git.GerritApi; + +/// +/// An empty object for operations that return nothing or when we want to ignore the result. +/// +public class Empty +{ +} diff --git a/src/Copybara.Core/Git/GerritApi/FetchInfo.cs b/src/Copybara.Core/Git/GerritApi/FetchInfo.cs new file mode 100644 index 000000000..7c82ea279 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/FetchInfo.cs @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#fetch-info +public class FetchInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("ref")] + public string? Ref { get; set; } + + public FetchInfo() + { + } + + public FetchInfo(string url, string @ref) + { + Url = url; + Ref = @ref; + } + + public string? GetUrl() => Url; + + public string? GetRef() => Ref; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => $"FetchInfo{{url={Url}, ref={Ref}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/GerritApi.cs b/src/Copybara.Core/Git/GerritApi/GerritApi.cs new file mode 100644 index 000000000..09586e642 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/GerritApi.cs @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Common; +using Copybara.Exceptions; +using ProfilerType = Copybara.Profiler.Profiler; + +namespace Copybara.Git.GerritApi; + +/// +/// A mini API for getting and updating Gerrit projects through the Gerrit REST API. Port of +/// com.google.copybara.git.gerritapi.GerritApi. +/// +public class GerritApi +{ + protected readonly IGerritApiTransport Transport; + protected readonly ProfilerType ProfilerInstance; + + public GerritApi(IGerritApiTransport transport, ProfilerType profiler) + { + Transport = Preconditions.CheckNotNull(transport); + ProfilerInstance = Preconditions.CheckNotNull(profiler); + } + + public async Task> GetChangesAsync(ChangesQuery query) + { + using (ProfilerInstance.Start("gerrit_get_changes")) + { + var result = await Transport + .GetAsync>("/changes/?" + query.AsUrlParams()) + .ConfigureAwait(false); + return (result ?? new List()).ToImmutableArray(); + } + } + + public async Task GetChangeAsync(string changeId, GetChangeInput input) + { + using (ProfilerInstance.Start("gerrit_get_change")) + { + return (await Transport + .GetAsync("/changes/" + changeId + "?" + input.AsUrlParams()) + .ConfigureAwait(false))!; + } + } + + public async Task GetChangeDetailAsync(string changeId, GetChangeInput input) + { + using (ProfilerInstance.Start("gerrit_get_change_detail")) + { + return (await Transport + .GetAsync("/changes/" + changeId + "/detail?" + input.AsUrlParams()) + .ConfigureAwait(false))!; + } + } + + public async Task AbandonChangeAsync(string changeId, AbandonInput abandonInput) + { + using (ProfilerInstance.Start("gerrit_abandon_change")) + { + return (await Transport + .PostAsync("/changes/" + changeId + "/abandon", abandonInput) + .ConfigureAwait(false))!; + } + } + + public async Task RestoreChangeAsync(string changeId, RestoreInput restoreInput) + { + using (ProfilerInstance.Start("gerrit_restore_change")) + { + return (await Transport + .PostAsync("/changes/" + changeId + "/restore", restoreInput) + .ConfigureAwait(false))!; + } + } + + public async Task CheckSubmitRequirementAsync( + string changeId, SubmitRequirementInput submitRequirementInput) + { + using (ProfilerInstance.Start("gerrit_check_submit_requirement")) + { + return (await Transport + .PostAsync( + "/changes/" + changeId + "/check.submit_requirement", submitRequirementInput) + .ConfigureAwait(false))!; + } + } + + /// + /// Look for a Gerrit project using its ID. The ID differs from the name in that certain + /// characters are escaped. E.g. plugins%2Freplication vs plugins/replication. + /// + /// a ProjectInfo if project is found, otherwise null. + public async Task GetProjectByIdAsync(string id) + { + using (ProfilerInstance.Start("gerrit_list_projects")) + { + try + { + return await Transport + .GetAsync("/projects/" + id) + .ConfigureAwait(false); + } + catch (GerritApiException e) + { + if (e.GetResponseCode() == GerritApiException.ResponseCodeValue.NOT_FOUND) + { + return null; + } + + throw; + } + } + } + + public async Task CreateProjectAsync(string project) + { + using (ProfilerInstance.Start("gerrit_create_project")) + { + return (await Transport + .PutAsync("/projects/" + Escape(project), new ProjectInput()) + .ConfigureAwait(false))!; + } + } + + private static string Escape(string project) + { + // Gerrit does a good validation in the server side, but we do some basic checks + ValidationException.CheckCondition( + !project.Contains(' '), "Invalid project name, has spaces: '%s'", project); + return project.Replace("/", "%2F"); + } + + public async Task GetAccessInfoAsync(string project) + { + using (ProfilerInstance.Start("gerrit_access")) + { + return (await Transport + .GetAsync("/projects/" + project + "/access") + .ConfigureAwait(false))!; + } + } + + public async Task SetReviewAsync( + string changeId, string revisionId, SetReviewInput setReviewInput) + { + using (ProfilerInstance.Start("gerrit_set_review")) + { + return (await Transport + .PostAsync( + "/changes/" + changeId + "/revisions/" + revisionId + "/review", setReviewInput) + .ConfigureAwait(false))!; + } + } + + public async Task DeleteReviewerAsync( + string changeId, long accountId, DeleteReviewerInput deleteReviewerInput) + { + using (ProfilerInstance.Start("gerrit_delete_reviewer_by_account_id")) + { + await Transport + .PostAsync( + "/changes/" + changeId + "/reviewers/" + accountId + "/delete", + deleteReviewerInput) + .ConfigureAwait(false); + } + } + + public async Task DeleteReviewerAsync( + string changeId, string email, DeleteReviewerInput deleteReviewerInput) + { + using (ProfilerInstance.Start("gerrit_delete_reviewer_by_email")) + { + await Transport + .PostAsync( + "/changes/" + changeId + "/reviewers/" + email + "/delete", deleteReviewerInput) + .ConfigureAwait(false); + } + } + + public async Task AddReviewerAsync(string changeId, ReviewerInput reviewerInput) + { + using (ProfilerInstance.Start("gerrit_add_reviewer")) + { + await Transport + .PostAsync("/changes/" + changeId + "/reviewers", reviewerInput) + .ConfigureAwait(false); + } + } + + public async Task GetSelfAccountAsync() + { + using (ProfilerInstance.Start("gerrit_get_self")) + { + return (await Transport + .GetAsync("/accounts/self") + .ConfigureAwait(false))!; + } + } + + public async Task> GetActionsAsync( + string changeId, string revision) + { + using (ProfilerInstance.Start("gerrit_get_actions")) + { + var result = await Transport + .GetAsync>( + "/changes/" + changeId + "/revisions/" + revision + "/actions") + .ConfigureAwait(false); + return (result ?? new Dictionary()).ToImmutableDictionary(); + } + } + + public async Task DeleteVoteAsync( + string changeId, string accountId, string labelId, DeleteVoteInput deleteVoteInput) + { + using (ProfilerInstance.Start("gerrit_delete_reviewer_vote")) + { + await Transport + .PostAsync( + "/changes/" + changeId + "/reviewers/" + accountId + "/votes/" + labelId + + "/delete", + deleteVoteInput) + .ConfigureAwait(false); + } + } + + public async Task SubmitChangeAsync(string changeId, SubmitInput submitInput) + { + using (ProfilerInstance.Start("gerrit_submit_change")) + { + return (await Transport + .PostAsync("/changes/" + changeId + "/submit", submitInput) + .ConfigureAwait(false))!; + } + } +} diff --git a/src/Copybara.Core/Git/GerritApi/GerritApiException.cs b/src/Copybara.Core/Git/GerritApi/GerritApiException.cs new file mode 100644 index 000000000..15231b553 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/GerritApiException.cs @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using Copybara.Exceptions; + +namespace Copybara.Git.GerritApi; + +/// Exception that maps to Gerrit Http error codes. +public class GerritApiException : RepoException +{ + public static readonly Regex ErrorPattern = new( + ".*
(.*)
.*", + RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Multiline); + + private static readonly ImmutableDictionary CodeMap = + Enum.GetValues() + .ToImmutableDictionary(rc => (int)rc); + + private readonly string _baseMessage; + private readonly ResponseCodeValue _responseCode; + private readonly string _gerritResponseMsg; + private readonly string _gerritRequestMsg; + private readonly int _exitCode; + + public GerritApiException( + int exitCode, string message, string gerritResponseMsg, string gerritRequest) + : base(message) + { + _baseMessage = message; + _exitCode = exitCode; + _responseCode = ParseResponseCode(exitCode); + _gerritResponseMsg = gerritResponseMsg; + _gerritRequestMsg = gerritRequest; + } + + public ResponseCodeValue GetResponseCode() => _responseCode; + + public int GetExitCode() => _exitCode; + + private static ResponseCodeValue ParseResponseCode(int code) => + CodeMap.TryGetValue(code, out var rc) ? rc : ResponseCodeValue.UNKNOWN; + + public override string Message => + string.Format( + "{0}: Received error with code {1} from Gerrit: {2}\n\nThe request was:\n\n{3}\n\n" + + "The full response was:\n\n{4}", + _baseMessage, + _exitCode, + ExtractError(), + _gerritRequestMsg, + _gerritResponseMsg); + + private string ExtractError() + { + var matcher = ErrorPattern.Match(_gerritResponseMsg); + if (matcher.Success) + { + return matcher.Groups[1].Value; + } + + return _gerritResponseMsg; + } + + public string GetGerritResponseMsg() => _gerritResponseMsg; + + /// + /// Gerrit known response codes. + /// + /// Note that UNKNOWN will be used for any other not in this list. + /// + /// NOTE(port): named ResponseCodeValue to avoid colliding with the + /// GetResponseCode() accessor within the same type; the Java enum is + /// GerritApiException.ResponseCode. + public enum ResponseCodeValue + { + UNKNOWN = 0, + BAD_REQUEST = 400, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + CONFLICT = 409, + PRECONDITION_FAILED = 412, + UNPROCESSABLE_ENTITY = 422, + } +} diff --git a/src/Copybara.Core/Git/GerritApi/GerritApiTransportImpl.cs b/src/Copybara.Core/Git/GerritApi/GerritApiTransportImpl.cs new file mode 100644 index 000000000..eabc97788 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/GerritApiTransportImpl.cs @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Json; + +namespace Copybara.Git.GerritApi; + +/// +/// Implementation of that uses direct http calls. Port of +/// com.google.copybara.git.gerritapi.GerritApiTransportImpl. +/// +/// +/// NOTE(port): the Java original uses google-http-client + gson. This port uses +/// and , and reuses +/// to strip Gerrit's )]}' XSSI no-execute prefix. Credentials +/// are obtained from the git credential helper via , and +/// applied as HTTP Basic auth, mirroring upstream. +/// +public class GerritApiTransportImpl : IGerritApiTransport +{ + private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(1); + + /// Serialization options mirroring gson: omit nulls, no indentation. + private static readonly JsonSerializerOptions RequestOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + private readonly GitRepository _repo; + private readonly Uri _uri; + private readonly HttpClient _httpClient; + + public GerritApiTransportImpl(GitRepository repo, Uri uri, HttpClient httpClient) + { + _repo = repo; + _uri = Preconditions.CheckNotNull(uri); + _httpClient = Preconditions.CheckNotNull(httpClient); + } + + public async Task GetAsync(string path) + { + var userPassword = GetCredentialsIfPresent(_uri.ToString()); + var url = GetUrl(path); + using var request = new HttpRequestMessage(HttpMethod.Get, url); + return await ExecuteAsync(request, userPassword, url).ConfigureAwait(false); + } + + public async Task PostAsync(string path, object request) + { + var userPassword = GetCredentials(_uri.ToString()); + var url = GetUrl(path); + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url) + { + Content = JsonContent(request), + }; + return await ExecuteAsync(httpRequest, userPassword, url).ConfigureAwait(false); + } + + public async Task PutAsync(string path, object request) + { + var userPassword = GetCredentials(_uri.ToString()); + var url = GetUrl(path); + using var httpRequest = new HttpRequestMessage(HttpMethod.Put, url) + { + Content = JsonContent(request), + }; + return await ExecuteAsync(httpRequest, userPassword, url).ConfigureAwait(false); + } + + public Uri GetUrl(string path) + { + Preconditions.CheckArgument(path.StartsWith('/'), path); + return new Uri(_uri, _uri.AbsolutePath + path); + } + + private static HttpContent JsonContent(object request) + { + string json = JsonSerializer.Serialize(request, request.GetType(), RequestOptions); + return new StringContent(json, Encoding.UTF8, "application/json"); + } + + private async Task ExecuteAsync( + HttpRequestMessage request, GitCredential.UserPassword? userPassword, Uri url) + { + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + if (userPassword != null) + { + var raw = $"{userPassword.GetUsername()}:{userPassword.GetPasswordBeCareful()}"; + var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(raw)); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", encoded); + } + + HttpResponseMessage response; + try + { + using var cts = new CancellationTokenSource(Timeout); + response = await _httpClient.SendAsync(request, cts.Token).ConfigureAwait(false); + } + catch (Exception e) when (e is HttpRequestException or OperationCanceledException) + { + throw new RepoException("Error running Gerrit API operation " + url, e); + } + + using (response) + { + if (!response.IsSuccessStatusCode) + { + string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new GerritApiException( + (int)response.StatusCode, "Error calling gerrit", content, url.ToString()); + } + + try + { + return await GsonParserUtil + .ParseHttpResponseAsync(response, stripNoExecutePrefix: true) + .ConfigureAwait(false); + } + catch (ArgumentException e) + { + throw new RepoException( + string.Format( + "Cannot parse response as type {0}.\nRequest: {1}\n", typeof(T), url), + e); + } + } + } + + /// + /// Credentials for API should be optional for any read operation (GET). + /// + private GitCredential.UserPassword? GetCredentialsIfPresent(string url) + { + try + { + return GetCredentials(url); + } + catch (ValidationException) + { + return null; + } + } + + /// Gets the credentials from the git credential helper. + /// + /// + private GitCredential.UserPassword GetCredentials(string url) + { + try + { + return _repo.CredentialFill(url); + } + catch (ValidationException e) + { + throw new ValidationException( + $"Cannot get credentials for host {url}, from credentials helper", e); + } + } +} diff --git a/src/Copybara.Core/Git/GerritApi/GerritApiTransportWithChecker.cs b/src/Copybara.Core/Git/GerritApi/GerritApiTransportWithChecker.cs new file mode 100644 index 000000000..fcc5cb0d3 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/GerritApiTransportWithChecker.cs @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Checks; +using Copybara.Common; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Git.GerritApi; + +/// +/// A wrapper that runs an over each request +/// before delegating. Port of +/// com.google.copybara.git.gerritapi.GerritApiTransportWithChecker. +/// +/// +/// NOTE(port): the Java original delegates to a com.google.copybara.checks.ApiChecker. Since +/// that helper is not (yet) ported, this class inlines its convenience logic (build a field map from +/// the arguments and call ). +/// The response type, which upstream passes as a reflected Type, is represented here by the +/// generic argument and surfaced to the checker as a string. +/// +public class GerritApiTransportWithChecker : IGerritApiTransport +{ + private readonly IGerritApiTransport _delegate; + private readonly IChecker _checker; + private readonly Console _console; + + public GerritApiTransportWithChecker( + IGerritApiTransport @delegate, IChecker checker, Console console) + { + _delegate = Preconditions.CheckNotNull(@delegate); + _checker = Preconditions.CheckNotNull(checker); + _console = Preconditions.CheckNotNull(console); + } + + public Task GetAsync(string path) + { + Check( + ("path", path), + ("response_type", typeof(T).ToString())); + return _delegate.GetAsync(path); + } + + public Task PostAsync(string path, object request) + { + Check( + ("path", path), + ("request", request.ToString() ?? string.Empty), + ("response_type", typeof(T).ToString())); + return _delegate.PostAsync(path, request); + } + + public Task PutAsync(string path, object request) + { + Check( + ("path", path), + ("request", request.ToString() ?? string.Empty), + ("response_type", typeof(T).ToString())); + return _delegate.PutAsync(path, request); + } + + private void Check(params (string Field, string Value)[] fields) + { + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var (field, value) in fields) + { + builder[field] = value; + } + + _checker.DoCheck(builder.ToImmutable(), _console); + } +} diff --git a/src/Copybara.Core/Git/GerritApi/GerritApiUtil.cs b/src/Copybara.Core/Git/GerritApi/GerritApiUtil.cs new file mode 100644 index 000000000..d27c6d987 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/GerritApiUtil.cs @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Globalization; + +namespace Copybara.Git.GerritApi; + +/// Utilities for dealing with Gerrit API. +public static class GerritApiUtil +{ + /// + /// Parses dates like "2014-12-21 17:30:08.000000000". + /// + /// + /// NOTE(port): Java uses DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.n") where + /// n is nano-of-second (up to 9 digits) and the zone is fixed to UTC. .NET's custom format + /// uses fffffffff for 9 fractional-second digits; parsing yields a UTC + /// . + /// + private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fffffffff"; + + /// Parses a Gerrit timestamp into a UTC . + public static DateTimeOffset ParseTimestamp(string date) + { + // AssumeUniversal + AdjustToUniversal mirrors Java's .withZone(ZoneOffset.UTC). + var dt = DateTime.ParseExact( + date, + TimestampFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + return new DateTimeOffset(dt, TimeSpan.Zero); + } +} diff --git a/src/Copybara.Core/Git/GerritApi/GerritEventType.cs b/src/Copybara.Core/Git/GerritApi/GerritEventType.cs new file mode 100644 index 000000000..98599d262 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/GerritEventType.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Git.GerritApi; + +/// Type of events that we can monitor from Gerrit. +public enum GerritEventType +{ + /// Used if we don't know the event type. + UNKNOWN, + + LABELS, + SUBMIT_REQUIREMENTS, + STATUS, +} diff --git a/src/Copybara.Core/Git/GerritApi/GetChangeInput.cs b/src/Copybara.Core/Git/GerritApi/GetChangeInput.cs new file mode 100644 index 000000000..ae61ed803 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/GetChangeInput.cs @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; + +namespace Copybara.Git.GerritApi; + +/// +/// An object that represents the input parameters for get change: +/// +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change +/// +public class GetChangeInput : IEquatable +{ + private readonly IReadOnlySet _include; + + public GetChangeInput() + : this(ImmutableHashSet.Empty) + { + } + + public GetChangeInput(IReadOnlySet include) + { + _include = include; + } + + public string AsUrlParams() => string.Join("&", _include.Select(i => "o=" + i)); + + public bool Equals(GetChangeInput? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other is null) + { + return false; + } + + return _include.SetEquals(other._include); + } + + public override bool Equals(object? o) => o is GetChangeInput other && Equals(other); + + public override int GetHashCode() + { + var hash = default(HashCode); + foreach (var i in _include.OrderBy(x => x)) + { + hash.Add(i); + } + + return hash.ToHashCode(); + } +} diff --git a/src/Copybara.Core/Git/GerritApi/GitPersonInfo.cs b/src/Copybara.Core/Git/GerritApi/GitPersonInfo.cs new file mode 100644 index 000000000..bd11fedcc --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/GitPersonInfo.cs @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#git-person-info +[StarlarkBuiltin("gerritapi.GitPersonInfo", Doc = "Git person information.")] +public class GitPersonInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } + + [JsonPropertyName("date")] + public string? Date { get; set; } + + [JsonPropertyName("tz")] + public int Tz { get; set; } + + [StarlarkMethod( + "name", + Doc = "The name of the author/committer.", + StructField = true, + AllowReturnNones = true)] + public string? GetName() => Name; + + [StarlarkMethod( + "email", + Doc = "The email address of the author/committer.", + StructField = true, + AllowReturnNones = true)] + public string? GetEmail() => Email; + + public DateTimeOffset GetDate() => + GerritApiUtil.ParseTimestamp(Date!).ToOffset(TimeSpan.FromMinutes(Tz)); + + [StarlarkMethod( + "date", + Doc = "The timestamp of when this identity was constructed.", + StructField = true, + AllowReturnNones = true)] + public string? GetDateForSkylark() => Date; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"GitPersonInfo{{name={Name}, email={Email}, date={Date}, tz={Tz}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/IGerritApiTransport.cs b/src/Copybara.Core/Git/GerritApi/IGerritApiTransport.cs new file mode 100644 index 000000000..f56cdfd48 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/IGerritApiTransport.cs @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Exceptions; + +namespace Copybara.Git.GerritApi; + +/// +/// Http transport interface for talking to a Gerrit host. Port of +/// com.google.copybara.git.gerritapi.GerritApiTransport. +/// +/// +/// NOTE(port): Java passes a java.lang.reflect.Type responseType to each method. In the .NET +/// port the response type is expressed as a generic type argument T, which is both more +/// idiomatic and works directly with . Calls are async because the +/// underlying HTTP work is I/O bound. +/// +public interface IGerritApiTransport +{ + /// Do a http GET call. + /// + /// + Task GetAsync(string path); + + /// Do a http POST call. + /// + /// + Task PostAsync(string path, object request); + + /// Do a http PUT call. + /// + /// + Task PutAsync(string path, object request); +} diff --git a/src/Copybara.Core/Git/GerritApi/IncludeResult.cs b/src/Copybara.Core/Git/GerritApi/IncludeResult.cs new file mode 100644 index 000000000..932ee7d06 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/IncludeResult.cs @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Git.GerritApi; + +/// Fields to include in the response . +public enum IncludeResult +{ + LABELS, + DETAILED_LABELS, + CURRENT_REVISION, + ALL_REVISIONS, + DOWNLOAD_COMMANDS, + CURRENT_COMMIT, + ALL_COMMITS, + CURRENT_FILES, + ALL_FILES, + DETAILED_ACCOUNTS, + REVIEWER_UPDATES, + MESSAGES, + CURRENT_ACTIONS, + CHANGE_ACTIONS, + REVIEWED, + SUBMITTABLE, + WEB_LINKS, + CHECK, + COMMIT_FOOTERS, + PUSH_CERTIFICATES, + SUBMIT_REQUIREMENTS, +} diff --git a/src/Copybara.Core/Git/GerritApi/LabelInfo.cs b/src/Copybara.Core/Git/GerritApi/LabelInfo.cs new file mode 100644 index 000000000..cd7f476ed --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/LabelInfo.cs @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#label-info +[StarlarkBuiltin("gerritapi.LabelInfo", Doc = "Gerrit label information.")] +public class LabelInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("optional")] + public bool Optional { get; set; } + + [JsonPropertyName("approved")] + public AccountInfo? Approved { get; set; } + + [JsonPropertyName("rejected")] + public AccountInfo? Rejected { get; set; } + + [JsonPropertyName("recommended")] + public AccountInfo? Recommended { get; set; } + + [JsonPropertyName("disliked")] + public AccountInfo? Disliked { get; set; } + + [JsonPropertyName("blocking")] + public bool Blocking { get; set; } + + [JsonPropertyName("value")] + public int Value { get; set; } + + [JsonPropertyName("default_value")] + public int DefaultValue { get; set; } + + [JsonPropertyName("values")] + public IReadOnlyDictionary? Values { get; set; } + + [JsonPropertyName("all")] + public IReadOnlyList? All { get; set; } + + public bool IsOptional() => Optional; + + [StarlarkMethod( + "approved", + Doc = + "One user who approved this label on the change (voted the maximum value) as an " + + "AccountInfo entity.", + StructField = true, + AllowReturnNones = true)] + public AccountInfo? GetApproved() => Approved; + + [StarlarkMethod( + "rejected", + Doc = + "One user who rejected this label on the change (voted the minimum value) as an " + + "AccountInfo entity.", + StructField = true, + AllowReturnNones = true)] + public AccountInfo? GetRejected() => Rejected; + + [StarlarkMethod( + "recommended", + Doc = + "One user who recommended this label on the change (voted positively, but not the " + + "maximum value) as an AccountInfo entity.", + StructField = true, + AllowReturnNones = true)] + public AccountInfo? GetRecommended() => Recommended; + + [StarlarkMethod( + "disliked", + Doc = + "One user who disliked this label on the change (voted negatively, but not the " + + "minimum value) as an AccountInfo entity.", + StructField = true, + AllowReturnNones = true)] + public AccountInfo? GetDisliked() => Disliked; + + [StarlarkMethod( + "blocking", + Doc = "If true, the label blocks submit operation. If not set, the default is false.", + StructField = true)] + public bool IsBlocking() => Blocking; + + [StarlarkMethod( + "value", + Doc = + "The voting value of the user who recommended/disliked this label on the change if " + + "it is not `\"+1\"`/`\"-1\"`.", + StructField = true)] + public int GetValue() => Value; + + [StarlarkMethod( + "default_value", + Doc = + "The default voting value for the label. This value may be outside the range " + + "specified in permitted_labels.", + StructField = true)] + public int GetDefaultValue() => DefaultValue; + + [StarlarkMethod( + "values", + Doc = + "A map of all values that are allowed for this label. The map maps the values " + + "(`\"-2\"`, `\"-1\"`, `\"0\"`, `\"+1\"`, `\"+2\"`) to the value descriptions.", + StructField = true)] + public IReadOnlyDictionary GetValues() => + Values is null ? ImmutableDictionary.Empty : Values.ToImmutableDictionary(); + + [StarlarkMethod( + "all", + Doc = + "List of all approvals for this label as a list of ApprovalInfo entities. Items " + + "in this list may not represent actual votes cast by users; if a user votes on " + + "any label, a corresponding ApprovalInfo will appear in this list for all labels.", + StructField = true)] + public IReadOnlyList GetAll() => + All is not null ? All.ToImmutableArray() : ImmutableArray.Empty; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"LabelInfo{{optional={Optional}, approved={Approved}, rejected={Rejected}, " + + $"recommended={Recommended}, disliked={Disliked}, blocking={Blocking}, value={Value}, " + + $"defaultValue={DefaultValue}, values={Values}, all={All}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/NotifyType.cs b/src/Copybara.Core/Git/GerritApi/NotifyType.cs new file mode 100644 index 000000000..8d155c7a2 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/NotifyType.cs @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Git.GerritApi; + +/// Type of notification to send when abandoning/deleting a review or reviewer. +/// +/// NOTE(port): In the Java original, ALL is annotated with @NullValue, meaning it is +/// serialized as null (i.e. omitted). Callers mirror upstream by converting the enum to its +/// wire representation via before assigning it to a string field. +/// +public enum NotifyType +{ + NONE, + OWNER, + OWNER_REVIEWERS, + ALL, +} + +/// Helpers for translating to/from Gerrit wire values. +public static class NotifyTypeExtensions +{ + /// + /// Returns the wire representation of the notify type, or null for + /// which upstream annotates with @NullValue (i.e. omitted from the request). + /// + public static string? ToWireValue(this NotifyType notify) => + notify == NotifyType.ALL ? null : notify.ToString(); +} diff --git a/src/Copybara.Core/Git/GerritApi/ParentCommitInfo.cs b/src/Copybara.Core/Git/GerritApi/ParentCommitInfo.cs new file mode 100644 index 000000000..67883748c --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ParentCommitInfo.cs @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// Restricted version of for describing parents. +[StarlarkBuiltin("gerritapi.ParentCommitInfo", Doc = "Gerrit parent commit information.")] +public class ParentCommitInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("commit")] + public string? Commit { get; set; } + + [JsonPropertyName("subject")] + public string? Subject { get; set; } + + [StarlarkMethod( + "commit", + Doc = + "The commit ID. Not set if included in a RevisionInfo entity that is contained " + + "in a map which has the commit ID as key.", + StructField = true, + AllowReturnNones = true)] + public string? GetCommit() => Commit; + + [StarlarkMethod( + "subject", + Doc = "The subject of the commit (header line of the commit message).", + StructField = true, + AllowReturnNones = true)] + public string? GetSubject() => Subject; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"ParentCommitInfo{{commit={Commit}, subject={Subject}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/ProjectAccessInfo.cs b/src/Copybara.Core/Git/GerritApi/ProjectAccessInfo.cs new file mode 100644 index 000000000..dadcd2ceb --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ProjectAccessInfo.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// +/// Relevant field(s) of the ProjectAccessInfo message. +/// https://gerrit-review.googlesource.com/Documentation/rest-api-access.html#project-access-info +/// +public class ProjectAccessInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("is_owner")] + public bool IsOwner { get; set; } + + public bool GetIsOwner() => IsOwner; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => $"ProjectAccessInfo{{is_owner={IsOwner}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/ProjectInfo.cs b/src/Copybara.Core/Git/GerritApi/ProjectInfo.cs new file mode 100644 index 000000000..2e4ffde08 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ProjectInfo.cs @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-info +public class ProjectInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("parent")] + public string? Parent { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("state")] + public string? StateString { get; set; } + + [JsonPropertyName("branches")] + public IReadOnlyDictionary? Branches { get; set; } + + public enum State + { + ACTIVE, + READ_ONLY, + HIDDEN, + } + + public string? GetId() => Id; + + public string? GetName() => Name; + + public string? GetParent() => Parent; + + public string? GetDescription() => Description; + + public State? GetState() => StateString is null ? null : Enum.Parse(StateString); + + public IReadOnlyDictionary? GetBranches() => Branches; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"ProjectInfo{{id={Id}, name={Name}, parent={Parent}, description={Description}, " + + $"state={StateString}, branches={Branches}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/ProjectInput.cs b/src/Copybara.Core/Git/GerritApi/ProjectInput.cs new file mode 100644 index 000000000..512889197 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ProjectInput.cs @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Git.GerritApi; + +/// +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-input +/// +public class ProjectInput +{ +} diff --git a/src/Copybara.Core/Git/GerritApi/RestoreInput.cs b/src/Copybara.Core/Git/GerritApi/RestoreInput.cs new file mode 100644 index 000000000..100cd9248 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/RestoreInput.cs @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; + +namespace Copybara.Git.GerritApi; + +/// +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#restore-input +/// +public class RestoreInput +{ + [JsonPropertyName("message")] + public string? Message { get; set; } + + private RestoreInput(string? message) + { + Message = message; + } + + public static RestoreInput Create(string? message) => new(message); + + public static RestoreInput CreateWithoutComment() => new(null); +} diff --git a/src/Copybara.Core/Git/GerritApi/ReviewResult.cs b/src/Copybara.Core/Git/GerritApi/ReviewResult.cs new file mode 100644 index 000000000..5d5fbdc52 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ReviewResult.cs @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-result +[StarlarkBuiltin("gerritapi.ReviewResult", Doc = "Gerrit review result.")] +public class ReviewResult : IStarlarkPrintableValue +{ + [JsonPropertyName("labels")] + public IReadOnlyDictionary? Labels { get; set; } + + [JsonPropertyName("ready")] + public bool Ready { get; set; } + + public ReviewResult(IReadOnlyDictionary? labels, bool ready) + { + Labels = labels; + Ready = ready; + } + + public ReviewResult() + { + } + + [StarlarkMethod( + "labels", + Doc = "Map of labels to values after the review was posted.", + StructField = true)] + public IReadOnlyDictionary GetLabelsForStarlark() + { + var m = ImmutableDictionary.CreateBuilder(); + foreach (var e in GetLabels()) + { + m[e.Key] = StarlarkInt.Of(e.Value); + } + + return m.ToImmutable(); + } + + public IReadOnlyDictionary GetLabels() => + Labels is null ? ImmutableDictionary.Empty : Labels.ToImmutableDictionary(); + + [StarlarkMethod( + "ready", + Doc = + "If true, the change was moved from WIP to ready for review as a result of this action." + + " Not set if false.", + StructField = true)] + public bool IsReady() => Ready; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => $"ReviewResult{{labels={Labels}, ready={Ready}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/ReviewerInput.cs b/src/Copybara.Core/Git/GerritApi/ReviewerInput.cs new file mode 100644 index 000000000..8d958d302 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/ReviewerInput.cs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Copybara.Common; + +namespace Copybara.Git.GerritApi; + +/// +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#reviewer-input +/// request json. +/// +public class ReviewerInput +{ + [JsonPropertyName("reviewer")] + public string Reviewer { get; set; } + + public ReviewerInput(string reviewer) + { + Reviewer = Preconditions.CheckNotNull(reviewer); + } +} diff --git a/src/Copybara.Core/Git/GerritApi/RevisionInfo.cs b/src/Copybara.Core/Git/GerritApi/RevisionInfo.cs new file mode 100644 index 000000000..97fe7efda --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/RevisionInfo.cs @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-info +[StarlarkBuiltin("gerritapi.RevisionInfo", Doc = "Gerrit revision information.")] +public class RevisionInfo : IStarlarkPrintableValue +{ + [JsonPropertyName("kind")] + public string? KindString { get; set; } + + [JsonPropertyName("_number")] + public int PatchsetNumber { get; set; } + + [JsonPropertyName("created")] + public string? Created { get; set; } + + [JsonPropertyName("uploader")] + public AccountInfo? Uploader { get; set; } + + [JsonPropertyName("ref")] + public string? Ref { get; set; } + + [JsonPropertyName("fetch")] + public IReadOnlyDictionary? Fetch { get; set; } + + [JsonPropertyName("commit")] + public CommitInfo? Commit { get; set; } + + public RevisionKind GetKind() => Enum.Parse(KindString!); + + [StarlarkMethod( + "kind", + Doc = + "The change kind. Valid values are REWORK, TRIVIAL_REBASE, MERGE_FIRST_PARENT_UPDATE, " + + "NO_CODE_CHANGE, and NO_CHANGE.", + StructField = true, + AllowReturnNones = true)] + public string? GetKindAsString() => KindString; + + [StarlarkMethod( + "patchset_number", + Doc = "The patch set number, or edit if the patch set is an edit.", + StructField = true)] + public int GetPatchsetNumber() => PatchsetNumber; + + [StarlarkMethod( + "created", + Doc = "The timestamp of when the patch set was created.", + StructField = true, + AllowReturnNones = true)] + public string? GetCreated() => Created; + + [StarlarkMethod( + "uploader", + Doc = "The uploader of the patch set as an AccountInfo entity.", + StructField = true, + AllowReturnNones = true)] + public AccountInfo? GetUploader() => Uploader; + + [StarlarkMethod( + "ref", + Doc = "The Git reference for the patch set.", + StructField = true, + AllowReturnNones = true)] + public string? GetRef() => Ref; + + public IReadOnlyDictionary GetFetch() => + Fetch is null ? ImmutableDictionary.Empty : Fetch.ToImmutableDictionary(); + + [StarlarkMethod( + "commit", + Doc = "The commit of the patch set as CommitInfo entity.", + StructField = true, + AllowReturnNones = true)] + public CommitInfo? GetCommit() => Commit; + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); + + public override string ToString() => + $"RevisionInfo{{kind={KindString}, patchsetNumber={PatchsetNumber}, created={Created}, " + + $"uploader={Uploader}, ref={Ref}, fetch={Fetch}, commit={Commit}}}"; +} + +/// +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-info +/// +/// NOTE(port): Java nests this as RevisionInfo.Kind; here it is a sibling type named +/// RevisionKind to avoid a naming collision within the same namespace. +public enum RevisionKind +{ + REWORK, + TRIVIAL_REBASE, + MERGE_FIRST_PARENT_UPDATE, + NO_CODE_CHANGE, + NO_CHANGE, +} diff --git a/src/Copybara.Core/Git/GerritApi/SetReviewInput.cs b/src/Copybara.Core/Git/GerritApi/SetReviewInput.cs new file mode 100644 index 000000000..9c715dab5 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/SetReviewInput.cs @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-input. +/// +[StarlarkBuiltin( + "SetReviewInput", + Doc = + "Input for posting a review to Gerrit. See " + + "https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-input")] +public class SetReviewInput : IStarlarkPrintableValue, IEquatable +{ + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("labels")] + public IReadOnlyDictionary Labels { get; set; } + + [JsonPropertyName("tag")] + public string? Tag { get; set; } + + /// The notify type, serialized to its Gerrit wire value (null for ALL). + [JsonPropertyName("notify")] + public string? NotifyWire => NotifyType?.ToWireValue(); + + [JsonIgnore] + public NotifyType? NotifyType { get; set; } + + public SetReviewInput() + { + Labels = new Dictionary(); + } + + private SetReviewInput( + string? message, IReadOnlyDictionary labels, string? tag, NotifyType notify) + { + Message = message; + Labels = labels; + Tag = tag; + NotifyType = notify; + } + + public SetReviewInput(string? message, IReadOnlyDictionary labels) + : this(message, labels, null, global::Copybara.Git.GerritApi.NotifyType.ALL) + { + } + + public static SetReviewInput Create( + string? message, IReadOnlyDictionary labels, string? tag) => + Create(message, labels, tag, global::Copybara.Git.GerritApi.NotifyType.ALL); + + public static SetReviewInput Create( + string? message, IReadOnlyDictionary labels, string? tag, NotifyType notify) => + new(message, labels, tag, notify); + + public string? GetMessage() => Message; + + public IReadOnlyDictionary GetLabels() => Labels; + + public NotifyType? GetNotify() => NotifyType; + + public string? GetTag() => Tag; + + public override string ToString() => + $"SetReviewInput{{message={Message}, labels={Labels}, tag={Tag}}}"; + + public bool Equals(SetReviewInput? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other is null) + { + return false; + } + + return Message == other.Message + && LabelsEqual(Labels, other.Labels) + && Tag == other.Tag; + } + + public override bool Equals(object? o) => o is SetReviewInput other && Equals(other); + + public override int GetHashCode() + { + var hash = default(HashCode); + hash.Add(Message); + foreach (var e in Labels) + { + hash.Add(e.Key); + hash.Add(e.Value); + } + + return hash.ToHashCode(); + } + + private static bool LabelsEqual(IReadOnlyDictionary a, IReadOnlyDictionary b) + { + if (a.Count != b.Count) + { + return false; + } + + foreach (var e in a) + { + if (!b.TryGetValue(e.Key, out var v) || v != e.Value) + { + return false; + } + } + + return true; + } + + public void Repr(Printer printer, StarlarkSemantics semantics) => printer.Append(ToString()); +} diff --git a/src/Copybara.Core/Git/GerritApi/SubmitInput.cs b/src/Copybara.Core/Git/GerritApi/SubmitInput.cs new file mode 100644 index 000000000..3a00b0c9e --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/SubmitInput.cs @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; + +namespace Copybara.Git.GerritApi; + +/// +/// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-change +/// request json. +/// NotifyInfo (notify_details) not included for now. +/// on_behalf_of not included for now until we have a use case. +/// +public class SubmitInput +{ + [JsonPropertyName("notify")] + public string? Notify { get; set; } + + public SubmitInput(NotifyType? notify) + { + Notify = notify?.ToWireValue(); + } +} diff --git a/src/Copybara.Core/Git/GerritApi/SubmitRequirementExpressionInfo.cs b/src/Copybara.Core/Git/GerritApi/SubmitRequirementExpressionInfo.cs new file mode 100644 index 000000000..568a0bc41 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/SubmitRequirementExpressionInfo.cs @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// +/// Result of evaluating a single submit requirement expression. This API entity is populated from +/// Gerrit's SubmitRequirementExpressionResult. +/// +[StarlarkBuiltin( + "gerritapi.SubmitRequirementExpressionInfo", + Doc = "Result of evaluating submit requirement expression")] +public class SubmitRequirementExpressionInfo : IStarlarkValue +{ + [JsonPropertyName("expression")] + public string? Expression { get; set; } + + [JsonPropertyName("status")] + public string? StatusString { get; set; } + + [JsonPropertyName("fulfilled")] + public bool Fulfilled { get; set; } + + [StarlarkMethod( + "expression", + Doc = "The submit requirement expression as a string.", + StructField = true, + AllowReturnNones = true)] + public string? GetExpression() => Expression; + + public SubmitRequirementExpressionStatus GetStatus() => + Enum.Parse(StatusString!); + + [StarlarkMethod( + "status", + Doc = "The status of the submit requirement evaluation.", + StructField = true, + AllowReturnNones = true)] + public string? GetStatusAsString() => StatusString; + + [StarlarkMethod( + "fulfilled", + Doc = + "If true, this submit requirement result was created from a legacy SubmitRecord." + + " Otherwise, it was created by evaluating a submit requirement.", + StructField = true)] + public bool GetFulfilled() => Fulfilled; + + public override string ToString() => + $"SubmitRequirementExpressionInfo{{expression={Expression}, status={StatusString}, " + + $"fulfilled={Fulfilled}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/SubmitRequirementExpressionStatus.cs b/src/Copybara.Core/Git/GerritApi/SubmitRequirementExpressionStatus.cs new file mode 100644 index 000000000..8e7893d32 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/SubmitRequirementExpressionStatus.cs @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Git.GerritApi; + +/// Status describing the result of evaluating the submit requirement expression. +public enum SubmitRequirementExpressionStatus +{ + PASS, + FAIL, + ERROR, + NOT_EVALUATED, +} diff --git a/src/Copybara.Core/Git/GerritApi/SubmitRequirementInput.cs b/src/Copybara.Core/Git/GerritApi/SubmitRequirementInput.cs new file mode 100644 index 000000000..2aa281494 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/SubmitRequirementInput.cs @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// +/// An object that represents the input parameters for a submit requirement: +/// +/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-requirement-input +/// +public class SubmitRequirementInput : IStarlarkValue +{ + /// Submit requirement name. + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Query expression that can be evaluated on any change. If evaluated to true on a change, the + /// submit requirement is fulfilled and not blocking change submission. + /// + [JsonPropertyName("submittability_expression")] + public string SubmittabilityExpression { get; set; } + + public SubmitRequirementInput(string name, string submittabilityExpression) + { + Name = name; + SubmittabilityExpression = submittabilityExpression; + } + + [StarlarkMethod("name", Doc = "The submit requirement name.", StructField = true)] + public string GetName() => Name; + + [StarlarkMethod( + "submittability_expression", + Doc = + "Query expression that can be evaluated on any change. If evaluated to true on a change," + + " the submit requirement is fulfilled and not blocking change submission.", + StructField = true)] + public string GetSubmittabilityExpression() => SubmittabilityExpression; +} diff --git a/src/Copybara.Core/Git/GerritApi/SubmitRequirementResultInfo.cs b/src/Copybara.Core/Git/GerritApi/SubmitRequirementResultInfo.cs new file mode 100644 index 000000000..76e4cd6a5 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/SubmitRequirementResultInfo.cs @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GerritApi; + +/// Result of evaluating a submit requirement on a change. +public class SubmitRequirementResultInfo : IStarlarkValue +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("status")] + public string? StatusString { get; set; } + + [JsonPropertyName("submittability_expression_result")] + public SubmitRequirementExpressionInfo? SubmittabilityExpressionResult { get; set; } + + [JsonPropertyName("is_legacy")] + public bool IsLegacy { get; set; } + + [StarlarkMethod("name", Doc = "The submit requirement name.", StructField = true)] + public string? GetName() => Name; + + public SubmitRequirementResultStatus GetStatus() => + Enum.Parse(StatusString!); + + [StarlarkMethod( + "status", + Doc = "The status of the submit requirement evaluation.", + StructField = true)] + public string? GetStatusAsString() => StatusString; + + [StarlarkMethod( + "is_legacy", + Doc = + "If true, this submit requirement result was created from a legacy SubmitRecord." + + " Otherwise, it was created by evaluating a submit requirement.", + StructField = true)] + public bool GetIsLegacy() => IsLegacy; + + [StarlarkMethod( + "submittability_expression_result", + Doc = + "A SubmitRequirementExpressionInfo containing the result of evaluating the" + + " submittabilityexpression. If the submit requirement does not apply, the status" + + " field of the result will be set to NOT_EVALUATED.", + StructField = true)] + public SubmitRequirementExpressionInfo? GetSubmittabilityExpressionResult() => + SubmittabilityExpressionResult; + + public override string ToString() => + $"SubmitRequirementResultInfo{{name={Name}, status={StatusString}, " + + $"submittabilityExpressionResult={SubmittabilityExpressionResult}, isLegacy={IsLegacy}}}"; +} diff --git a/src/Copybara.Core/Git/GerritApi/SubmitRequirementResultStatus.cs b/src/Copybara.Core/Git/GerritApi/SubmitRequirementResultStatus.cs new file mode 100644 index 000000000..71598eec4 --- /dev/null +++ b/src/Copybara.Core/Git/GerritApi/SubmitRequirementResultStatus.cs @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Git.GerritApi; + +/// Status describing the result of evaluating the submit requirement. +public enum SubmitRequirementResultStatus +{ + SATISFIED, + UNSATISFIED, + OVERRIDDEN, + NOT_APPLICABLE, + ERROR, + FORCED, +} diff --git a/src/Copybara.Core/Git/GitDestination.cs b/src/Copybara.Core/Git/GitDestination.cs new file mode 100644 index 000000000..fa0020c25 --- /dev/null +++ b/src/Copybara.Core/Git/GitDestination.cs @@ -0,0 +1,995 @@ +/* + * Copyright (C) 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using Copybara.Checks; +using Copybara.Common; +using Copybara.Effect; +using Copybara.Exceptions; +using Copybara.Revision; +using Copybara.Util; +using Starlark.Annot; +using Starlark.Eval; +using Console = Copybara.Util.Console.Console; + +namespace Copybara.Git; + +/// A Git repository destination. Port of com.google.copybara.git.GitDestination. +public class GitDestination : IDestination +{ + private const string OriginLabelSeparator = ": "; + public const int SmallNumFilesCheckerThreshold = 100; + + // Mirrors GitModule.PRIMARY_BRANCHES. + private static readonly ImmutableHashSet PrimaryBranches = + ImmutableHashSet.Create("master", "main"); + + /// Holder for the labels that should be added to the destination change message. + public sealed class MessageInfo + { + public IReadOnlyList LabelsToAdd { get; } + + public MessageInfo(IReadOnlyList labelsToAdd) + { + LabelsToAdd = Preconditions.CheckNotNull(labelsToAdd); + } + } + + private readonly string _repoUrl; + private readonly string _fetch; + protected readonly string PushRef; + private readonly bool _partialFetch; + internal readonly bool PrimaryBranchMigrationMode; + + private readonly string? _tagName; + private readonly string? _tagMsg; + private readonly GitDestinationOptions _destinationOptions; + private readonly GitOptions _gitOptions; + private readonly GeneralOptions _generalOptions; + + private string? _resolvedPrimary; + private readonly IEnumerable _integrates; + private readonly IWriteHook _writerHook; + private readonly IChecker? _checker; + private readonly LazyResourceLoader _localRepo; + private readonly CredentialFileHandler? _credentials; + + internal GitDestination( + string repoUrl, + string fetch, + string push, + bool partialFetch, + bool primaryBranchMigrationMode, + string? tagName, + string? tagMsg, + GitDestinationOptions destinationOptions, + GitOptions gitOptions, + GeneralOptions generalOptions, + IWriteHook writerHook, + IEnumerable integrates, + IChecker? checker, + CredentialFileHandler? credentials) + { + _repoUrl = Preconditions.CheckNotNull(repoUrl); + _fetch = Preconditions.CheckNotNull(fetch); + PushRef = Preconditions.CheckNotNull(push); + _partialFetch = partialFetch; + PrimaryBranchMigrationMode = primaryBranchMigrationMode; + _tagName = tagName; + _tagMsg = tagMsg; + _destinationOptions = Preconditions.CheckNotNull(destinationOptions); + _gitOptions = Preconditions.CheckNotNull(gitOptions); + _generalOptions = Preconditions.CheckNotNull(generalOptions); + _integrates = Preconditions.CheckNotNull(integrates); + _writerHook = Preconditions.CheckNotNull(writerHook); + _checker = checker; + _localRepo = LazyResourceLoader.Memoized( + _ => destinationOptions.LocalGitRepo(repoUrl, credentials)); + _credentials = credentials; + } + + /// + /// Throws an exception if the user.email or user.name Git configuration settings are not set. + /// + private static void VerifyUserInfoConfigured(GitRepository repo) + { + string output = repo.SimpleCommand("config", "-l").GetStdout(); + bool nameConfigured = false; + bool emailConfigured = false; + foreach (var line in output.Split('\n')) + { + if (line.StartsWith("user.name=", StringComparison.Ordinal)) + { + nameConfigured = true; + } + else if (line.StartsWith("user.email=", StringComparison.Ordinal)) + { + emailConfigured = true; + } + } + ValidationException.CheckCondition( + nameConfigured && emailConfigured, + "'user.name' and/or 'user.email' are not configured. Please run `git config --global" + + " SETTING VALUE` to set them"); + } + + public IDestination.IWriter NewWriter(WriterContext writerContext) + { + var state = new WriterState( + _localRepo, _destinationOptions.GetLocalBranch(GetPush(), writerContext.IsDryRun())); + + return new WriterImpl( + writerContext.IsDryRun(), + _repoUrl, + GetFetch(), + GetPush(), + _partialFetch, + _tagName, + _tagMsg, + _generalOptions, + _gitOptions, + _writerHook, + state, + _destinationOptions.NonFastForwardPush, + _integrates, + _destinationOptions.LastRevFirstParent, + _destinationOptions.IgnoreIntegrationErrors, + _destinationOptions.LocalRepoPath, + _destinationOptions.CommitterName, + _destinationOptions.CommitterEmail, + _destinationOptions.RebaseWhenBaseline(), + _gitOptions.VisitChangePageSize, + _gitOptions.GitTagOverwrite, + _checker, + _destinationOptions, + _credentials); + } + + /// State to be maintained between writer instances. + public class WriterState + { + internal bool AlreadyFetched; + internal bool FirstWrite = true; + internal readonly LazyResourceLoader LocalRepo; + internal readonly string LocalBranch; + + internal WriterState(LazyResourceLoader localRepo, string localBranch) + { + LocalRepo = localRepo; + LocalBranch = localBranch; + } + } + + /// A write hook allows customizing the behavior of the git.destination writer. + public interface IWriteHook + { + /// Customize the writer for a particular destination. + MessageInfo GenerateMessageInfo(TransformResult transformResult); + + /// Validate or modify the current change to be pushed. + void BeforePush( + GitRepository repo, + MessageInfo messageInfo, + bool skipPush, + IReadOnlyList integrateLabels, + IReadOnlyList originChanges) + { + } + + /// Construct the reference to push based on the pushToRefsFor reference. + string GetPushReference( + GitRepository primaryBranch, string pushToRefsFor, TransformResult transformResult); + + /// Process the server response from the push command and compute the effects. + IReadOnlyList AfterPush( + string serverResponse, + MessageInfo messageInfo, + GitRevision pushedRevision, + IReadOnlyList originChanges); + + IEndpoint GetFeedbackEndPoint(Console console) => IEndpoint.NoopEndpoint; + + ImmutableListMultimap Describe() => + ImmutableListMultimap.Empty; + } + + /// A write hook for standard git repositories. + public class DefaultWriteHook : IWriteHook + { + public MessageInfo GenerateMessageInfo(TransformResult transformResult) + { + IRevision rev = transformResult.GetCurrentRevision(); + return new MessageInfo( + transformResult.IsSetRevId() + ? new List + { + new(transformResult.GetRevIdLabel() + OriginLabelSeparator + rev.AsString()), + } + : new List()); + } + + public IReadOnlyList AfterPush( + string serverResponse, + MessageInfo messageInfo, + GitRevision pushedRevision, + IReadOnlyList originChanges) => + ImmutableArray.Create( + new DestinationEffect( + DestinationEffect.EffectType.CREATED, + $"Created revision {pushedRevision.GetHash()}", + originChanges.Cast().ToList(), + new DestinationEffect.DestinationRef( + pushedRevision.GetHash(), "commit", url: null))); + + public string GetPushReference( + GitRepository repo, string pushToRefsFor, TransformResult transformResult) => + pushToRefsFor; + } + + /// + /// A writer for git.*destination destinations. Not a public interface; don't use directly. + /// + public class WriterImpl : IDestination.IWriter + where TS : WriterState + { + internal readonly bool SkipPush; + private readonly string _repoUrl; + private readonly string _remoteFetch; + private readonly string _remotePush; + private readonly string? _tagNameTemplate; + private readonly string? _tagMsgTemplate; + private readonly bool _force; + private readonly bool _partialFetch; + private readonly Console _baseConsole; + private readonly GeneralOptions _generalOptions; + private readonly GitOptions _gitOptions; + private readonly IWriteHook _writeHook; + internal readonly TS State; + private readonly bool _nonFastForwardPush; + private readonly IEnumerable _integrates; + private readonly bool _lastRevFirstParent; + private readonly bool _ignoreIntegrationErrors; + private readonly string? _localRepoPath; + private readonly string _committerName; + private readonly string _committerEmail; + private readonly bool _rebase; + private readonly int _visitChangePageSize; + private readonly bool _gitTagOverwrite; + private readonly IChecker? _checker; + private readonly GitDestinationOptions _destinationOptions; + + internal WriterImpl( + bool skipPush, + string repoUrl, + string remoteFetch, + string remotePush, + bool partialFetch, + string? tagNameTemplate, + string? tagMsgTemplate, + GeneralOptions generalOptions, + GitOptions gitOptions, + IWriteHook writeHook, + TS state, + bool nonFastForwardPush, + IEnumerable integrates, + bool lastRevFirstParent, + bool ignoreIntegrationErrors, + string? localRepoPath, + string committerName, + string committerEmail, + bool rebase, + int visitChangePageSize, + bool gitTagOverwrite, + IChecker? checker, + GitDestinationOptions destinationOptions, + CredentialFileHandler? credentials) + { + SkipPush = skipPush; + _repoUrl = Preconditions.CheckNotNull(repoUrl); + _remoteFetch = Preconditions.CheckNotNull(remoteFetch); + _remotePush = Preconditions.CheckNotNull(remotePush); + _partialFetch = partialFetch; + _tagNameTemplate = tagNameTemplate; + _tagMsgTemplate = tagMsgTemplate; + _force = generalOptions.IsForced(); + _baseConsole = Preconditions.CheckNotNull(generalOptions.GetConsole()); + _generalOptions = generalOptions; + _gitOptions = Preconditions.CheckNotNull(gitOptions); + _writeHook = Preconditions.CheckNotNull(writeHook); + State = Preconditions.CheckNotNull(state); + _nonFastForwardPush = nonFastForwardPush; + _integrates = Preconditions.CheckNotNull(integrates); + _lastRevFirstParent = lastRevFirstParent; + _ignoreIntegrationErrors = ignoreIntegrationErrors; + _localRepoPath = localRepoPath; + _committerName = committerName; + _committerEmail = committerEmail; + _rebase = rebase; + _visitChangePageSize = visitChangePageSize; + _gitTagOverwrite = gitTagOverwrite; + _checker = checker; + _destinationOptions = Preconditions.CheckNotNull(destinationOptions); + } + + public void VisitChanges(GitRevision? start, IChangesVisitor visitor) + { + GitRepository repository = GetRepository(_baseConsole); + try + { + FetchIfNeeded(repository, _baseConsole); + } + catch (ValidationException e) + { + throw new CannotResolveRevisionException( + "Cannot visit changes because fetch failed. Does the destination branch exist?", + e); + } + GitRevision? startRef = GetLocalBranchRevision(repository); + if (startRef == null) + { + return; + } + ChangeReader.Builder queryChanges = + ChangeReader.Builder.ForDestination(repository, _baseConsole); + + GitVisitorUtil.VisitChanges( + start ?? startRef, + visitor, + queryChanges, + _generalOptions, + "destination", + _visitChangePageSize); + } + + private void FetchIfNeeded(GitRepository repo, Console console) + { + if (!State.AlreadyFetched) + { + GitRevision? revision = FetchFromRemote(console, repo, _repoUrl, _remoteFetch); + if (revision != null) + { + try + { + repo.Branch(State.LocalBranch).WithStartPoint(revision.GetHash()).Run(); + } + catch (RepoException e) + { + if (e.Message.Contains($"{State.LocalBranch} already exists")) + { + return; + } + throw; + } + } + State.AlreadyFetched = true; + } + } + + public DestinationStatus? GetDestinationStatus(Glob destinationFiles, string labelName) + { + GitRepository repo = GetRepository(_baseConsole); + try + { + FetchIfNeeded(repo, _baseConsole); + } + catch (AccessValidationException) + { + throw; + } + catch (ValidationException e) + { + _baseConsole.WarnFmt("Error caught when fetching from destination: {0}", e.Message); + return null; + } + GitRevision? startRef = GetLocalBranchRevision(repo); + if (startRef == null) + { + return null; + } + + var pathMatcher = destinationFiles.RelativeTo(""); + var visitor = new DestinationStatusVisitor(pathMatcher, labelName); + ChangeReader.Builder changeReader = + ChangeReader.Builder.ForDestination(repo, _baseConsole) + .SetFirstParent(_lastRevFirstParent) + .Grep("^" + labelName + OriginLabelSeparator); + try + { + GitVisitorUtil.VisitChanges( + startRef, + visitor, + changeReader, + _generalOptions, + "get_destination_status", + _visitChangePageSize); + } + catch (CannotResolveRevisionException e) + { + _baseConsole.WarnFmt("Error caught when visiting changes: {0}", e.Message); + return null; + } + return visitor.GetDestinationStatus(); + } + + public IEndpoint GetFeedbackEndPoint(Console console) => + _writeHook.GetFeedbackEndPoint(console); + + private GitRevision? GetLocalBranchRevision(GitRepository gitRepository) + { + try + { + return gitRepository.ResolveReference(State.LocalBranch); + } + catch (CannotResolveRevisionException) + { + if (_force) + { + return null; + } + throw new RepoException( + $"Could not find {_remoteFetch} in {_repoUrl} and '{GeneralOptions.Force}' was" + + " not used"); + } + } + + public bool SupportsHistory() => true; + + public IReadOnlyList Write( + TransformResult transformResult, Glob destinationFiles, Console console) + { + string? baseline = transformResult.GetBaseline(); + GitRepository scratchClone = GetRepository(console); + FetchIfNeeded(scratchClone, console); + + console.ProgressFmt("Git Destination: Checking out {0}", _remoteFetch); + + GitRevision? localBranchRevision = GetLocalBranchRevision(scratchClone); + UpdateLocalBranchToBaseline(scratchClone, baseline); + if (State.FirstWrite) + { + string reference = baseline ?? State.LocalBranch; + ConfigForPush(GetRepository(console), _repoUrl, _remotePush); + if (!_force && localBranchRevision == null) + { + throw new RepoException( + $"Cannot checkout '{reference}' from '{_repoUrl}'. Use" + + $" '{GeneralOptions.Force}' if the destination is a new git repo or you" + + " don't care about the destination current status"); + } + if (localBranchRevision != null) + { + scratchClone.SimpleCommand( + GetMaxRepoTimeout(), "checkout", "-f", "-q", reference); + } + else + { + // Configure the commit to go to local branch instead of main branch. + scratchClone.SimpleCommand( + "symbolic-ref", "HEAD", GetCompleteRef(State.LocalBranch)); + } + State.FirstWrite = false; + } + else + { + if (!SkipPush) + { + FetchFromRemote(console, scratchClone, _repoUrl, _remoteFetch); + } + // Checkout again in case the origin checkout changed the branch (origin = destination) + if (string.IsNullOrEmpty(scratchClone.GetCurrentBranch())) + { + scratchClone.SimpleCommand( + GetMaxRepoTimeout(), "checkout", "-q", "-f", State.LocalBranch); + } + } + var pathMatcher = destinationFiles.RelativeTo(scratchClone.GetWorkTree()!); + // Get the submodules before we stage them for deletion with add --all. + var excludedAdder = new AddExcludedFilesToIndex(scratchClone, pathMatcher); + excludedAdder.Prepare(transformResult.GetPath()); + excludedAdder.FindSubmodules(console); + + GitRepository alternate = scratchClone.WithWorkTree(transformResult.GetPath()); + + console.Progress("Git Destination: Adding all files"); + using (_generalOptions.Profiler().Start("add_files")) + { + alternate.Add().Force().All().Run(); + } + + console.Progress("Git Destination: Excluding files"); + using (_generalOptions.Profiler().Start("exclude_files")) + { + excludedAdder.Add(); + } + + console.Progress("Git Destination: Creating a local commit"); + MessageInfo messageInfo = _writeHook.GenerateMessageInfo(transformResult); + + alternate.Commit( + transformResult.GetAuthor().ToString(), + transformResult.GetTimestamp(), + AddDestinationLabels( + messageInfo, + transformResult.GetSummary().Trim().Length == 0 + ? "Internal change" + : transformResult.GetSummary())); + + MaybeCheckHeadCommit(alternate, transformResult.GetSummary(), messageInfo); + + var integrateLabels = new List(); + foreach (var integrate in _integrates) + { + IIntegrateLabel? integrateLabel = + integrate.Run( + alternate, + _repoUrl, + _generalOptions, + messageInfo, + path => !pathMatcher.Matches(Path.Combine(scratchClone.GetWorkTree()!, path)), + transformResult, + _ignoreIntegrationErrors); + + if (integrateLabel != null) + { + integrateLabels.Add(integrateLabel); + } + } + + ValidationException.CheckCondition( + transformResult.GetSummary().Trim().Length != 0, + "Change description is empty - this can be the result of scrubbing or an origin" + + " change without description."); + + // Don't leave unstaged/untracked files in the work-tree. + scratchClone.SimpleCommand("reset", "--hard"); + scratchClone.ForceClean(); + + GitRevision? afterRebaseRev = null; + if (baseline != null && _rebase) + { + var rebaseLocks = new[] + { + Path.Combine(alternate.GetGitDir(), "rebase-apply"), + Path.Combine(alternate.GetGitDir(), "rebase-merge"), + }; + foreach (var rebaseLock in rebaseLocks) + { + if (Directory.Exists(rebaseLock) || File.Exists(rebaseLock)) + { + console.Warn("Removing previous rebase failure lock: " + rebaseLock); + FileUtil.DeleteRecursively(rebaseLock); + } + } + + alternate.SimpleCommand("reset", "--hard"); + ValidationException.CheckCondition( + localBranchRevision != null, + "Unable to rebase because the local branch's revision was not resolvable."); + alternate + .RebaseCmdFor(localBranchRevision!.GetHash()) + .ErrorAdvice( + "Please consider to use flag --nogit-destination-rebase to workaround") + .Run(); + afterRebaseRev = alternate.ResolveReference("HEAD"); + if (afterRebaseRev.GetHash() == localBranchRevision.GetHash()) + { + throw new EmptyChangeException( + "Empty change after rebase. The only affected paths were already applied in" + + " main branch. This usually happens if in presubmit workflows where" + + " the used config file is more up-to-date than the origin change" + + " baseline."); + } + } + + string localBranchName = ""; + if (_localRepoPath != null) + { + if (afterRebaseRev != null) + { + localBranchName = "copybara/local"; + alternate.SimpleCommand( + GetMaxRepoTimeout(), "checkout", "-B", localBranchName, + afterRebaseRev.GetHash()); + } + scratchClone.SimpleCommand(GetMaxRepoTimeout(), "checkout", State.LocalBranch); + } + + if (transformResult.IsConfirmedInOrigin()) + { + // Diffs were shown and approved in origin. + } + else if (transformResult.IsAskForConfirmation()) + { + console.Info( + DiffUtil.Colorize( + console, scratchClone.SimpleCommand("show", "HEAD").GetStdout())); + if (!console.PromptConfirmationFmt( + "Proceed with push to {0} {1}?", _repoUrl, _remotePush)) + { + console.Warn("Migration aborted by user."); + throw new ChangeRejectedException( + "User aborted execution: did not confirm diff changes."); + } + } + + GitRevision head = scratchClone.ResolveReference("HEAD"); + IReadOnlyList originChanges = transformResult.GetChanges().GetCurrent(); + string? tagName = CreateTag(scratchClone, console, transformResult); + _writeHook.BeforePush(scratchClone, messageInfo, SkipPush, integrateLabels, originChanges); + + if (SkipPush) + { + console.InfoFmt( + "Git Destination: skipped push to remote. Check the local commits by running:" + + " GIT_DIR={0} git log {1}", + scratchClone.GetGitDir(), + localBranchName); + return ImmutableArray.Create( + new DestinationEffect( + DestinationEffect.EffectType.CREATED, + $"Dry run commit '{head}' created locally at {scratchClone.GetGitDir()}", + originChanges.Cast().ToList(), + new DestinationEffect.DestinationRef(head.GetHash(), "commit", url: null))); + } + string push = + _writeHook.GetPushReference(scratchClone, GetCompleteRef(_remotePush), transformResult); + console.Progress($"Git Destination: Pushing to {_repoUrl} {push}"); + ValidationException.CheckCondition( + !_nonFastForwardPush || _remoteFetch != _remotePush, + "non fast-forward push is only allowed when fetch != push"); + + string capturedTag = tagName!; + string capturedPush = push; + string serverResponse = + _generalOptions.RepoTask( + "push", + () => + scratchClone + .Push() + .WithRefspecs( + _repoUrl, + capturedTag != null + ? new[] + { + scratchClone.CreateRefSpec( + (_nonFastForwardPush ? "+" : "") + "HEAD:" + capturedPush), + scratchClone.CreateRefSpec( + (_gitTagOverwrite ? "+" : "") + capturedTag), + } + : new[] + { + scratchClone.CreateRefSpec( + (_nonFastForwardPush ? "+" : "") + "HEAD:" + capturedPush), + }) + .WithPushOptions(_gitOptions.GitPushOptions.ToImmutableArray()) + .Run()); + return _writeHook.AfterPush(serverResponse, messageInfo, head, originChanges); + } + + private string AddDestinationLabels(MessageInfo messageInfo, string summary) + { + ChangeMessage msg = ChangeMessage.ParseMessage(summary); + foreach (var label in messageInfo.LabelsToAdd) + { + msg = msg.WithNewOrReplacedLabel( + label.GetName(), label.GetSeparator(), label.GetValue()); + } + return msg.ToString(); + } + + private TimeSpan GetMaxRepoTimeout() => + _generalOptions.RepoTimeout > _generalOptions.CommandsTimeout + ? _generalOptions.RepoTimeout + : _generalOptions.CommandsTimeout; + + /// + /// Given a change in HEAD, if a checker is configured, it checks the affected files and the + /// commit message. + /// + private void MaybeCheckHeadCommit( + GitRepository alternate, string beforeCommitMsg, MessageInfo messageInfo) + { + if (_checker == null) + { + return; + } + var head = alternate.Log("HEAD").WithLimit(1).IncludeFiles(true).IncludeBody(true).Run(); + var commit = head[0]; + var files = commit.Files; + string target = alternate.GetWorkTree()!; + // If only a few files, create a copy so the checker doesn't check the whole tree. + if (files != null && files.Count < SmallNumFilesCheckerThreshold) + { + string dest = _generalOptions.GetDirFactory().NewTempDir("git_dest_checker"); + FileUtil.CopyFilesRecursively( + alternate.GetWorkTree()!, + dest, + FileUtil.CopySymlinkStrategy.IgnoreInvalidSymlinks, + Glob.CreateSingleFilesGlob(files)); + target = dest; + } + _checker.DoCheck(target, _baseConsole); + + // TODO(peer): DescriptionChecker processing is owned by the checks peer port. Wire it up + // once DescriptionChecker is available. + } + + private string? CreateTag( + GitRepository gitRepository, Console console, TransformResult transformResult) + { + if (_tagNameTemplate == null) + { + return null; + } + + string? tagName = null; + string? tagMsg = null; + try + { + tagName = LabelFinder.MapLabels( + transformResult.GetLabelFinder(), _tagNameTemplate); + if (_tagMsgTemplate != null) + { + tagMsg = LabelFinder.MapLabels( + transformResult.GetLabelFinder(), _tagMsgTemplate); + } + } + catch (ValidationException e) + { + console.WarnFmt("Get label failed. Error: {0}", e.Message); + } + if (tagName == null) + { + return null; + } + + try + { + if (tagMsg == null) + { + gitRepository.Tag(tagName).Force(_gitTagOverwrite).Run(); + } + else + { + gitRepository.Tag(tagName).WithAnnotatedTag(tagMsg).Force(_gitTagOverwrite).Run(); + } + return tagName; + } + catch (Exception e) when (e is RepoException or ValidationException) + { + if (e.Message.Contains($"tag '{tagName}' already exists")) + { + console.WarnFmt( + "Tag {0} exists. To overwrite it please use flag '--git-tag-overwrite'", + _tagNameTemplate); + } + else + { + console.WarnFmt( + "Create tag failed. Error: {0}. Note that we don't want to fail because of" + + " this", + e.Message); + } + return null; + } + } + + /// Get the local associated with the writer. + public GitRepository GetRepository(Console console) => State.LocalRepo.Load(console); + + private void UpdateLocalBranchToBaseline(GitRepository repo, string? baseline) + { + if (baseline != null && !repo.RefExists(baseline)) + { + throw new RepoException( + "Cannot find baseline '" + baseline + + (GetLocalBranchRevision(repo) != null + ? "' from fetch reference '" + _remoteFetch + "'" + : "' and fetch reference '" + _remoteFetch + "' itself") + + " in " + _repoUrl + "."); + } + if (baseline != null) + { + repo.SimpleCommand("update-ref", State.LocalBranch, baseline); + } + } + + private GitRevision? FetchFromRemote( + Console console, GitRepository repo, string repoUrl, string fetch) + { + string completeFetchRef = GetCompleteRef(fetch); + using (_generalOptions.Profiler().Start("destination_fetch")) + { + console.Progress("Git Destination: Fetching: " + repoUrl + " " + completeFetchRef); + try + { + return repo.FetchSingleRef( + repoUrl, completeFetchRef, _partialFetch, _destinationOptions.GetFetchDepth()); + } + catch (CannotResolveRevisionException) + { + string warning = + $"Git Destination: '{completeFetchRef}' doesn't exist in '{repoUrl}'"; + ValidationException.CheckCondition( + _force, + "{0}. Use {1} flag if you want to push anyway", + warning, + GeneralOptions.Force); + console.Warn(warning); + } + } + return null; + } + + private static string GetCompleteRef(string fetch) => + fetch.StartsWith("refs/", StringComparison.Ordinal) ? fetch : "refs/heads/" + fetch; + + private void ConfigForPush(GitRepository repo, string repoUrl, string push) + { + if (_localRepoPath != null) + { + repo.SimpleCommand("config", "remote.copybara_remote.url", repoUrl); + repo.SimpleCommand( + "config", "remote.copybara_remote.push", State.LocalBranch + ":" + push); + repo.SimpleCommand( + "config", "branch." + State.LocalBranch + ".remote", "copybara_remote"); + } + if (!string.IsNullOrEmpty(_committerName)) + { + repo.SimpleCommand("config", "user.name", _committerName); + } + if (!string.IsNullOrEmpty(_committerEmail)) + { + repo.SimpleCommand("config", "user.email", _committerEmail); + } + VerifyUserInfoConfigured(repo); + } + + public DestinationReader GetDestinationReader( + Console console, Origin.Baseline? baseline, string workdir) => + GetDestinationReader(console, baseline?.GetBaseline(), workdir); + + public DestinationReader GetDestinationReader( + Console console, string? baseline, string workdir) + { + GitRepository repo = GetRepository(console); + FetchIfNeeded(repo, console); + GitRevision? rev; + if (baseline != null) + { + rev = repo.ResolveReference(baseline); + } + else + { + rev = GetLocalBranchRevision(repo); + } + // In case of --force, the destination might be empty and have no revisions. Do not fail. + if (rev == null) + { + console.Info( + "Destination reader requested, but destination is empty. Using noop reader"); + return DestinationReader.NoopDestinationReader; + } + return new GitDestinationReader(repo, rev, workdir); + } + } + + internal string GetFetch() + { + if (PrimaryBranchMigrationMode && PrimaryBranches.Contains(_fetch)) + { + string? resolved = GetResolvedPrimary(); + if (resolved != null) + { + return resolved; + } + } + return _fetch; + } + + internal string GetPush() + { + if (PrimaryBranchMigrationMode && PrimaryBranches.Contains(PushRef)) + { + string? resolved = GetResolvedPrimary(); + if (resolved != null) + { + return resolved; + } + } + return PushRef; + } + + protected string? GetResolvedPrimary() + { + if (_resolvedPrimary == null) + { + try + { + _resolvedPrimary = + GetLocalRepo().Load(_generalOptions.GetConsole()).GetPrimaryBranch(_repoUrl); + } + catch (RepoException) + { + return null; + } + } + return _resolvedPrimary; + } + + public IEnumerable GetIntegrates() => _integrates; + + public string GetLabelNameWhenOrigin() => GitRepository.GitOriginRevId; + + public override string ToString() => + $"GitDestination{{repoUrl={_repoUrl}, fetch={_fetch}, push={PushRef}," + + $" partialFetch={_partialFetch}, primaryBranchMigrationMode={PrimaryBranchMigrationMode}}}"; + + public IWriteHook GetWriterHook() => _writerHook; + + /// Not a public API. It is subject to change. + public LazyResourceLoader GetLocalRepo() => _localRepo; + + public string GetType() => "git.destination"; + + public ImmutableListMultimap Describe(Glob? destinationFiles) + { + var builder = ImmutableListMultimap.CreateBuilder(); + builder.Put("type", GetType()); + builder.Put("url", _repoUrl); + builder.Put("fetch", _fetch); + builder.Put("push", PushRef); + builder.Put("primaryBranchMigrationMode", PrimaryBranchMigrationMode.ToString()); + builder.PutAll(_writerHook.Describe()); + if (destinationFiles != null + && !destinationFiles.Roots().IsEmpty + && !destinationFiles.Roots().Contains("")) + { + builder.PutAll("root", destinationFiles.Roots()); + } + if (_partialFetch) + { + builder.Put("partialFetch", _partialFetch.ToString()); + } + if (_tagName != null) + { + builder.Put("tagName", _tagName); + } + if (_tagMsg != null) + { + builder.Put("tagMsg", _tagMsg); + } + if (_checker != null) + { + builder.Put("checker", _checker.GetType().FullName ?? _checker.GetType().Name); + } + foreach (var integrate in _integrates) + { + builder.Put("integrate", $"{integrate.GetLabel()}:{integrate.GetStrategy()}"); + } + + return builder.Build(); + } + + public IReadOnlyList> DescribeCredentials() + { + if (_credentials == null) + { + return ImmutableArray>.Empty; + } + return GitDescribeCredentials.Convert(_credentials.DescribeCredentials()); + } +} diff --git a/src/Copybara.Core/Git/GitDestinationOptions.cs b/src/Copybara.Core/Git/GitDestinationOptions.cs new file mode 100644 index 000000000..19be06a76 --- /dev/null +++ b/src/Copybara.Core/Git/GitDestinationOptions.cs @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Authoring; +using Copybara.Common; +using Copybara.Exceptions; + +namespace Copybara.Git; + +/// +/// Arguments for , , and other Git components. +/// Port of com.google.copybara.git.GitDestinationOptions. +/// +public sealed class GitDestinationOptions : IOption +{ + private readonly GeneralOptions _generalOptions; + private readonly GitOptions _gitOptions; + + [Flag( + "--git-committer-name", + "If set, overrides the committer name for the generated commits in git destination.")] + public string CommitterName { get; set; } = ""; + + [Flag( + "--git-committer-email", + "If set, overrides the committer e-mail for the generated commits in git destination.")] + public string CommitterEmail { get; set; } = ""; + + [Flag( + "--git-skip-checker", + "If true and git.destination has a configured checker, it will not be used in the" + + " migration.", + Arity = 1)] + public bool SkipGitChecker { get; set; } + + public GitDestinationOptions(GeneralOptions generalOptions, GitOptions gitOptions) + { + _generalOptions = Preconditions.CheckNotNull(generalOptions); + _gitOptions = Preconditions.CheckNotNull(gitOptions); + } + + internal Author GetCommitter() => new(CommitterName, CommitterEmail); + + [Flag("--git-destination-url", "If set, overrides the git destination URL.")] + internal string? Url { get; set; } + + [Flag( + "--git-destination-fetch", + "If set, overrides the git destination fetch reference.")] + public string? Fetch { get; set; } + + [Flag( + "--git-destination-push", + "If set, overrides the git destination push reference.")] + public string? Push { get; set; } + + [Flag( + "--git-destination-path", + "If set, the tool will use this directory for the local repository. Note that if the" + + " directory exists it needs to be a git repository. Copybara will revert any" + + " staged/unstaged changes. For example, you can override destination url with a local" + + " non-bare repo (or existing empty folder) with this flag.")] + public string? LocalRepoPath { get; set; } + + [Flag( + "--git-destination-last-rev-first-parent", + "Use git --first-parent flag when looking for last-rev in previous commits")] + internal bool LastRevFirstParent { get; set; } + + [Flag( + "--git-destination-non-fast-forward", + "Allow non-fast-forward pushes to the destination. We only allow this when used with" + + " different push != fetch references.")] + internal bool NonFastForwardPush { get; set; } + + [Flag( + "--git-destination-ignore-integration-errors", + "If an integration error occurs, ignore it and continue without the integrate")] + internal bool IgnoreIntegrationErrors { get; set; } + + [Flag( + "--nogit-destination-rebase", + "Don't rebase the change automatically for workflows CHANGE_REQUEST mode")] + public bool NoRebase { get; set; } + + [Flag( + "--git-destination-fetch-depth", + "Use a shallow clone of the specified depth for git.destination")] + internal int? FetchDepth { get; set; } + + public int? GetFetchDepth() => FetchDepth; + + internal bool RebaseWhenBaseline() => !NoRebase; + + /// + /// Returns a non-bare repo. Either because it uses a custom worktree or because it is a user + /// non-bare repo. + /// + internal GitRepository LocalGitRepo(string url, CredentialFileHandler? creds) + { + GitRepository repo = GetLocalGitRepository(url); + if (creds != null) + { + try + { + creds.Install(repo, _gitOptions.GetConfigCredsFile(_generalOptions)); + } + catch (IOException e) + { + throw new RepoException("Unable to store git credentials", e); + } + } + return repo; + } + + private GitRepository GetLocalGitRepository(string url) + { + try + { + if (string.IsNullOrEmpty(LocalRepoPath)) + { + return _gitOptions.CachedBareRepoForUrl(url) + .WithWorkTree(_generalOptions.GetDirFactory().NewTempDir("git_dest")); + } + string path = LocalRepoPath; + + if (!Directory.Exists(path) || (Directory.Exists(path) && IsGitRepoOrEmptyDir(path))) + { + Directory.CreateDirectory(path); + return _gitOptions.InitRepo( + GitRepository.NewRepo( + _generalOptions.IsVerbose(), + path, + _gitOptions.GetGitEnvironment(_generalOptions.GetEnvironment()), + _generalOptions.RepoTimeout, + _gitOptions.GitNoVerify, + _gitOptions.GetPushOptionsValidator())); + } + throw new RepoException(path + " is not empty and is not a git repository"); + } + catch (IOException e) + { + throw new RepoException("Cannot create local repository", e); + } + } + + private static bool IsGitRepoOrEmptyDir(string path) => + Directory.Exists(Path.Combine(path, ".git")) + || (!Directory.EnumerateFileSystemEntries(path).Any()); + + /// Used internally to be able to traverse the local repo once a migration finishes. + public string? CustomLocalBranch { get; set; } + + /// Returns the local branch that will be used for working on the change before pushing. + internal string GetLocalBranch(string resolvedPush, bool dryRun) => + LocalRepoPath != null + ? resolvedPush // This is nicer for the user + : CustomLocalBranch + ?? "copybara/resolvedPush-" + Guid.NewGuid() + (dryRun ? "-dryrun" : ""); +} diff --git a/src/Copybara.Core/Git/GitDestinationReader.cs b/src/Copybara.Core/Git/GitDestinationReader.cs new file mode 100644 index 000000000..a8c560141 --- /dev/null +++ b/src/Copybara.Core/Git/GitDestinationReader.cs @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Copybara.Common; +using Copybara.Config; +using Copybara.Exceptions; +using Copybara.Util; +using Starlark.Annot; + +namespace Copybara.Git; + +/// +/// A DestinationReader for reading files from a GitDestination. Port of +/// com.google.copybara.git.GitDestinationReader. +/// +[StarlarkBuiltin( + "git_destination_reader", + Doc = "Handle to read from a git destination", + Documented = false)] +public class GitDestinationReader : DestinationReader +{ + private readonly GitRepository _repository; + private readonly string _workDir; + private readonly GitRevision _baseline; + + public GitDestinationReader(GitRepository repository, GitRevision baseline, string workDir) + { + _repository = Preconditions.CheckNotNull(repository); + _baseline = Preconditions.CheckNotNull(baseline); + _workDir = Preconditions.CheckNotNull(workDir); + } + + public override string ReadFile(string path) => + _repository.ReadFile(_baseline.GetHash(), path); + + public override void CopyDestinationFiles(object glob, object path) + { + var checkoutPath = SkylarkUtil.ConvertFromNoneable(path, null); + Glob resolvedGlob = Glob.WrapGlob(glob, null)!; + if (checkoutPath == null) + { + CopyDestinationFilesToDirectory(resolvedGlob, _workDir); + } + else + { + CopyDestinationFilesToDirectory(resolvedGlob, checkoutPath.FullPath()); + } + } + + public override void CopyDestinationFilesToDirectory(Glob glob, string directory) + { + var treeElements = _repository.LsTree(_baseline, null, true, true); + var pathMatcher = glob.RelativeTo(directory); + foreach (var file in treeElements) + { + string path = Path.Combine(directory, file.Path); + if (pathMatcher.Matches(path)) + { + try + { + string? parent = Path.GetDirectoryName(path); + if (parent != null) + { + Directory.CreateDirectory(parent); + } + } + catch (IOException e) + { + throw new RepoException( + $"Cannot create parent directory for {path}", e); + } + } + } + _repository.Checkout(glob, directory, _baseline); + } + + public override bool Exists(string path) + { + try + { + return _repository.ReadFile(_baseline.GetHash(), path) != null; + } + catch (RepoException) + { + return false; + } + } + + public override string? LastModified(string path) => + _repository.LastModified(_baseline.GetHash(), path); +} diff --git a/src/Copybara.Core/Git/GitHub/Api/AbstractGitHubApiTransport.cs b/src/Copybara.Core/Git/GitHub/Api/AbstractGitHubApiTransport.cs new file mode 100644 index 000000000..31b8fc6c3 --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/AbstractGitHubApiTransport.cs @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Copybara.Common; +using Copybara.Exceptions; +using Copybara.Git; +using ConsoleT = Copybara.Util.Console.Console; + +namespace Copybara.Git.GitHub.Api; + +/// +/// Base implementation of that uses +/// and System.Text.Json for the requests. +/// +/// +/// Port of com.google.copybara.git.github.api.AbstractGitHubApiTransport (google-http-client +/// based). Credentials are resolved through the git credential helper, exactly as upstream: a token +/// for the API host is preferred, falling back to the web host. +/// +public abstract class AbstractGitHubApiTransport : IGitHubApiTransport +{ + private const string GitHubDotComApiUrl = "https://api.github.com"; + private const string GitHubDotComWebUrl = "https://github.com"; + + protected readonly string ApiUrl; + protected readonly string WebUrl; + protected readonly GitRepository Repo; + protected readonly HttpClient HttpClient; + protected readonly string StorePath; + protected readonly ConsoleT Console; + protected readonly bool BearerAuth; + + protected AbstractGitHubApiTransport( + GitRepository repo, + HttpClient httpClient, + string storePath, + bool bearerAuth, + ConsoleT console, + string webUrl) + { + Repo = Preconditions.CheckNotNull(repo); + HttpClient = Preconditions.CheckNotNull(httpClient); + StorePath = storePath; + Console = Preconditions.CheckNotNull(console); + BearerAuth = bearerAuth; + WebUrl = BuildWebUrl(Preconditions.CheckNotNull(webUrl)); + ApiUrl = DetermineApiUrl(WebUrl); + } + + /// Sends the request, allowing subclasses to intercept it (e.g. for tests). + protected abstract Task ExecuteRequestAsync(HttpRequestMessage request); + + public async Task GetAsync( + string path, + ImmutableListMultimap headers, + string requestDescription) + { + GitCredential.UserPassword? credentials = GetCredentialsIfPresent(); + Uri url = GetFullEndpointUrl(path); + try + { + Console.VerboseFmt("Executing {0}", requestDescription); + using var request = new HttpRequestMessage(HttpMethod.Get, url); + ApplyHeaders(request, credentials, headers); + using HttpResponseMessage response = await ExecuteRequestAsync(request).ConfigureAwait(false); + await ThrowIfError(response, "GET", path, null).ConfigureAwait(false); + return await ParseResponseAsync(response).ConfigureAwait(false); + } + catch (HttpRequestException e) + { + throw new RepoException("Error running GitHub API operation " + path, e); + } + } + + public Task GetAsync(string path, string requestDescription) => + GetAsync(path, ImmutableListMultimap.Empty, requestDescription); + + public async Task PostAsync(string path, object request, string requestType) + { + GitCredential.UserPassword credentials = GetCredentials(); + Uri url = GetFullEndpointUrl(path); + string requestJson = JsonSerializer.Serialize(request, request.GetType(), GitHubApiJson.Options); + try + { + Console.VerboseFmt("Executing {0}", requestType); + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url) + { + Content = new StringContent(requestJson, Encoding.UTF8, "application/json"), + }; + ApplyHeaders(httpRequest, credentials, ImmutableListMultimap.Empty); + using HttpResponseMessage response = + await ExecuteRequestAsync(httpRequest).ConfigureAwait(false); + await ThrowIfError(response, "POST", path, requestJson).ConfigureAwait(false); + return await ParseResponseAsync(response).ConfigureAwait(false); + } + catch (HttpRequestException e) + { + throw new RepoException("Error running GitHub API operation " + path, e); + } + } + + public abstract Task DeleteAsync(string path, string requestType); + + private async Task ParseResponseAsync(HttpResponseMessage response) + { + T? responseObj = await ParseHttpResponseAsync(response).ConfigureAwait(false); + if (responseObj is IPaginatedPayload paginatedPayload) + { + return (T)paginatedPayload.AnnotatePayload(ApiUrl, MaybeGetLinkHeader(response)); + } + + return responseObj; + } + + private static async Task ParseHttpResponseAsync(HttpResponseMessage response) + { + if (response.StatusCode == HttpStatusCode.NoContent) + { + return default; + } + + byte[] bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + if (bytes.Length == 0) + { + return default; + } + + try + { + return JsonSerializer.Deserialize(bytes, GitHubApiJson.Options); + } + catch (Exception e) when (e is JsonException or NotSupportedException) + { + throw new RepoException( + $"Cannot parse content as type {typeof(T)}.\nContent: {Encoding.UTF8.GetString(bytes)}\n", + e); + } + } + + /// Throws a if the response is not a success. + protected async Task ThrowIfError( + HttpResponseMessage response, string method, string path, string? request) + { + if (response.IsSuccessStatusCode) + { + return; + } + + string? content = response.Content == null + ? null + : await response.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new GitHubApiException( + (int)response.StatusCode, + ParseErrorOrIgnore(content), + method, + path, + request, + content); + } + + protected Uri GetFullEndpointUrl(string path) + { + string maybePrefix = path.StartsWith("/", StringComparison.Ordinal) ? "" : "/"; + return new Uri(ApiUrl + maybePrefix + path); + } + + protected static ClientError? ParseErrorOrIgnore(string? content) + { + if (content == null) + { + return null; + } + + try + { + return JsonSerializer.Deserialize(content, GitHubApiJson.Options); + } + catch (JsonException) + { + return new ClientError(); + } + } + + protected static string? MaybeGetLinkHeader(HttpResponseMessage response) + { + if (response.Headers.TryGetValues("Link", out IEnumerable? link)) + { + return link.FirstOrDefault(); + } + + return null; + } + + protected void ApplyHeaders( + HttpRequestMessage request, + GitCredential.UserPassword? userPassword, + ImmutableListMultimap headers) + { + if (userPassword != null) + { + if (BearerAuth) + { + request.Headers.Authorization = + new AuthenticationHeaderValue("Bearer", userPassword.GetPasswordBeCareful()); + } + else + { + string basic = Convert.ToBase64String( + Encoding.ASCII.GetBytes( + userPassword.GetUsername() + ":" + userPassword.GetPasswordBeCareful())); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", basic); + } + } + + foreach (KeyValuePair header in headers) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + /// Credentials for API should be optional for any read operation (GET). + protected GitCredential.UserPassword? GetCredentialsIfPresent() + { + try + { + return GetCredentials(); + } + catch (ValidationException) + { + string msg = + $"GitHub credentials not found in {StorePath}. Assuming the repository is public."; + Console.Info(msg); + return null; + } + } + + /// + /// Gets the credentials from git credential helper. First tries the apiUrl host, then falls back + /// to the webUrl host. + /// + /// + /// + protected GitCredential.UserPassword GetCredentials() + { + try + { + return Repo.CredentialFill(ApiUrl); + } + catch (ValidationException) + { + try + { + return Repo.CredentialFill(WebUrl); + } + catch (ValidationException e1) + { + throw new ValidationException( + $"Cannot get credentials for host {WebUrl} or {ApiUrl} from credentials helper." + + " Make sure either your credential helper has the username and password/token" + + $" or if you don't use one, that file '{StorePath}' contains one of the two" + + " lines: \nEither:\n" + + $"https://USERNAME:TOKEN@{RemoveHttpsPrefix(ApiUrl)}\n" + + "or:\n" + + $"https://USERNAME:TOKEN@{RemoveHttpsPrefix(WebUrl)}\n" + + "\n" + + "Note that spaces or other special characters need to be escaped. For example" + + " ' ' should be %20 and '@' should be %40 (For example when using the email" + + " as username)", + e1); + } + } + } + + private static string RemoveHttpsPrefix(string url) => url.Replace("https://", ""); + + private static string BuildWebUrl(string hostName) => "https://" + hostName; + + private static string DetermineApiUrl(string hostName) + { + // Github.com has a unique API URL. + // GitHub Enterprise instances have a specific format of API URL. + if (hostName == GitHubDotComWebUrl) + { + return GitHubDotComApiUrl; + } + + return hostName + "/api/v3"; + } + + public string GetApiUrl() => ApiUrl; + + public string GetWebUrl() => WebUrl; +} diff --git a/src/Copybara.Core/Git/GitHub/Api/AddAssignees.cs b/src/Copybara.Core/Git/GitHub/Api/AddAssignees.cs new file mode 100644 index 000000000..40bf3b722 --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/AddAssignees.cs @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; + +namespace Copybara.Git.GitHub.Api; + +/// +/// An object that represents the list of github users to assign to a pull request or issue. +/// +public class AddAssignees +{ + [JsonPropertyName("assignees")] + public List? Assignees { get; set; } + + public List? GetAssignees() => Assignees; + + public void SetAssignees(List assignees) => Assignees = assignees; + + public AddAssignees(List assignees) + { + Assignees = assignees; + } + + public AddAssignees() + { + } +} diff --git a/src/Copybara.Core/Git/GitHub/Api/AddLabels.cs b/src/Copybara.Core/Git/GitHub/Api/AddLabels.cs new file mode 100644 index 000000000..855eeb12c --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/AddLabels.cs @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using Copybara.Common; + +namespace Copybara.Git.GitHub.Api; + +/// +/// Request type for adding a label to an issue. +/// https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue +/// +public class AddLabels +{ + [JsonPropertyName("labels")] + public List? Labels { get; set; } + + public AddLabels(IReadOnlyList labels) + { + Preconditions.CheckNotNull(labels); + Labels = new List(labels); + } + + public AddLabels() + { + } + + public IReadOnlyList GetLabels() => + Labels == null ? ImmutableArray.Empty : Labels.ToImmutableArray(); +} diff --git a/src/Copybara.Core/Git/GitHub/Api/AuthorAssociation.cs b/src/Copybara.Core/Git/GitHub/Api/AuthorAssociation.cs new file mode 100644 index 000000000..9ad919c6f --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/AuthorAssociation.cs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Copybara.Git.GitHub.Api; + +/// +/// GitHub type of author for pull request, reviews, etc. +/// +/// See https://developer.github.com/v4/reference/enum/commentauthorassociation/ +/// +public enum AuthorAssociation +{ + CONTRIBUTOR, + COLLABORATOR, + MEMBER, + OWNER, + NONE, + FIRST_TIMER, + FIRST_TIME_CONTRIBUTOR, +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CheckRun.cs b/src/Copybara.Core/Git/GitHub/Api/CheckRun.cs new file mode 100644 index 000000000..74501de2b --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CheckRun.cs @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GitHub.Api; + +/// +/// Represents a GitHub App's checkRun detail. +/// https://developer.github.com/v3/checks/runs/#create-a-check-run +/// https://developer.github.com/v3/checks/runs/#response +/// +[StarlarkBuiltin( + "github_check_run_obj", + Doc = + "Detail about a check run as defined in " + + "https://developer.github.com/v3/checks/runs/#create-a-check-run")] +public class CheckRun : IStarlarkValue +{ + [JsonPropertyName("details_url")] + public string? DetailUrl { get; set; } + + [JsonPropertyName("status")] + public CheckRunStatus Status { get; set; } + + [JsonPropertyName("conclusion")] + public CheckRunConclusion? Conclusion { get; set; } + + [JsonPropertyName("head_sha")] + public string? Sha { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("app")] + public GitHubApp? App { get; set; } + + [JsonPropertyName("output")] + public Output? Output { get; set; } + + [JsonPropertyName("pull_requests")] + public List? PullRequests { get; set; } + + [StarlarkMethod( + "detail_url", + Doc = "The URL of the integrator's site that has the full details of the check.", + StructField = true, + AllowReturnNones = true)] + public string? GetDetailUrl() => DetailUrl; + + [StarlarkMethod( + "status", + Doc = "The current status of the check run. Can be one of queued, in_progress, or completed.", + StructField = true)] + public string GetStatus() => Status.ToString().ToLowerInvariant(); + + [StarlarkMethod( + "conclusion", + Doc = "The final conclusion of the check. Can be one of success, failure, neutral, " + + "cancelled, timed_out, or action_required.", + StructField = true, + AllowReturnNones = true)] + public string? GetConclusion() => Conclusion?.ToString().ToLowerInvariant(); + + [StarlarkMethod("sha", Doc = "The SHA-1 the check run is based on", StructField = true)] + public string? GetSha() => Sha; + + [StarlarkMethod("name", Doc = "The name of the check", StructField = true)] + public string? GetName() => Name; + + [StarlarkMethod("app", Doc = "The detail of a GitHub App, such as id, slug, and name", StructField = true)] + public GitHubApp? GetApp() => App; + + [StarlarkMethod( + "output", + Doc = "The description of a GitHub App's run, including title, summary, text.", + StructField = true)] + public Output? GetOutput() => Output; + + [StarlarkMethod( + "pulls", + Doc = "Pull requests associated with this check_run ('number' only)", + StructField = true)] + public IReadOnlyList GetPullRequests() => + PullRequests == null + ? ImmutableArray.Empty + : PullRequests.ToImmutableArray(); + + public override string ToString() => + $"CheckRun{{details_url={DetailUrl}, status={Status}, conclusion={Conclusion}, sha={Sha}," + + $" name={Name}, app={App}, output={Output}, pulls={PullRequests}}}"; + + /// PR submessage in check_run. + public class CheckRunPullRequest : IStarlarkValue + { + [JsonPropertyName("number")] + public int Number { get; set; } + + [StarlarkMethod("number", Doc = "Number of a PR liked to the check_run", StructField = true)] + public int GetNumber() => Number; + + public override string ToString() => $"PullRequest{{number={Number}}}"; + } +} + +/// Status of a check run. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum CheckRunStatus +{ + [JsonStringEnumMemberName("queued")] + QUEUED, + + [JsonStringEnumMemberName("in_progress")] + IN_PROGRESS, + + [JsonStringEnumMemberName("completed")] + COMPLETED, + + [JsonStringEnumMemberName("pending")] + PENDING, +} + +/// Conclusion of a check run status. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum CheckRunConclusion +{ + NONE, + + [JsonStringEnumMemberName("success")] + SUCCESS, + + [JsonStringEnumMemberName("failure")] + FAILURE, + + [JsonStringEnumMemberName("neutral")] + NEUTRAL, + + [JsonStringEnumMemberName("timed_out")] + TIMEDOUT, + + [JsonStringEnumMemberName("cancelled")] + CANCELLED, + + [JsonStringEnumMemberName("action_required")] + ACTIONREQUIRED, + + [JsonStringEnumMemberName("skipped")] + SKIPPED, + + [JsonStringEnumMemberName("stale")] + STALE, + + [JsonStringEnumMemberName("startup_failure")] + STARTUPFAILURE, +} + +/// Helpers for . +public static class CheckRunConclusions +{ + private static readonly IReadOnlyDictionary ApiValues = + new Dictionary + { + [CheckRunConclusion.NONE] = "none", + [CheckRunConclusion.SUCCESS] = "success", + [CheckRunConclusion.FAILURE] = "failure", + [CheckRunConclusion.NEUTRAL] = "neutral", + [CheckRunConclusion.TIMEDOUT] = "timed_out", + [CheckRunConclusion.CANCELLED] = "cancelled", + [CheckRunConclusion.ACTIONREQUIRED] = "action_required", + [CheckRunConclusion.SKIPPED] = "skipped", + [CheckRunConclusion.STALE] = "stale", + [CheckRunConclusion.STARTUPFAILURE] = "startup_failure", + }; + + public static string GetApiVal(this CheckRunConclusion conclusion) => ApiValues[conclusion]; + + /// + /// Given a String value of Conclusion as returned by GitHub API, returns the equivalent enum + /// value or null if not found. + /// + public static CheckRunConclusion? FromValue(string val) + { + foreach (var kv in ApiValues) + { + if (kv.Value == val) + { + return kv.Key; + } + } + + return null; + } +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CheckRuns.cs b/src/Copybara.Core/Git/GitHub/Api/CheckRuns.cs new file mode 100644 index 000000000..4577ac999 --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CheckRuns.cs @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GitHub.Api; + +/// +/// Represents the response of list check runs for a specific ref. +/// https://developer.github.com/v3/checks/runs/#list-check-runs-for-a-specific-ref +/// +[StarlarkBuiltin( + "github_check_runs_obj", + Doc = + "List check runs for a specific ref " + + "https://developer.github.com/v3/checks/runs/#list-check-runs-for-a-specific-ref")] +public class CheckRuns : IStarlarkValue, IPaginatedPayload +{ + [JsonPropertyName("total_count")] + public int TotalCount { get; set; } + + [JsonPropertyName("check_runs")] + public PaginatedList CheckRunsList { get; set; } = new(); + + public CheckRuns() + { + } + + private CheckRuns(int totalCount, PaginatedList checkRuns) + { + CheckRunsList = checkRuns; + TotalCount = totalCount; + } + + [StarlarkMethod("total_count", Doc = "The total count of check runs.", StructField = true)] + public int GetTotalCount() => TotalCount; + + [StarlarkMethod("check_runs", Doc = "The list of the detail for each check run.", StructField = true)] + public IReadOnlyList GetCheckRuns() => CheckRunsList; + + public override string ToString() => + $"CheckRuns{{total_count={TotalCount}, check_runs={CheckRunsList}}}"; + + public PaginatedList GetPayload() => CheckRunsList; + + public IPaginatedPayload AnnotatePayload(string apiPrefix, string? linkHeader) => + new CheckRuns(TotalCount, CheckRunsList.WithPaginationInfo(apiPrefix, linkHeader)); +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CheckSuite.cs b/src/Copybara.Core/Git/GitHub/Api/CheckSuite.cs new file mode 100644 index 000000000..91a74e486 --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CheckSuite.cs @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GitHub.Api; + +/// +/// Represents a GitHub App's checkRun detail. +/// https://docs.github.com/en/rest/checks/suites?apiVersion=2022-11-28#list-check-suites-for-a-git-reference +/// +[StarlarkBuiltin( + "github_check_suite_obj", + Doc = + "Detail about a check run as defined in " + + "https://developer.github.com/v3/checks/runs/#create-a-check-run")] +public class CheckSuite : IStarlarkValue +{ + [JsonPropertyName("status")] + public CheckRunStatus Status { get; set; } + + [JsonPropertyName("conclusion")] + public CheckRunConclusion? Conclusion { get; set; } + + [JsonPropertyName("head_sha")] + public string? Sha { get; set; } + + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("app")] + public GitHubApp? App { get; set; } + + [StarlarkMethod("id", Doc = "Check suite identifier", StructField = true)] + public StarlarkInt GetId() => StarlarkInt.Of(Id); + + [StarlarkMethod( + "status", + Doc = "The current status of the check run. Can be one of queued, in_progress, pending," + + " or completed.", + StructField = true)] + public string GetStatus() => Status.ToString().ToLowerInvariant(); + + [StarlarkMethod( + "conclusion", + Doc = "The final conclusion of the check. Can be one of success, failure, neutral, " + + "cancelled, timed_out, or action_required.", + StructField = true, + AllowReturnNones = true)] + public string? GetConclusion() => Conclusion?.ToString().ToLowerInvariant(); + + [StarlarkMethod("sha", Doc = "The SHA-1 the check run is based on", StructField = true)] + public string? GetSha() => Sha; + + [StarlarkMethod("app", Doc = "The detail of a GitHub App, such as id, slug, and name", StructField = true)] + public GitHubApp? GetApp() => App; + + public override string ToString() => + $"CheckSuite{{id={Id}, status={Status}, conclusion={Conclusion}, sha={Sha}, app={App}}}"; +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CheckSuites.cs b/src/Copybara.Core/Git/GitHub/Api/CheckSuites.cs new file mode 100644 index 000000000..2143ec3fc --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CheckSuites.cs @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GitHub.Api; + +/// +/// Represents a GitHub App's checkSuites response detail. +/// https://docs.github.com/en/rest/checks/suites?apiVersion=2022-11-28#list-check-suites-for-a-git-reference +/// +[StarlarkBuiltin( + "github_check_suites_response_obj", + Doc = + "Detail about a check run as defined in " + + "https://docs.github.com/en/rest/checks/suites?apiVersion=2022-11-28#list-check-suites-for-a-git-reference")] +public class CheckSuites : IStarlarkValue, IPaginatedPayload +{ + [JsonPropertyName("total_count")] + public int TotalCount { get; set; } + + [JsonPropertyName("check_suites")] + public PaginatedList CheckSuitesList { get; set; } = new(); + + public CheckSuites() + { + } + + private CheckSuites(int totalCount, PaginatedList checkSuites) + { + CheckSuitesList = checkSuites; + TotalCount = totalCount; + } + + public PaginatedList GetPayload() => CheckSuitesList; + + public IPaginatedPayload AnnotatePayload(string apiPrefix, string? linkHeader) => + new CheckSuites(TotalCount, CheckSuitesList.WithPaginationInfo(apiPrefix, linkHeader)); +} diff --git a/src/Copybara.Core/Git/GitHub/Api/ClientError.cs b/src/Copybara.Core/Git/GitHub/Api/ClientError.cs new file mode 100644 index 000000000..367d695ac --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/ClientError.cs @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Copybara.Git.GitHub.Api; + +/// +/// GitHub client errors always have a message and sometimes a documentation_url. +/// +public class ClientError +{ + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("documentation_url")] + public string? DocumentationUrl { get; set; } + + [JsonPropertyName("errors")] + public List? Errors { get; set; } + + public string? GetMessage() => Message; + + public string? GetDocumentationUrl() => DocumentationUrl; + + public IReadOnlyList GetErrors() => + Errors == null ? ImmutableArray.Empty : Errors.ToImmutableArray(); + + public override string ToString() + { + try + { + return JsonSerializer.Serialize(this); + } + catch (Exception e) + { + throw new InvalidOperationException("Unexpected error: ", e); + } + } + + /// An individual error entry within a . + public class ErrorItem + { + [JsonPropertyName("resource")] + public string? Resource { get; set; } + + [JsonPropertyName("field")] + public string? Field { get; set; } + + [JsonPropertyName("code")] + public ErrorType Code { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + public string? GetResource() => Resource; + + public string? GetField() => Field; + + public ErrorType GetCode() => Code; + + public string? GetMessage() => Message; + } +} + +/// Error type codes reported by GitHub client errors. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ErrorType +{ + [JsonStringEnumMemberName("missing")] + MISSING, + + [JsonStringEnumMemberName("missing_field")] + MISSING_FIELD, + + [JsonStringEnumMemberName("invalid")] + INVALID, + + [JsonStringEnumMemberName("already_exists")] + ALREADY_EXISTS, + + [JsonStringEnumMemberName("custom")] + CUSTOM, +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CombinedStatus.cs b/src/Copybara.Core/Git/GitHub/Api/CombinedStatus.cs new file mode 100644 index 000000000..e9bf20137 --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CombinedStatus.cs @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GitHub.Api; + +/// +/// A combined commit status object. +/// +/// https://developer.github.com/v3/repos/statuses +/// +[StarlarkBuiltin( + "github_api_combined_status_obj", + Doc = + "Combined Information about a commit status as defined in" + + " https://developer.github.com/v3/repos/statuses. This is a subset of the available" + + " fields in GitHub")] +public class CombinedStatus : IStarlarkValue +{ + [JsonPropertyName("state")] + public StatusState State { get; set; } + + [JsonPropertyName("sha")] + public string? Sha { get; set; } + + [JsonPropertyName("total_count")] + public int TotalCount { get; set; } + + [JsonPropertyName("statuses")] + public List? Statuses { get; set; } + + public StatusState GetState() => State; + + [StarlarkMethod( + "state", + Doc = "The overall state of all statuses for a commit: success, failure, pending or error", + StructField = true)] + public string GetStateForSkylark() => State.ToString().ToLowerInvariant(); + + [StarlarkMethod("sha", Doc = "The SHA-1 of the commit", StructField = true)] + public string? GetSha() => Sha; + + [StarlarkMethod("total_count", Doc = "Total number of statuses", StructField = true)] + public int GetTotalCount() => TotalCount; + + [StarlarkMethod("statuses", Doc = "List of statuses for the commit", StructField = true)] + public ISequence GetStatuses() => + StarlarkList.ImmutableCopyOf((Statuses ?? new List()).Cast()); +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CommentBody.cs b/src/Copybara.Core/Git/GitHub/Api/CommentBody.cs new file mode 100644 index 000000000..9b14720f4 --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CommentBody.cs @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; + +namespace Copybara.Git.GitHub.Api; + +/// +/// Request type for https://docs.github.com/en/rest/reference/issues#create-an-issue-comment. +/// +public class CommentBody +{ + [JsonPropertyName("body")] + public string? Body { get; set; } + + public CommentBody(string? body) + { + Body = body; + } + + public CommentBody() + { + } + + public string? GetBody() => Body; +} diff --git a/src/Copybara.Core/Git/GitHub/Api/Commit.cs b/src/Copybara.Core/Git/GitHub/Api/Commit.cs new file mode 100644 index 000000000..00f9369e5 --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/Commit.cs @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GitHub.Api; + +/// Represents the current status of a ref, as returned by the git/refs API call. +[StarlarkBuiltin( + "github_api_commit_obj", + Doc = + "Commit field for GitHub commit information" + + " https://developer.github.com/v3/git/commits/#get-a-commit." + + " This is a subset of the available fields in GitHub")] +public class Commit : IStarlarkValue +{ + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("author")] + public CommitAuthor? Author { get; set; } + + [JsonPropertyName("committer")] + public CommitAuthor? Committer { get; set; } + + [StarlarkMethod("message", Doc = "Message of the commit", StructField = true)] + public string? GetMessage() => Message; + + [StarlarkMethod("author", Doc = "Author of the commit", StructField = true)] + public CommitAuthor? GetAuthor() => Author; + + [StarlarkMethod("committer", Doc = "Committer of the commit", StructField = true)] + public CommitAuthor? GetCommitter() => Committer; + + [StarlarkBuiltin( + "github_api_commit_author_obj", + Doc = + "Author/Committer for commit field for GitHub commit information" + + " https://developer.github.com/v3/git/commits/#get-a-commit." + + " This is a subset of the available fields in GitHub")] + public class CommitAuthor : IStarlarkValue + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } + + [JsonPropertyName("date")] + public string? Date { get; set; } + + public DateTimeOffset GetDate() => DateTimeOffset.Parse(Date!); + + [StarlarkMethod("date", Doc = "Date of the commit", StructField = true)] + public string? GetDateForSkylark() => Date; + + [StarlarkMethod("name", Doc = "Name of the author/committer", StructField = true)] + public string? GetName() => Name; + + [StarlarkMethod("email", Doc = "Email of the author/committer", StructField = true)] + public string? GetEmail() => Email; + + public override string ToString() => + $"CommitAuthor{{name={Name}, email={Email}, date={Date}}}"; + } + + public override string ToString() => + $"Commit{{message={Message}, author={Author}, committer={Committer}}}"; +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CommitHistoryResponse.cs b/src/Copybara.Core/Git/GitHub/Api/CommitHistoryResponse.cs new file mode 100644 index 000000000..5bcfa1770 --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CommitHistoryResponse.cs @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; + +namespace Copybara.Git.GitHub.Api; + +/// POJO representing the response from GitHubGraphQLApi.GET_COMMIT_HISTORY_QUERY. +public class CommitHistoryResponse +{ + [JsonPropertyName("data")] + public CommitHistoryData? Data { get; set; } + + public CommitHistoryData? GetData() => Data; + + public override string ToString() => $"CommitHistoryResponse{{data={Data}}}"; + + /// Represents 'data' values. + public class CommitHistoryData + { + [JsonPropertyName("repository")] + public CommitHistoryRepository? Repository { get; set; } + + public CommitHistoryRepository? GetRepository() => Repository; + + public override string ToString() => $"Data{{repository={Repository}}}"; + } + + /// Represents 'repository' values. + public class CommitHistoryRepository + { + [JsonPropertyName("ref")] + public CommitHistoryRef? Ref { get; set; } + + public CommitHistoryRef? GetRef() => Ref; + + public override string ToString() => $"Repository{{ref={Ref}}}"; + } + + /// Represents 'ref' values. + public class CommitHistoryRef + { + [JsonPropertyName("target")] + public Target? Target { get; set; } + + public Target? GetTarget() => Target; + + public override string ToString() => $"Ref{{target={Target}}}"; + } + + /// Represents 'target' values. + public class Target + { + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("history")] + public HistoryNodes? HistoryNodes { get; set; } + + public string? GetId() => Id; + + public HistoryNodes? GetHistoryNodes() => HistoryNodes; + + public override string ToString() => $"Target{{id={Id}, historyNodes={HistoryNodes}}}"; + } + + /// Represents 'history.nodes' values. + public class HistoryNodes + { + [JsonPropertyName("nodes")] + public List? Nodes { get; set; } + + public List? GetNodes() => Nodes; + + public override string ToString() => $"HistoryNodes{{nodes={Nodes}}}"; + } + + /// Represents 'history.node' element values. + public class HistoryNode + { + [JsonPropertyName("associatedPullRequests")] + public AssociatedPullRequests? AssociatedPullRequests { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("oid")] + public string? Oid { get; set; } + + public AssociatedPullRequests? GetAssociatedPullRequests() => AssociatedPullRequests; + + public string? GetId() => Id; + + public string? GetOid() => Oid; + + public override string ToString() => + $"HistoryNode{{associatedPullRequest={AssociatedPullRequests}, id={Id}, oid={Oid}}}"; + } + + /// Represents 'associatedPullRequests' values. + public class AssociatedPullRequests + { + [JsonPropertyName("edges")] + public List? Edges { get; set; } + + public List? GetEdges() => Edges; + + public override string ToString() => $"AssociatedPullRequests{{edges={Edges}}}"; + } + + /// Represents 'associatedPullRequests.edges' values. + public class PullRequestEdges + { + [JsonPropertyName("node")] + public AssociatedPullRequestNode? Node { get; set; } + + public AssociatedPullRequestNode? GetNode() => Node; + + public override string ToString() => $"PullRequestEdges{{node={Node}}}"; + } + + /// Represents 'associatedPullRequests.edges.node' values. + public class AssociatedPullRequestNode + { + [JsonPropertyName("reviewDecision")] + public string? ReviewDecision { get; set; } + + [JsonPropertyName("author")] + public Author? Author { get; set; } + + [JsonPropertyName("latestOpinionatedReviews")] + public LatestOpinionatedReviews? LatestOpinionatedReviews { get; set; } + + [JsonPropertyName("mergedBy")] + public MergedBy? MergedBy { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + public string? GetReviewDecision() => ReviewDecision; + + public Author? GetAuthor() => Author; + + public LatestOpinionatedReviews? GetLatestOpinionatedReviews() => LatestOpinionatedReviews; + + public MergedBy? GetMergedBy() => MergedBy; + + public string? GetTitle() => Title; + + public override string ToString() => + $"AssociatedPullRequestNode{{reviewDecision={ReviewDecision}, author={Author}," + + $" latestOpinionatedReviews={LatestOpinionatedReviews}, mergedBy={MergedBy}," + + $" title={Title}}}"; + } + + /// Represents 'latestOpinionatedReviews' values. + public class LatestOpinionatedReviews + { + [JsonPropertyName("edges")] + public List? Edges { get; set; } + + public List? GetEdges() => Edges; + + public override string ToString() => $"LatestOpinionatedReviews{{edges={Edges}}}"; + } + + /// Represents 'latestOptionatedReviews.edges' values. + public class AuthorEdges + { + [JsonPropertyName("node")] + public AuthorNode? Node { get; set; } + + public AuthorNode? GetNode() => Node; + + public override string ToString() => $"AuthorEdges{{node={Node}}}"; + } + + /// Represents 'latestOpinionatedReviews.edges.node' values. + public class AuthorNode + { + [JsonPropertyName("author")] + public Author? Author { get; set; } + + [JsonPropertyName("state")] + public string? State { get; set; } + + public string? GetState() => State; + + public Author? GetAuthor() => Author; + + public override string ToString() => $"AuthorNode{{author={Author}, state={State}}}"; + } + + /// Represents 'author' values. + public class Author + { + [JsonPropertyName("login")] + public string? Login { get; set; } + + public string? GetLogin() => Login; + + public override string ToString() => $"Author{{login={Login}}}"; + } + + /// Represents 'mergedBy' values. + public class MergedBy + { + [JsonPropertyName("login")] + public string? Login { get; set; } + + public string? GetLogin() => Login; + + public override string ToString() => $"MergedBy{{login={Login}}}"; + } +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CreatePullRequest.cs b/src/Copybara.Core/Git/GitHub/Api/CreatePullRequest.cs new file mode 100644 index 000000000..aca3ff51e --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CreatePullRequest.cs @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Copybara.Common; + +namespace Copybara.Git.GitHub.Api; + +/// An object that represents the creation of a Pull Request. +public class CreatePullRequest +{ + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("body")] + public string? Body { get; set; } + + /// + /// Branch to use for the pull request, can be a reference to another github repository if + /// somerepo:branch format is used. + /// + [JsonPropertyName("head")] + public string? Head { get; set; } + + /// Base of the pull request, usually something like 'master'. + [JsonPropertyName("base")] + public string? Base { get; set; } + + [JsonPropertyName("draft")] + public bool Draft { get; set; } + + public string? GetTitle() => Title; + + public string? GetBody() => Body; + + public string? GetHead() => Head; + + public string? GetBase() => Base; + + public void SetTitle(string title) => Title = title; + + public bool GetDraft() => Draft; + + public CreatePullRequest(string title, string body, string head, string @base, bool draft) + { + Title = Preconditions.CheckNotNull(title); + Body = Preconditions.CheckNotNull(body); + Head = Preconditions.CheckNotNull(head); + Base = Preconditions.CheckNotNull(@base); + Draft = draft; + } + + public CreatePullRequest() + { + } +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CreateReleaseRequest.cs b/src/Copybara.Core/Git/GitHub/Api/CreateReleaseRequest.cs new file mode 100644 index 000000000..33f261dcc --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CreateReleaseRequest.cs @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Starlark.Annot; +using Starlark.Eval; + +namespace Copybara.Git.GitHub.Api; + +/// +/// Input for creating a release. +/// https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release +/// +[StarlarkBuiltin( + "github_create_release_obj", + Doc = "GitHub API value type for release params. See " + + "https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release")] +public class CreateReleaseRequest : IStarlarkValue +{ + [JsonPropertyName("tag_name")] + public string? TagName { get; set; } + + [JsonPropertyName("body")] + public string? Body { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("target_commitish")] + public string? TargetCommitish { get; set; } + + [JsonPropertyName("prerelease")] + public bool? PreRelease { get; set; } + + [JsonPropertyName("draft")] + public bool? Draft { get; set; } + + [JsonPropertyName("make_latest")] + public bool? MakeLatest { get; set; } + + [JsonPropertyName("generate_release_notes")] + public bool? GenerateReleaseNotes { get; set; } + + public CreateReleaseRequest(string tagName) + { + TagName = tagName; + } + + public CreateReleaseRequest() + { + // just for reflection. + } + + [StarlarkMethod( + "with_body", + Doc = "Set the body for the release.")] + public CreateReleaseRequest WithBody([Param(Name = "body", Doc = "Body for the release")] string body) + { + Body = body; + return this; + } + + [StarlarkMethod( + "with_name", + Doc = "Set the name for the release.")] + public CreateReleaseRequest WithName([Param(Name = "name", Doc = "Name for the release")] string name) + { + Name = name; + return this; + } + + [StarlarkMethod( + "with_commitish", + Doc = "Set the commitish to be used for the release. Defaults to HEAD")] + public CreateReleaseRequest WithCommitish( + [Param(Name = "commitish", Doc = "Commitish for the release")] string targetCommitish) + { + TargetCommitish = targetCommitish; + return this; + } + + [StarlarkMethod( + "set_draft", + Doc = "Is this a draft release?")] + public CreateReleaseRequest WithDraft([Param(Name = "draft", Doc = "Mark release as draft?")] bool draft) + { + Draft = draft; + return this; + } + + [StarlarkMethod( + "set_latest", + Doc = "Is this the latest release?")] + public CreateReleaseRequest WithMakeLatest( + [Param(Name = "make_latest", Doc = "Mark release as latest?")] bool makeLatest) + { + MakeLatest = makeLatest; + return this; + } + + [StarlarkMethod( + "set_prerelease", + Doc = "Is this a prerelease?")] + public CreateReleaseRequest WithPreRelease( + [Param(Name = "prerelease", Doc = "Mark release as prerelease?")] bool preRelease) + { + PreRelease = preRelease; + return this; + } + + [StarlarkMethod( + "set_generate_release_notes", + Doc = "Generate release notes?")] + public CreateReleaseRequest WithGenerateReleaseNotes( + [Param(Name = "generate_notes", Doc = "Generate notes?")] bool generateReleaseNotes) + { + GenerateReleaseNotes = generateReleaseNotes; + return this; + } + + public string? GetTagName() => TagName; + + public string? GetBody() => Body; + + public string? GetName() => Name; + + public string? GetTargetCommitish() => TargetCommitish; +} diff --git a/src/Copybara.Core/Git/GitHub/Api/CreateStatusRequest.cs b/src/Copybara.Core/Git/GitHub/Api/CreateStatusRequest.cs new file mode 100644 index 000000000..4f80dec6e --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/CreateStatusRequest.cs @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using Copybara.Common; + +namespace Copybara.Git.GitHub.Api; + +/// +/// Request for creating commit statuses: +/// +/// https://developer.github.com/v3/repos/statuses/#create-a-status +/// +public class CreateStatusRequest +{ + [JsonPropertyName("state")] + public StatusState State { get; set; } + + [JsonPropertyName("target_url")] + public string? TargetUrl { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("context")] + public string Context { get; set; } + + public CreateStatusRequest( + StatusState state, string? targetUrl, string? description, string context) + { + State = state; + TargetUrl = targetUrl; + Description = description; + Context = Preconditions.CheckNotNull(context); + } + + public StatusState GetState() => State; + + public string? GetTargetUrl() => TargetUrl; + + public string? GetDescription() => Description; + + public string GetContext() => Context; +} diff --git a/src/Copybara.Core/Git/GitHub/Api/GitHubApi.cs b/src/Copybara.Core/Git/GitHub/Api/GitHubApi.cs new file mode 100644 index 000000000..a42b4a05a --- /dev/null +++ b/src/Copybara.Core/Git/GitHub/Api/GitHubApi.cs @@ -0,0 +1,674 @@ +/* + * Copyright (C) 2016 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Text; +using Copybara.Common; +using Copybara.Exceptions; +using ConsoleT = Copybara.Util.Console.Console; +using ProfilerT = Copybara.Profiler.Profiler; + +namespace Copybara.Git.GitHub.Api; + +/// +/// A mini API for getting and updating GitHub projects through the GitHub REST API. +/// +public class GitHubApi +{ + private readonly IGitHubApiTransport _transport; + private readonly ProfilerT _profiler; + private readonly ConsoleT? _console; + + public const int MaxPerPage = 100; + private const int MaxPages = 10; + + public GitHubApi(IGitHubApiTransport transport, ProfilerT profiler) + : this(transport, profiler, null) + { + } + + public GitHubApi(IGitHubApiTransport transport, ProfilerT profiler, ConsoleT? console) + { + _transport = Preconditions.CheckNotNull(transport); + _profiler = Preconditions.CheckNotNull(profiler); + _console = console; + } + + /// Get all the pull requests for a project. + /// a project in the form of "google/copybara" + /// query parameters for the list operation + public Task> GetPullRequestsAsync( + string projectId, PullRequestListParams @params) + { + Preconditions.CheckNotNull(@params); + return PaginatedGetAsync>( + "github_api_list_pulls", + "Project", + ImmutableListMultimap.Empty, + string.Format( + "repos/{0}/pulls?per_page={1}{2}", projectId, MaxPerPage, @params.ToParams()), + "GET repos/%s/pulls"); + } + + /// + /// Get a specific pull request for a project. + /// + public async Task GetPullRequestAsync(string projectId, long number) + { + using ProfilerTaskScope ignore = Scope("github_api_get_pull"); + try + { + return (await _transport.GetAsync( + $"repos/{projectId}/pulls/{number}", "GET repos/%s/pulls/%d").ConfigureAwait(false))!; + } + catch (GitHubApiException e) + { + throw TreatGitHubException(e, "Pull Request"); + } + } + + /// Get comments for a specific pull request. + public async Task GetPullRequestCommentAsync(string projectId, long commentId) + { + using ProfilerTaskScope ignore = Scope("github_api_get_pull_comment"); + try + { + return (await _transport.GetAsync( + $"repos/{projectId}/pulls/comments/{commentId}", + "GET repos/%s/pulls/comments/%d").ConfigureAwait(false))!; + } + catch (GitHubApiException e) + { + throw TreatGitHubException(e, "Pull Request Comment"); + } + } + + /// Get comments for a specific pull request. + public Task> GetPullRequestCommentsAsync( + string projectId, long prNumber) + { + return PaginatedGetAsync>( + "github_api_get_reviews", + "Pull Request Comments", + ImmutableListMultimap.Empty, + $"repos/{projectId}/pulls/{prNumber}/comments?per_page={MaxPerPage}", + "GET repos/%s/pulls/%d/comments"); + } + + /// Get reviews for a pull request. + public Task> GetReviewsAsync(string projectId, long number) + { + return PaginatedGetAsync>( + "github_api_get_reviews", + "Pull Request or project", + ImmutableListMultimap.Empty, + $"repos/{projectId}/pulls/{number}/reviews?per_page={MaxPerPage}", + "GET repos/%s/pulls/%d/reviews"); + } + + private async Task> PaginatedGetAsync( + string profilerName, + string entity, + ImmutableListMultimap headers, + string path, + string requestTemplate) + where TResponse : class, IPaginatedPayload + { + var builder = ImmutableArray.CreateBuilder(); + int pages = 0; + string? currentPath = path; + while (currentPath != null && pages < MaxPages) + { + using ProfilerTaskScope ignore = Scope($"{profilerName}_page_{pages}"); + try + { + TResponse? response = + await _transport.GetAsync(currentPath, headers, requestTemplate) + .ConfigureAwait(false); + PaginatedList page = response!.GetPayload(); + builder.AddRange(page.GetElements()); + currentPath = page.NextUrl; + pages++; + } + catch (GitHubApiException e) + { + throw TreatGitHubException(e, entity); + } + } + + if (pages == MaxPages && _console != null && currentPath != null) + { + _console.WarnFmt( + "Copybara ran a paginated GET request {0} to GitHub for {1} pages, and that is the" + + " maximum number of pages Copybara will read. It is possible that additional pages" + + " were not read.", + currentPath, MaxPages); + } + + return builder.ToImmutable(); + } + + /// Create a pull request. + public async Task CreatePullRequestAsync(string projectId, CreatePullRequest request) + { + using ProfilerTaskScope ignore = Scope("github_api_create_pull"); + return (await _transport.PostAsync( + $"repos/{projectId}/pulls", request, "POST repos/%s/pulls").ConfigureAwait(false))!; + } + + /// + /// Adds assignees to an issue or pull request. + /// https://docs.github.com/en/rest/issues/assignees#add-assignees-to-an-issue + /// + public async Task AddAssigneesAsync(string projectId, long number, AddAssignees request) + { + using ProfilerTaskScope ignore = Scope("github_api_add_assignees"); + return (await _transport.PostAsync( + $"repos/{projectId}/issues/{number}/assignees", request, + "POST repos/%s/issues/%d/assignees").ConfigureAwait(false))!; + } + + /// Update a pull request. + public async Task UpdatePullRequestAsync( + string projectId, long number, UpdatePullRequest request) + { + using ProfilerTaskScope ignore = Scope("github_api_update_pull"); + return (await _transport.PostAsync( + $"repos/{projectId}/pulls/{number}", request, "POST repos/%s/pulls/%s") + .ConfigureAwait(false))!; + } + + /// + /// Listing issues and pull requests based on . + /// https://docs.github.com/en/rest/search#search-issues-and-pull-requests + /// + public async Task GetIssuesOrPullRequestsSearchResultsAsync( + IssuesAndPullRequestsSearchRequestParams @params) + { + using ProfilerTaskScope ignore = Scope("github_api_search_issues_or_pull_requests"); + return (await _transport.GetAsync( + $"search/issues?q={@params.ToParams()}", "GET search/issues?q=%s").ConfigureAwait(false))!; + } + + /// + /// Get a user's permission level. + /// https://developer.github.com/v3/repos/collaborators/#review-a-users-permission-level + /// + public async Task GetUserPermissionLevelAsync( + string projectId, string usrLogin) + { + using ProfilerTaskScope ignore = Scope("github_api_update_pull"); + return (await _transport.GetAsync( + $"repos/{projectId}/collaborators/{usrLogin}/permission", + "GET repos/%s/collaborators/%s/permission").ConfigureAwait(false))!; + } + + /// + /// Get authenticated User. https://developer.github.com/v3/users/#get-the-authenticated-user + /// + public async Task GetAuthenticatedUserAsync() + { + using ProfilerTaskScope ignore = Scope("github_api_get_authenticated_user"); + return (await _transport.GetAsync("user", "GET user").ConfigureAwait(false))!; + } + + /// + /// Get a specific issue for a project. + /// + /// Use this method to get the Pull Request labels. + /// + public async Task GetIssueAsync(string projectId, long number) + { + using ProfilerTaskScope ignore = Scope("github_api_get_issue"); + try + { + return (await _transport.GetAsync( + $"repos/{projectId}/issues/{number}", "GET repos/%s/issues/%d") + .ConfigureAwait(false))!; + } + catch (GitHubApiException e) + { + throw TreatGitHubException(e, "Issue"); + } + } + + /// Create an issue. + public async Task CreateIssueAsync(string projectId, Issue.CreateIssueRequest request) + { + using ProfilerTaskScope ignore = Scope("github_api_create_issue"); + return (await _transport.PostAsync( + $"repos/{projectId}/issues", request, "POST repos/%s/issues").ConfigureAwait(false))!; + } + + /// Get all the refs for a repo (git ls-remote). + public async Task> GetLsRemoteAsync(string projectId) + { + using ProfilerTaskScope ignore = Scope("github_api_list_refs"); + try + { + List? result = await _transport.GetAsync>( + $"repos/{projectId}/git/refs?per_page={MaxPerPage}", "GET repos/%s/git/refs") + .ConfigureAwait(false); + return (result ?? new List()).ToImmutableArray(); + } + catch (GitHubApiException e) + { + // Per https://developer.github.com/v3/git/, GH returns 409 - conflict if the repo is + // empty or in the process of being created. + if (e.GetResponseCode() == GitHubApiResponseCode.CONFLICT) + { + return ImmutableArray.Empty; + } + + throw; + } + } + + public async Task CreateStatusAsync( + string projectId, string sha1, CreateStatusRequest request) + { + using ProfilerTaskScope ignore = Scope("github_api_create_status"); + Status result = (await _transport.PostAsync( + $"repos/{projectId}/statuses/{sha1}", request, "Create status").ConfigureAwait(false))!; + if (result.GetContext() == null || result.StateRaw == null) + { + throw new RepoException( + "Something went wrong at the GitHub API transport level." + + $" Context: {result.GetContext()} state: {result.StateRaw}"); + } + + return result; + } + + public async Task UpdateReferenceAsync( + string projectId, string @ref, UpdateReferenceRequest request) + { + Preconditions.CheckArgument( + @ref.StartsWith("refs/", StringComparison.Ordinal), + "References has to be complete references in the form of refs/heads/foo. But was: {0}", + @ref); + using ProfilerTaskScope ignore = Scope("github_api_update_reference"); + Ref result = (await _transport.PostAsync( + $"repos/{projectId}/git/{@ref}", request, "POST repos/%s/git/%s").ConfigureAwait(false))!; + if (result.GetRef() == null || result.GetSha() == null || result.GetUrl() == null) + { + throw new RepoException( + "Something went wrong at the GitHub API transport level." + + $" ref: {result.GetRef()} sha: {result.GetSha()}, url: {result.GetUrl()}"); + } + + return result; + } + + public async Task DeleteReferenceAsync(string projectId, string @ref) + { + Preconditions.CheckArgument( + @ref.StartsWith("refs/", StringComparison.Ordinal), + "References has to be complete references in the form of refs/heads/foo. But was: {0}", + @ref); + // There is no good reason for deleting master. + Preconditions.CheckArgument( + @ref != "refs/heads/master", + "Copybara doesn't allow to delete master branch for security reasons"); + + using ProfilerTaskScope ignore = Scope("github_api_delete_reference"); + await _transport.DeleteAsync($"repos/{projectId}/git/{@ref}", "DELETE repos/%s/git/%s") + .ConfigureAwait(false); + } + + public async Task GetReferenceAsync(string projectId, string @ref) + { + using ProfilerTaskScope ignore = Scope("github_api_get_reference"); + ValidationException.CheckCondition( + @ref.StartsWith("refs/", StringComparison.Ordinal), "Ref must start with \"refs/\""); + return (await _transport.GetAsync( + $"repos/{projectId}/git/{@ref}", "GET repos/%s/git/%s").ConfigureAwait(false))!; + } + + public async Task GetRepositoryAsync(string projectId) + { + using ProfilerTaskScope ignore = Scope("github_api_get_repository"); + return (await _transport.GetAsync( + $"repos/{projectId}", "GET repos/%s").ConfigureAwait(false))!; + } + + public Task> GetReferencesAsync(string projectId) + { + using ProfilerTaskScope ignore = Scope("github_api_get_references"); + return PaginatedGetAsync>( + "github_api_get_references", + "Project", + ImmutableListMultimap.Empty, + $"repos/{projectId}/git/refs?per_page={MaxPerPage}", + "GET repos/%s/git/refs"); + } + + public async Task GetCombinedStatusAsync(string projectId, string @ref) + { + using ProfilerTaskScope ignore = Scope("github_api_get_combined_status"); + return (await _transport.GetAsync( + $"repos/{projectId}/commits/{@ref}/status?per_page={MaxPerPage}", + "GET repos/%s/commits/%s/status").ConfigureAwait(false))!; + } + + /// + /// Calls the GitHub API REST endpoint to list check runs for a specific ref. + /// + /// If is provided, only check runs with that name are + /// returned. + /// + public Task> GetCheckRunsAsync( + string projectId, string @ref, string? checkName) + { + using ProfilerTaskScope ignore = Scope("github_api_get_check_runs"); + var headers = ImmutableListMultimap.Of( + "Accept", "application/vnd.github.antiope-preview+json"); + if (string.IsNullOrEmpty(checkName)) + { + return PaginatedGetAsync( + "github_api_get_check_runs_get", + "Check Run", + headers, + $"repos/{projectId}/commits/{@ref}/check-runs?per_page={MaxPerPage}", + "GET repos/%s/commits/%s/check-runs"); + } + + return PaginatedGetAsync( + "github_api_get_check_runs_get", + "Check Run", + headers, + $"repos/{projectId}/commits/{@ref}/check-runs?per_page={MaxPerPage}&check_name={checkName}", + "GET repos/%s/commits/%s/check-runs"); + } + + /// + /// Calls the GitHub API REST endpoint to list check runs for a specific ref. + /// + public Task> GetCheckRunsAsync(string projectId, string @ref) => + GetCheckRunsAsync(projectId, @ref, checkName: null); + + /// + /// https://docs.github.com/en/rest/checks/suites/#list-check-suites-for-a-git-reference + /// + public Task> GetCheckSuitesAsync(string projectId, string @ref) + { + using ProfilerTaskScope ignore = Scope("github_api_get_check_suites"); + var headers = ImmutableListMultimap.Of( + "Accept", "application/vnd.github.antiope-preview+json"); + return PaginatedGetAsync( + "github_api_get_check_runs_get", + "Check Run", + headers, + $"repos/{projectId}/commits/{@ref}/check-suites?per_page={MaxPerPage}", + "GET repos/%s/commits/%s/check-suites"); + } + + public async Task GetCommitAsync(string projectId, string @ref) + { + using ProfilerTaskScope ignore = Scope("github_api_get_commit"); + return (await _transport.GetAsync( + $"repos/{projectId}/commits/{@ref}", "GET repos/%s/commits/%s").ConfigureAwait(false))!; + } + + /// https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue + public async Task> AddLabelsAsync( + string project, long prNumber, IReadOnlyList labels) + { + using ProfilerTaskScope ignore = Scope("github_api_add_labels"); + List