Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
016f0d6c3f
|
|||
|
147f6df1b3
|
|||
|
5c22250a63
|
|||
|
9dd30a1745
|
|||
|
f1eb883b93
|
|||
|
9631c62ee3
|
|||
|
6fe459c39e
|
|||
|
b9cf913528
|
|||
|
f9710e5992
|
|||
|
a26d8695f5
|
|||
|
20f430fb8f
|
|||
|
4420bd428a
|
|||
|
8d73287a60
|
|||
|
5bac7aa577
|
|||
|
47bad744d5
|
|||
|
2650bdc670
|
|||
|
a0f8611a76
|
|||
|
86a344fa9f
|
|||
|
56359f3488
|
|||
|
fbb4a22e98
|
|||
|
3cfd882f38
|
|||
|
a885e3dfa3
|
|||
|
04bfd4a6ae
|
|||
|
fb60c22257
|
|||
|
0148ad6df2
|
|||
|
322327d6d7
|
|||
|
9ea0ab566e
|
|||
|
4362ad9c3f
|
|||
|
1972260317
|
|||
|
8aa4b73cab
|
|||
|
f2a2374c4f
|
|||
|
7d23172c71
|
|||
|
11ec829a90
|
|||
|
2152388ee3
|
|||
|
0836dbd6da
|
|||
|
e2309f5635
|
|||
|
c7d349e3cc
|
|||
|
9b60ba0e6c
|
|||
|
c07a0ef13b
|
|||
|
d716348088
|
|||
|
9c62c06ebe
|
|||
|
84ac4a6a12
|
|||
|
0484f45d57
|
|||
|
7959ec274b
|
|||
|
ba7d63606c
|
|||
|
59bc977788
|
|||
|
b7ac6ee9d1
|
|||
|
2ca83829e0
|
|||
|
847ddbc7b5
|
17
.bump-version.json
Normal file
17
.bump-version.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"strategy" : {
|
||||||
|
"semvar" : {
|
||||||
|
"allowPreRelease" : true,
|
||||||
|
"strategy" : {
|
||||||
|
"gitTag" : {
|
||||||
|
"exactMatch" : false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target" : {
|
||||||
|
"module" : {
|
||||||
|
"name" : "BumpVersion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
.bump-version.pre.json
Normal file
27
.bump-version.pre.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"strategy" : {
|
||||||
|
"semvar" : {
|
||||||
|
"allowPreRelease" : true,
|
||||||
|
"preRelease" : {
|
||||||
|
"prefix" : "rc",
|
||||||
|
"strategy" : {
|
||||||
|
"gitTag" : {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requireExistingFile" : false,
|
||||||
|
"requireExistingSemVar" : false,
|
||||||
|
"strategy" : {
|
||||||
|
"gitTag" : {
|
||||||
|
"exactMatch" : false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target" : {
|
||||||
|
"module" : {
|
||||||
|
"name" : "BumpVersion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*.swift]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
20
.gitea/workflows/ci.yml
Normal file
20
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ubuntu:
|
||||||
|
name: Ubuntu
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup just.
|
||||||
|
uses: https://git.housh.dev/actions/setup-just.git@v1
|
||||||
|
- name: Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Setup buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Run tests.
|
||||||
|
run: just test-docker
|
||||||
17
.gitea/workflows/release.yaml
Normal file
17
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*.*.*"
|
||||||
|
- "v*.*.*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -7,27 +7,32 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
mac:
|
mac:
|
||||||
name: macOS
|
name: macOS
|
||||||
runs-on: macos-12
|
runs-on: macos-15
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
xcode: ['14.2']
|
xcode: ['16.1']
|
||||||
config: ['debug', 'release']
|
config: ['debug', 'release']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install just.
|
||||||
|
run: brew install just
|
||||||
- name: Select Xcode ${{ matrix.xcode }}
|
- name: Select Xcode ${{ matrix.xcode }}
|
||||||
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
|
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
|
||||||
- name: Swift Version
|
- name: Swift Version
|
||||||
run: swift --version
|
run: swift --version
|
||||||
- name: Run ${{ matrix.xcode }} Tests
|
- name: Run ${{ matrix.xcode }} Tests
|
||||||
run: make CONFIG=${{ matrix.config }} test-library
|
run: just test --configuration ${{ matrix.config }}
|
||||||
|
|
||||||
# ubuntu:
|
ubuntu:
|
||||||
# name: Ubuntu
|
name: Ubuntu
|
||||||
# runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
# steps:
|
steps:
|
||||||
# - uses: swift-actions/setup-swift@v1
|
- uses: actions/checkout@v4
|
||||||
# with:
|
- name: Setup just.
|
||||||
# swift-version: 5.7
|
uses: extractions/setup-just@v2
|
||||||
# - uses: actions/checkout@v3
|
- name: Setup QEMU
|
||||||
# - name: Run Tests
|
uses: docker/setup-qemu-action@v3
|
||||||
# run: make DOCKER_PLATFORM=linux/amd64 test-linux
|
- name: Setup buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Run tests.
|
||||||
|
run: just test-docker
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ DerivedData/
|
|||||||
.swiftpm/config/registries.json
|
.swiftpm/config/registries.json
|
||||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
.netrc
|
.netrc
|
||||||
|
.nvim/*
|
||||||
|
|||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
**/*.docc/*.md
|
||||||
2
.spi.yml
2
.spi.yml
@@ -1,4 +1,4 @@
|
|||||||
version: 1
|
version: 1
|
||||||
builder:
|
builder:
|
||||||
configs:
|
configs:
|
||||||
documentation_targets: [CliVersion]
|
- documentation_targets: [BumpVersion]
|
||||||
|
|||||||
11
.swiftformat
Normal file
11
.swiftformat
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
--self init-only
|
||||||
|
--indent 2
|
||||||
|
--ifdef indent
|
||||||
|
--trimwhitespace always
|
||||||
|
--wraparguments before-first
|
||||||
|
--wrapparameters before-first
|
||||||
|
--wrapcollections preserve
|
||||||
|
--wrapconditions after-first
|
||||||
|
--typeblanklines preserve
|
||||||
|
--commas inline
|
||||||
|
--stripunusedargs closure-only
|
||||||
11
.swiftlint.yml
Normal file
11
.swiftlint.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
disabled_rules:
|
||||||
|
- closing_brace
|
||||||
|
- fuction_body_length
|
||||||
|
- opening_brace
|
||||||
|
- nesting
|
||||||
|
|
||||||
|
included:
|
||||||
|
- Sources
|
||||||
|
- Tests
|
||||||
|
|
||||||
|
ignore_multiline_statement_conditions: true
|
||||||
@@ -90,13 +90,28 @@
|
|||||||
ReferencedContainer = "container:">
|
ReferencedContainer = "container:">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CliClient"
|
||||||
|
BuildableName = "CliClient"
|
||||||
|
BlueprintName = "CliClient"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
codeCoverageEnabled = "YES">
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO">
|
skipped = "NO">
|
||||||
@@ -108,6 +123,16 @@
|
|||||||
ReferencedContainer = "container:">
|
ReferencedContainer = "container:">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "ConfigurationClientTests"
|
||||||
|
BuildableName = "ConfigurationClientTests"
|
||||||
|
BlueprintName = "ConfigurationClientTests"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
|
|||||||
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ARG SWIFT_VERSION="6.0.3"
|
||||||
|
FROM swift:${SWIFT_VERSION}
|
||||||
|
WORKDIR /app
|
||||||
|
COPY ./Package.* ./
|
||||||
|
RUN swift package resolve
|
||||||
|
COPY . .
|
||||||
|
RUN swift build
|
||||||
|
CMD ["swift", "test"]
|
||||||
42
Makefile
42
Makefile
@@ -1,42 +0,0 @@
|
|||||||
PLATFORM_MACOS = macOS
|
|
||||||
CONFIG := debug
|
|
||||||
DOCC_TARGET ?= CliVersion
|
|
||||||
DOCC_BASEPATH = $(shell basename "$(PWD)")
|
|
||||||
DOCC_DIR ?= ./docs
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf .build
|
|
||||||
|
|
||||||
build-documentation:
|
|
||||||
swift package \
|
|
||||||
--allow-writing-to-directory "$(DOCC_DIR)" \
|
|
||||||
generate-documentation \
|
|
||||||
--target "$(DOCC_TARGET)" \
|
|
||||||
--disable-indexing \
|
|
||||||
--transform-for-static-hosting \
|
|
||||||
--hosting-base-path "$(DOCC_BASEPATH)" \
|
|
||||||
--output-path "$(DOCC_DIR)"
|
|
||||||
|
|
||||||
preview-documentation:
|
|
||||||
swift package \
|
|
||||||
--disable-sandbox \
|
|
||||||
preview-documentation \
|
|
||||||
--target "$(DOCC_TARGET)"
|
|
||||||
|
|
||||||
test-linux:
|
|
||||||
docker run --rm \
|
|
||||||
--volume "$(PWD):$(PWD)" \
|
|
||||||
--workdir "$(PWD)" \
|
|
||||||
swift:5.7-focal \
|
|
||||||
swift test
|
|
||||||
|
|
||||||
test-library:
|
|
||||||
swift test -c $(CONFIG)
|
|
||||||
|
|
||||||
update-version:
|
|
||||||
swift package \
|
|
||||||
--disable-sandbox \
|
|
||||||
--allow-writing-to-package-directory \
|
|
||||||
update-version \
|
|
||||||
git-version
|
|
||||||
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"originHash" : "9fe004cf869b34d1fe07e8b58a90b044281e8e94805df6723d4604ba5a6400d9",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "combine-schedulers",
|
"identity" : "combine-schedulers",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb",
|
"revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13",
|
||||||
"version" : "1.0.0"
|
"version" : "1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23,8 +24,17 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-argument-parser.git",
|
"location" : "https://github.com/apple/swift-argument-parser.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
|
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
|
||||||
"version" : "1.2.3"
|
"version" : "1.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-cli-doc",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/m-housh/swift-cli-doc.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "bbace73d974fd3e6985461431692bea773c7c5d8",
|
||||||
|
"version" : "0.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,8 +42,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/swift-clocks",
|
"location" : "https://github.com/pointfreeco/swift-clocks",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb",
|
"revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53",
|
||||||
"version" : "1.0.0"
|
"version" : "1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -41,8 +51,17 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "ea631ce892687f5432a833312292b80db238186a",
|
"revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8",
|
||||||
"version" : "1.0.0"
|
"version" : "1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-custom-dump",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-custom-dump.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
||||||
|
"version" : "1.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -50,8 +69,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/swift-dependencies.git",
|
"location" : "https://github.com/pointfreeco/swift-dependencies.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5",
|
"revision" : "5526c8a27675dc7b18d6fa643abfb64bcb200b77",
|
||||||
"version" : "1.0.0"
|
"version" : "1.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,14 +78,14 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-docc-plugin.git",
|
"location" : "https://github.com/apple/swift-docc-plugin.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "26ac5758409154cc448d7ab82389c520fa8a8247",
|
"revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64",
|
||||||
"version" : "1.3.0"
|
"version" : "1.4.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-docc-symbolkit",
|
"identity" : "swift-docc-symbolkit",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-docc-symbolkit",
|
"location" : "https://github.com/swiftlang/swift-docc-symbolkit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
|
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
|
||||||
"version" : "1.0.0"
|
"version" : "1.0.0"
|
||||||
@@ -77,8 +96,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-log.git",
|
"location" : "https://github.com/apple/swift-log.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed",
|
"revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91",
|
||||||
"version" : "1.5.3"
|
"version" : "1.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -95,8 +114,17 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/m-housh/swift-shell-client.git",
|
"location" : "https://github.com/m-housh/swift-shell-client.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "9c0c4757d0a2e313d1a4a28e60418a753d3e4230",
|
"revision" : "d67f693f92428ef8adecb48c2b26ccac0d2f98cb",
|
||||||
"version" : "0.1.4"
|
"version" : "0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-syntax",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swiftlang/swift-syntax",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "0687f71944021d616d34d922343dcef086855920",
|
||||||
|
"version" : "600.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,10 +132,10 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631",
|
"revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1",
|
||||||
"version" : "1.0.2"
|
"version" : "1.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 2
|
"version" : 3
|
||||||
}
|
}
|
||||||
|
|||||||
109
Package.swift
109
Package.swift
@@ -1,76 +1,119 @@
|
|||||||
// swift-tools-version: 5.6
|
// swift-tools-version: 6.0
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "swift-cli-version",
|
name: "swift-cli-version",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v10_15)
|
.macOS(.v13)
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(name: "CliVersion", targets: ["CliVersion"]),
|
.executable(name: "bump-version", targets: ["BumpVersion"]),
|
||||||
|
.library(name: "CliClient", targets: ["CliClient"]),
|
||||||
|
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
|
||||||
|
.library(name: "FileClient", targets: ["FileClient"]),
|
||||||
|
.library(name: "GitClient", targets: ["GitClient"]),
|
||||||
|
.library(name: "LoggingExtensions", targets: ["LoggingExtensions"]),
|
||||||
.plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]),
|
.plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]),
|
||||||
.plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]),
|
.plugin(name: "BumpVersionPlugin", targets: ["BumpVersionPlugin"])
|
||||||
.plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"])
|
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.1.3"),
|
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.2"),
|
||||||
|
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.2.2"),
|
||||||
|
.package(url: "https://github.com/m-housh/swift-cli-doc.git", from: "0.2.1"),
|
||||||
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
|
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2")
|
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
|
||||||
|
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
|
||||||
|
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.3.3")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "cli-version",
|
name: "BumpVersion",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"CliVersion",
|
"CliClient",
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser")
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
|
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||||
|
.product(name: "CliDoc", package: "swift-cli-doc")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "CliVersion",
|
name: "CliClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "ShellClient", package: "swift-shell-client")
|
"ConfigurationClient",
|
||||||
|
"FileClient",
|
||||||
|
"GitClient",
|
||||||
|
"LoggingExtensions",
|
||||||
|
.product(name: "Logging", package: "swift-log"),
|
||||||
|
.product(name: "CustomDump", package: "swift-custom-dump")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "CliVersionTests",
|
name: "CliVersionTests",
|
||||||
dependencies: ["CliVersion"]
|
dependencies: ["CliClient", "TestSupport"]
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "ConfigurationClient",
|
||||||
|
dependencies: [
|
||||||
|
"FileClient",
|
||||||
|
"LoggingExtensions",
|
||||||
|
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "ConfigurationClientTests",
|
||||||
|
dependencies: ["ConfigurationClient", "TestSupport"]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "FileClient",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "GitClient",
|
||||||
|
dependencies: [
|
||||||
|
"FileClient",
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "GitClientTests",
|
||||||
|
dependencies: ["GitClient"]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "LoggingExtensions",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(name: "TestSupport"),
|
||||||
.plugin(
|
.plugin(
|
||||||
name: "BuildWithVersionPlugin",
|
name: "BuildWithVersionPlugin",
|
||||||
capability: .buildTool(),
|
capability: .buildTool(),
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"cli-version"
|
"BumpVersion"
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.plugin(
|
.plugin(
|
||||||
name: "GenerateVersionPlugin",
|
name: "BumpVersionPlugin",
|
||||||
capability: .command(
|
capability: .command(
|
||||||
intent: .custom(
|
intent: .custom(
|
||||||
verb: "generate-version",
|
verb: "bump-version",
|
||||||
description: "Generates a version file in the given target."
|
description: "Bumps a version file in the given target."
|
||||||
),
|
|
||||||
permissions: [
|
|
||||||
.writeToPackageDirectory(reason: "Generate a version file in the target's directory.")
|
|
||||||
]
|
|
||||||
),
|
|
||||||
dependencies: [
|
|
||||||
"cli-version"
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.plugin(
|
|
||||||
name: "UpdateVersionPlugin",
|
|
||||||
capability: .command(
|
|
||||||
intent: .custom(
|
|
||||||
verb: "update-version",
|
|
||||||
description: "Updates a version file in the given target."
|
|
||||||
),
|
),
|
||||||
permissions: [
|
permissions: [
|
||||||
.writeToPackageDirectory(reason: "Update a version file in the target's directory.")
|
.writeToPackageDirectory(reason: "Update a version file in the target's directory.")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"cli-version"
|
"BumpVersion"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,24 +7,28 @@ struct GenerateVersionBuildPlugin: BuildToolPlugin {
|
|||||||
context: PackagePlugin.PluginContext,
|
context: PackagePlugin.PluginContext,
|
||||||
target: PackagePlugin.Target
|
target: PackagePlugin.Target
|
||||||
) async throws -> [PackagePlugin.Command] {
|
) async throws -> [PackagePlugin.Command] {
|
||||||
guard let target = target as? SourceModuleTarget else { return [] }
|
guard let target = target as? SwiftSourceModuleTarget else { return [] }
|
||||||
|
|
||||||
let gitDirectoryPath = target.directory
|
let gitDirectoryPath = target.directoryURL
|
||||||
.removingLastComponent()
|
.deletingLastPathComponent()
|
||||||
.removingLastComponent()
|
.deletingLastPathComponent()
|
||||||
|
|
||||||
let tool = try context.tool(named: "cli-version")
|
let tool = try context.tool(named: "bump-version")
|
||||||
let outputPath = context.pluginWorkDirectory
|
let outputPath = context.pluginWorkDirectoryURL
|
||||||
|
|
||||||
let outputFile = outputPath.appending("Version.swift")
|
let outputFile = outputPath.appending(path: "Version.swift")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
.buildCommand(
|
.buildCommand(
|
||||||
displayName: "Build With Version Plugin",
|
displayName: "Build with Version Plugin",
|
||||||
executable: tool.path,
|
executable: tool.url,
|
||||||
arguments: ["build", "--verbose", "--git-directory", gitDirectoryPath.string, outputPath.string],
|
arguments: [
|
||||||
|
"build", "--verbose",
|
||||||
|
"--git-directory", gitDirectoryPath.absoluteString,
|
||||||
|
"--target-file-path", outputPath.absoluteString
|
||||||
|
],
|
||||||
environment: [:],
|
environment: [:],
|
||||||
inputFiles: target.sourceFiles.map(\.path),
|
inputFiles: target.sourceFiles.map(\.url),
|
||||||
outputFiles: [outputFile]
|
outputFiles: [outputFile]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
24
Plugins/BumpVersionPlugin/BumpVersionPlugin.swift
Normal file
24
Plugins/BumpVersionPlugin/BumpVersionPlugin.swift
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Foundation
|
||||||
|
import PackagePlugin
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct BumpVersionPlugin: CommandPlugin {
|
||||||
|
|
||||||
|
func performCommand(context: PluginContext, arguments: [String]) async throws {
|
||||||
|
print("Starting bump-version plugin")
|
||||||
|
let tool = try context.tool(named: "bump-version")
|
||||||
|
|
||||||
|
print("arguments: \(arguments)")
|
||||||
|
|
||||||
|
let process = Process()
|
||||||
|
process.executableURL = tool.url
|
||||||
|
process.arguments = arguments
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
|
||||||
|
guard process.terminationReason == .exit && process.terminationStatus == 0 else {
|
||||||
|
Diagnostics.error("Reason: \(process.terminationReason), status: \(process.terminationStatus)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import PackagePlugin
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct GenerateVersionPlugin: CommandPlugin {
|
|
||||||
|
|
||||||
func performCommand(context: PluginContext, arguments: [String]) async throws {
|
|
||||||
let gitVersion = try context.tool(named: "cli-version")
|
|
||||||
|
|
||||||
let arguments = ["generate"] + arguments
|
|
||||||
|
|
||||||
for target in context.package.targets {
|
|
||||||
guard let target = target as? SourceModuleTarget,
|
|
||||||
arguments.first(where: { $0.contains(target.name) }) != nil
|
|
||||||
else { continue }
|
|
||||||
|
|
||||||
let process = Process()
|
|
||||||
process.executableURL = URL(fileURLWithPath: gitVersion.path.string)
|
|
||||||
process.arguments = arguments
|
|
||||||
try process.run()
|
|
||||||
process.waitUntilExit()
|
|
||||||
|
|
||||||
guard process.terminationReason == .exit && process.terminationStatus == 0 else {
|
|
||||||
Diagnostics.error("Reason: \(process.terminationReason), status: \(process.terminationStatus)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import PackagePlugin
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct UpdateVersionPlugin: CommandPlugin {
|
|
||||||
|
|
||||||
func performCommand(context: PluginContext, arguments: [String]) async throws {
|
|
||||||
let gitVersion = try context.tool(named: "cli-version")
|
|
||||||
|
|
||||||
let arguments = ["update"] + arguments
|
|
||||||
|
|
||||||
for target in context.package.targets {
|
|
||||||
guard let target = target as? SourceModuleTarget,
|
|
||||||
arguments.first(where: { $0.contains(target.name) }) != nil
|
|
||||||
else { continue }
|
|
||||||
|
|
||||||
let process = Process()
|
|
||||||
process.executableURL = URL(fileURLWithPath: gitVersion.path.string)
|
|
||||||
process.arguments = arguments
|
|
||||||
try process.run()
|
|
||||||
process.waitUntilExit()
|
|
||||||
|
|
||||||
guard process.terminationReason == .exit && process.terminationStatus == 0 else {
|
|
||||||
Diagnostics.error("Reason: \(process.terminationReason), status: \(process.terminationStatus)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
Release.md
Normal file
26
Release.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Release Workflow Steps
|
||||||
|
|
||||||
|
This is a reminder of the steps used to create a release and update the homebrew formula.
|
||||||
|
|
||||||
|
> Note: These steps apply to the version hosted on `gitea`, on `github` more of these steps can be
|
||||||
|
> automated in `ci`, but there are no `macOS` host runners currently in `gitea`, so the bottles need
|
||||||
|
> built on `macOS`.
|
||||||
|
|
||||||
|
1. Update the version in `Sources/bump-version/Version.swift`.
|
||||||
|
1. Tag the commit with the next version tag.
|
||||||
|
1. Push the tagged commit, this will initiate the release being created.
|
||||||
|
1. Get the `sha` of the `*.tar.gz` in the release.
|
||||||
|
1. `just get-release-sha`
|
||||||
|
1. Update the homebrew formula url, sha256, and version at top of the homebrew formula.
|
||||||
|
1. `cd $(brew --repo michael/formula)`
|
||||||
|
1. Create a branch for a pull-request `git checkout -b bump-version`
|
||||||
|
1. `n Formula/bump-version.rb`
|
||||||
|
1. Build and generate a homebrew bottle.
|
||||||
|
1. `just bottle`
|
||||||
|
1. Update the `bottle do` section of homebrew formula with output during previous step.
|
||||||
|
1. Also make sure the `root_url` in the bottle section points to the new release.
|
||||||
|
1. Upload the bottle `*.tar.gz` file that was created to the release.
|
||||||
|
1. Generate a pull-request to the formula repo.
|
||||||
|
1. Generate a pull-request to this repo to merge into main.
|
||||||
|
1. Remove the bottle from current directory.
|
||||||
|
1. `just remove-bottles`
|
||||||
19
Sources/BumpVersion/Application.swift
Normal file
19
Sources/BumpVersion/Application.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct Application: AsyncParsableCommand {
|
||||||
|
static let commandName = "bump-version"
|
||||||
|
|
||||||
|
static let configuration: CommandConfiguration = .init(
|
||||||
|
commandName: commandName,
|
||||||
|
version: VERSION ?? "0.0.0",
|
||||||
|
subcommands: [
|
||||||
|
BuildCommand.self,
|
||||||
|
BumpCommand.self,
|
||||||
|
GenerateCommand.self,
|
||||||
|
ConfigCommand.self
|
||||||
|
],
|
||||||
|
defaultSubcommand: BumpCommand.self
|
||||||
|
)
|
||||||
|
}
|
||||||
25
Sources/BumpVersion/Commands/BuildCommand.swift
Normal file
25
Sources/BumpVersion/Commands/BuildCommand.swift
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
|
import Foundation
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
// NOTE: This command is only used with the build with version plugin.
|
||||||
|
struct BuildCommand: AsyncParsableCommand {
|
||||||
|
static let commandName = "build"
|
||||||
|
|
||||||
|
static let configuration: CommandConfiguration = .init(
|
||||||
|
commandName: Self.commandName,
|
||||||
|
abstract: Abstract.default("Used for the build with version plugin.").render(),
|
||||||
|
discussion: Discussion {
|
||||||
|
"This should generally not be interacted with directly, outside of the build plugin."
|
||||||
|
},
|
||||||
|
shouldDisplay: false
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
try await globals.run(\.build, command: Self.commandName)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Sources/BumpVersion/Commands/BumpCommand.swift
Normal file
40
Sources/BumpVersion/Commands/BumpCommand.swift
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
|
import Dependencies
|
||||||
|
|
||||||
|
struct BumpCommand: CommandRepresentable {
|
||||||
|
|
||||||
|
static let commandName = "bump"
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: Self.commandName,
|
||||||
|
abstract: Abstract.default("Bump version of a command-line tool."),
|
||||||
|
usage: Usage.default(commandName: nil),
|
||||||
|
discussion: Discussion.default(examples: [
|
||||||
|
makeExample(
|
||||||
|
label: "Basic usage, bump the minor version.",
|
||||||
|
example: "--minor"
|
||||||
|
),
|
||||||
|
makeExample(
|
||||||
|
label: "Dry run, just show what the bumped version would be.",
|
||||||
|
example: "--minor --print"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
help: """
|
||||||
|
The semvar bump option, this is ignored if the configuration is set to use a branch/commit sha strategy.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var bumpOption: CliClient.BumpOption = .patch
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
try await globals.run(\.bump, command: Self.commandName, args: bumpOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliClient.BumpOption: EnumerableFlag {}
|
||||||
230
Sources/BumpVersion/Commands/ConfigCommand.swift
Normal file
230
Sources/BumpVersion/Commands/ConfigCommand.swift
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
|
import ConfigurationClient
|
||||||
|
import CustomDump
|
||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ConfigCommand: AsyncParsableCommand {
|
||||||
|
static let commandName = "config"
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: Abstract.default("Configuration commands").render(),
|
||||||
|
subcommands: [
|
||||||
|
DumpConfig.self,
|
||||||
|
GenerateConfig.self
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfigCommand {
|
||||||
|
|
||||||
|
struct DumpConfig: CommandRepresentable {
|
||||||
|
static let commandName = "dump"
|
||||||
|
static let parentCommand = ConfigCommand.commandName
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: Self.commandName,
|
||||||
|
abstract: Abstract.default("Inspect the parsed configuration."),
|
||||||
|
usage: Usage.default(parentCommand: ConfigCommand.commandName, commandName: Self.commandName),
|
||||||
|
discussion: Discussion.default(
|
||||||
|
notes: [
|
||||||
|
"""
|
||||||
|
The default style is to print the output in `json`, however you can use the `--swift` flag to
|
||||||
|
print the output in `swift`.
|
||||||
|
"""
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
makeExample(label: "Show the project configuration.", example: ""),
|
||||||
|
makeExample(
|
||||||
|
label: "Update a configuration file with the dumped output",
|
||||||
|
example: "--disable-pre-release > .bump-version.prod.json"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
"""
|
||||||
|
Loads the project configuration file (if applicable) and merges the options passed in,
|
||||||
|
then prints the configuration to stdout.
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
aliases: ["d"]
|
||||||
|
)
|
||||||
|
@Flag(
|
||||||
|
help: "Change the style of what get's printed."
|
||||||
|
)
|
||||||
|
fileprivate var printStyle: PrintStyle = .json
|
||||||
|
|
||||||
|
@OptionGroup var globals: ConfigCommandOptions
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
let configuration = try await globals
|
||||||
|
.shared(command: Self.commandName)
|
||||||
|
.runClient(\.parsedConfiguration)
|
||||||
|
|
||||||
|
try globals.printConfiguration(configuration, style: printStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GenerateConfig: CommandRepresentable {
|
||||||
|
static let commandName = "generate"
|
||||||
|
static let parentCommand = ConfigCommand.commandName
|
||||||
|
|
||||||
|
static let configuration: CommandConfiguration = .init(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: Abstract.default("Generate a configuration file, based on the given options.").render(),
|
||||||
|
usage: Usage.default(parentCommand: ConfigCommand.commandName, commandName: commandName),
|
||||||
|
discussion: Discussion.default(examples: [
|
||||||
|
makeExample(
|
||||||
|
label: "Generate a configuration file for the 'foo' target.",
|
||||||
|
example: "-m foo"
|
||||||
|
),
|
||||||
|
makeExample(
|
||||||
|
label: "Show the output and don't write to a file.",
|
||||||
|
example: "-m foo --print"
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
aliases: ["g"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
help: "The style of the configuration."
|
||||||
|
)
|
||||||
|
var style: ConfigCommand.Style = .semvar
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .customLong("print"),
|
||||||
|
help: "Print json to stdout."
|
||||||
|
)
|
||||||
|
var printJson: Bool = false
|
||||||
|
|
||||||
|
@OptionGroup var globals: ConfigCommandOptions
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
try await withSetupDependencies {
|
||||||
|
@Dependency(\.configurationClient) var configurationClient
|
||||||
|
|
||||||
|
let configuration = try style.parseConfiguration(
|
||||||
|
configOptions: globals.configOptions,
|
||||||
|
extraOptions: globals.extraOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
switch printJson {
|
||||||
|
case true:
|
||||||
|
try globals.handlePrintJson(configuration)
|
||||||
|
case false:
|
||||||
|
let url = globals.configFileUrl
|
||||||
|
try await configurationClient.write(configuration, url)
|
||||||
|
print(url.cleanFilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfigCommand {
|
||||||
|
enum Style: EnumerableFlag {
|
||||||
|
case branch, semvar
|
||||||
|
|
||||||
|
func parseConfiguration(
|
||||||
|
configOptions: ConfigurationOptions,
|
||||||
|
extraOptions: [String]
|
||||||
|
) throws -> Configuration {
|
||||||
|
let strategy: Configuration.VersionStrategy
|
||||||
|
|
||||||
|
switch self {
|
||||||
|
case .branch:
|
||||||
|
strategy = .branch(includeCommitSha: configOptions.commitSha)
|
||||||
|
case .semvar:
|
||||||
|
strategy = try .semvar(configOptions.semvarOptions(extraOptions: extraOptions))
|
||||||
|
}
|
||||||
|
|
||||||
|
return try Configuration(
|
||||||
|
target: configOptions.target(),
|
||||||
|
strategy: strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@dynamicMemberLookup
|
||||||
|
struct ConfigCommandOptions: ParsableArguments {
|
||||||
|
|
||||||
|
@OptionGroup var configOptions: ConfigurationOptions
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: "Increase logging level, can be passed multiple times (example: -vvv)."
|
||||||
|
)
|
||||||
|
var verbose: Int
|
||||||
|
|
||||||
|
@Argument(
|
||||||
|
help: """
|
||||||
|
Arguments / options used for custom pre-release, options / flags must proceed a '--' in
|
||||||
|
the command. These are ignored if the `--custom-command` or `--custom-pre-release` flag is not set.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var extraOptions: [String] = []
|
||||||
|
|
||||||
|
subscript<T>(dynamicMember keyPath: KeyPath<ConfigurationOptions, T>) -> T {
|
||||||
|
configOptions[keyPath: keyPath]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConfigCommand.DumpConfig {
|
||||||
|
enum PrintStyle: EnumerableFlag {
|
||||||
|
case json, swift
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConfigCommand.ConfigCommandOptions {
|
||||||
|
|
||||||
|
func shared(command: String) throws -> CliClient.SharedOptions {
|
||||||
|
try configOptions.shared(
|
||||||
|
command: command,
|
||||||
|
extraOptions: extraOptions,
|
||||||
|
verbose: verbose
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePrintJson(_ configuration: Configuration) throws {
|
||||||
|
@Dependency(\.coders) var coders
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
let data = try coders.jsonEncoder().encode(configuration)
|
||||||
|
guard let string = String(bytes: data, encoding: .utf8) else {
|
||||||
|
logger.error("Error encoding configuration to json.")
|
||||||
|
throw ConfigurationEncodingError()
|
||||||
|
}
|
||||||
|
print(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printConfiguration(
|
||||||
|
_ configuration: Configuration,
|
||||||
|
style: ConfigCommand.DumpConfig.PrintStyle
|
||||||
|
) throws {
|
||||||
|
switch style {
|
||||||
|
case .json:
|
||||||
|
try handlePrintJson(configuration)
|
||||||
|
case .swift:
|
||||||
|
customDump(configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConfigurationOptions {
|
||||||
|
var configFileUrl: URL {
|
||||||
|
switch configurationFile {
|
||||||
|
case let .some(path):
|
||||||
|
return URL(filePath: path)
|
||||||
|
case .none:
|
||||||
|
return URL(filePath: ".bump-version.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfigurationEncodingError: Error {}
|
||||||
29
Sources/BumpVersion/Commands/GenerateCommand.swift
Normal file
29
Sources/BumpVersion/Commands/GenerateCommand.swift
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
struct GenerateCommand: CommandRepresentable {
|
||||||
|
static let commandName = "generate"
|
||||||
|
|
||||||
|
static let configuration: CommandConfiguration = .init(
|
||||||
|
commandName: Self.commandName,
|
||||||
|
abstract: Abstract.default("Generates a version file in your project."),
|
||||||
|
usage: Usage.default(commandName: Self.commandName),
|
||||||
|
discussion: Discussion.default(
|
||||||
|
examples: [
|
||||||
|
makeExample(label: "Basic usage.", example: "")
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
"This command can be interacted with directly, outside of the plugin usage context."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
try await globals.run(\.generate, command: Self.commandName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Basic Configuration
|
||||||
|
|
||||||
|
Basic configuration examples.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Generating a configuration file for your application is the easiest way to use the command-line
|
||||||
|
tool. The configuration specifies the location of the version file, either by a path to the file or
|
||||||
|
by the module that a `Version.swift` file resides in. It also declares the strategy for generating
|
||||||
|
new versions.
|
||||||
|
|
||||||
|
The command-line tool comes with a command to generate the configuration file for you, this should
|
||||||
|
be ran from the root of your project or by specifying the path to write the configuration file to
|
||||||
|
using the `-f | --configuration-file` option. The below examples assume that you're running in the
|
||||||
|
root project directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version config generate --target-module my-tool
|
||||||
|
```
|
||||||
|
|
||||||
|
The above command produces the following in a file named `.bump-version.json` with the generated
|
||||||
|
default settings. This will generate semvar style version (example: `1.0.0`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategy": {
|
||||||
|
"semvar": {
|
||||||
|
"allowPreRelease": true,
|
||||||
|
"strategy": {
|
||||||
|
"gitTag": {
|
||||||
|
"exactMatch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"module": {
|
||||||
|
"name": "my-tool"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: The above does not add a pre-release strategy although it "allows" it if you pass an option
|
||||||
|
> to command later, if you set "allowPreRelease" to false it will ignore any attempts to add a
|
||||||
|
> pre-release strategy when bumping the version.
|
||||||
|
|
||||||
|
Most commands accept the same options for configuration as the above `config generate` command.
|
||||||
|
Those get merged with your project configuration when calling a command, that allows you to override
|
||||||
|
any of your defaults depending on your use case. You can also generate several configuration files
|
||||||
|
and specify them by passing the `-f | --configuration-file` to the command.
|
||||||
|
|
||||||
|
## Inspecting parsed configuration.
|
||||||
|
|
||||||
|
You can inspect the configuration that get's parsed by using the `config dump` command. The dump
|
||||||
|
command will print the parsed `json` to `stdout`, which can be helpful in confirming that your
|
||||||
|
configuration is valid and does not work unexpectedly.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version config dump <options / overrides>
|
||||||
|
```
|
||||||
|
|
||||||
|
The dump command can also be used to generate a different configuration that is merged with your
|
||||||
|
default.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version config dump --pre-release-git-tag-style > .bump-version.prerelease.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Which would produce the following in `.bump-version.prerelease.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategy": {
|
||||||
|
"semvar": {
|
||||||
|
"allowPreRelease": true,
|
||||||
|
"preRelease": {
|
||||||
|
"strategy": {
|
||||||
|
"gitTag": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategy": {
|
||||||
|
"gitTag": {
|
||||||
|
"exactMatch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"module": {
|
||||||
|
"name": "my-tool"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You could then use this file when bumping your version.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version bump -f .bump-version.prerelease.json
|
||||||
|
```
|
||||||
|
|
||||||
|
> See Also: <doc:ConfigurationReference>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Command Reference
|
||||||
|
|
||||||
|
Learn about the provided commands.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The command-line tool provides the following commands.
|
||||||
|
|
||||||
|
> See Also:
|
||||||
|
>
|
||||||
|
> 1. <doc:BasicConfiguration>
|
||||||
|
> 1. <doc:ConfigurationReference>
|
||||||
|
|
||||||
|
All commands output the path of the file they generate or write to, to allow them to be piped into
|
||||||
|
other commands, however this will not work if you specify `--verbose` because then other output is
|
||||||
|
also written to `stdout`.
|
||||||
|
|
||||||
|
### Bump Command
|
||||||
|
|
||||||
|
This bumps the version to the next version based on the project configuration or passed in options.
|
||||||
|
This is the default command when calling the `bump-version` tool, so specifying the `bump` command
|
||||||
|
is not required, but will be shown in examples below for clarity.
|
||||||
|
|
||||||
|
> See Also: <doc:OptionsReference>
|
||||||
|
|
||||||
|
The following options are used to declare which part of a semvar to bump to the next version, they
|
||||||
|
are ignored if your configuration or options specify to use a `branch` strategy.
|
||||||
|
|
||||||
|
| Long | Description |
|
||||||
|
| ------------- | --------------------------------------- |
|
||||||
|
| --major | Bump the major portion of the semvar |
|
||||||
|
| --minor | Bump the minor portion of the semvar |
|
||||||
|
| --patch | Bump the patch portion of the semvar |
|
||||||
|
| --pre-release | Bump the pre-release suffix of a semvar |
|
||||||
|
|
||||||
|
#### Bump Command Usage Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version bump --minor
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to use the default configuration without generating your own project configuration, then
|
||||||
|
you can specify the path or module to the bump command. The default configuration will use the
|
||||||
|
`gitTag` strategy without any pre-release strategy.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version bump --minor --target-module my-tool
|
||||||
|
```
|
||||||
|
|
||||||
|
Show the output, but don't update the version file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version bump --major --print
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate Command
|
||||||
|
|
||||||
|
This generates a version file based on your configuration, setting it's initial value based on your
|
||||||
|
projects configuration strategy. This is generally only ran once after setting up a project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Commands
|
||||||
|
|
||||||
|
The following commands are used to work with project configuration.
|
||||||
|
|
||||||
|
#### Generate Command
|
||||||
|
|
||||||
|
Generates a configuration file based on the passed in options.
|
||||||
|
|
||||||
|
> See Also: <doc:OptionsReference>
|
||||||
|
|
||||||
|
The following options are used to declare strategy used for deriving the version.
|
||||||
|
|
||||||
|
| Long | Description |
|
||||||
|
| -------- | --------------------------------- |
|
||||||
|
| --branch | Use the branch strategy |
|
||||||
|
| --semvar | Use the semvar strategy (default) |
|
||||||
|
|
||||||
|
##### Generate Configuration Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version config generate -m my-tool
|
||||||
|
```
|
||||||
|
|
||||||
|
The above generates a configuration file using the default version strategy for a target module
|
||||||
|
named 'my-tool'.
|
||||||
|
|
||||||
|
#### Dump Command
|
||||||
|
|
||||||
|
Dumps the parsed configuration to `stdout`.
|
||||||
|
|
||||||
|
> See Also: <doc:OptionsReference>
|
||||||
|
|
||||||
|
The following options are used to declare what output gets printed.
|
||||||
|
|
||||||
|
| Long | Description |
|
||||||
|
| ------- | -------------------- |
|
||||||
|
| --json | Print json (default) |
|
||||||
|
| --swift | Print swift struct |
|
||||||
|
|
||||||
|
##### Dump Configuration Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version config dump --disable-pre-release
|
||||||
|
```
|
||||||
|
|
||||||
|
This command can also be used to extend a configuration file with new configuration by sending the
|
||||||
|
output to a new file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bump-version config dump --disable-pre-release > .bump-version.prod.json
|
||||||
|
```
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# Configuration Reference
|
||||||
|
|
||||||
|
Learn about the configuration.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
The target declares where a version file lives that can be bumped by the command-line tool.
|
||||||
|
|
||||||
|
A target can be specified either as a path from the root of the project to a file that contains the
|
||||||
|
version or as a module that contains the version file.
|
||||||
|
|
||||||
|
> Note: A version file should not contain any other code aside from the version as the entire file
|
||||||
|
> contents get over written when bumping the version.
|
||||||
|
|
||||||
|
### Target - Path Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"target": {
|
||||||
|
"path": "Sources/my-tool/Version.swift"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Target - Module Example
|
||||||
|
|
||||||
|
When using the module style a file name is not required if you use the default file name of
|
||||||
|
`Version.swift`, however it can be customized in your target specification.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"target": {
|
||||||
|
"module": {
|
||||||
|
"fileName": "CustomVersion.swift",
|
||||||
|
"name": "my-tool"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above will parse the path to the file as `Sources/my-tool/CustomVersion.swift`.
|
||||||
|
|
||||||
|
## Strategy
|
||||||
|
|
||||||
|
The strategy declares how to generate the next version of your project. There are currently two
|
||||||
|
strategies, `branch` and `semvar`, that we will discuss.
|
||||||
|
|
||||||
|
### Branch Strategy
|
||||||
|
|
||||||
|
This is the most basic strategy, which will derive the version via the git branch and optionally the
|
||||||
|
short version of the commit sha.
|
||||||
|
|
||||||
|
An example of this style may look like: `main-8d73287a60`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategy": {
|
||||||
|
"branch": {
|
||||||
|
"includeCommitSha": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you set `'"includeCommitSha" : false'` then only the branch name will be used.
|
||||||
|
|
||||||
|
### Semvar Strategy
|
||||||
|
|
||||||
|
This is the most common strategy to use. It has support for generating the next version using either
|
||||||
|
`gitTag` or a custom `command` strategy.
|
||||||
|
|
||||||
|
#### Git Tag Strategy
|
||||||
|
|
||||||
|
The `gitTag` strategy derives the next version using the output of `git describe --tags` command.
|
||||||
|
This requires a commit to have a semvar style tag in it's history, otherwise we will use `0.0.0` as
|
||||||
|
the tag until a commit is tagged.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategy": {
|
||||||
|
"semvar": {
|
||||||
|
"allowPreRelease": true,
|
||||||
|
"strategy": {
|
||||||
|
"gitTag": {
|
||||||
|
"exactMatch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you set `'"exactMatch": true'` then bumping the version will fail on commits that are not
|
||||||
|
specifically tagged.
|
||||||
|
|
||||||
|
#### Custom Command Strategy
|
||||||
|
|
||||||
|
The custom `command` strategy allows you to call an external command to derive the next version. The
|
||||||
|
external command should return something that can be parsed as a semvar.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategy": {
|
||||||
|
"semvar": {
|
||||||
|
"allowPreRelease": true,
|
||||||
|
"strategy": {
|
||||||
|
"command": {
|
||||||
|
"arguments": ["my-command", "--some-option", "foo"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: All arguments to custom commands need to be a separate string in the arguments array
|
||||||
|
> otherwise they may not get passed appropriately to the command, so
|
||||||
|
> `"my-command --some-option foo"` will likely not work as expected.
|
||||||
|
|
||||||
|
#### Pre-Release
|
||||||
|
|
||||||
|
Semvar strategies can also include a pre-release strategy that adds a suffix to the semvar version
|
||||||
|
that can be used. In order for pre-release suffixes to be allowed the `'"allowPreRelease": true'`
|
||||||
|
must be set on the semvar strategy, you must also supply a pre-release strategy either when calling
|
||||||
|
the bump-version command or in your configuration.
|
||||||
|
|
||||||
|
Currently there are three pre-release strategies, `branch`, `gitTag`, and custom `command`, which we
|
||||||
|
will discuss.
|
||||||
|
|
||||||
|
A pre-release semvar example: `1.0.0-1-8d73287a60`
|
||||||
|
|
||||||
|
##### Branch
|
||||||
|
|
||||||
|
This will use the branch and optionally short version of the commit sha in order to derive the
|
||||||
|
pre-release suffix.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategy": {
|
||||||
|
"semvar": {
|
||||||
|
"allowPreRelease": true,
|
||||||
|
"preRelease": {
|
||||||
|
"strategy": {
|
||||||
|
"branch": {
|
||||||
|
"includeCommitSha": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would produce something similar to: `1.0.0-main-8d73287a60`
|
||||||
|
|
||||||
|
##### Git Tag
|
||||||
|
|
||||||
|
This will use the full output of `git describe --tags` to include the pre-release suffix.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategy" : {
|
||||||
|
"semvar" : {
|
||||||
|
"allowPreRelease" : true,
|
||||||
|
"preRelease" : {
|
||||||
|
"strategy" : {
|
||||||
|
"gitTag" : {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would produce something similar to: `1.0.0-10-8d73287a60`
|
||||||
|
|
||||||
|
##### Custom Command
|
||||||
|
|
||||||
|
This allows you to call an external command to generate the pre-release suffix. We will use whatever
|
||||||
|
the output is as the suffix.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategy": {
|
||||||
|
"semvar": {
|
||||||
|
"allowPreRelease": true,
|
||||||
|
"preRelease": {
|
||||||
|
"strategy": {
|
||||||
|
"command": {
|
||||||
|
"arguments": ["my-command", "--some-option", "foo"],
|
||||||
|
"allowPrefix": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: All arguments to custom commands need to be a separate string in the arguments array
|
||||||
|
> otherwise they may not get passed appropriately to the command, so
|
||||||
|
> `"my-command --some-option foo"` will likely not work as expected.
|
||||||
|
|
||||||
|
##### Pre-Release Prefixes
|
||||||
|
|
||||||
|
All pre-release strategies can also accept a `prefix` that will appended prior to the generated
|
||||||
|
pre-release suffix. This can also be used without providing a pre-release strategy to only append
|
||||||
|
the `prefix` to the semvar.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"strategy" : {
|
||||||
|
"semvar" : {
|
||||||
|
"allowPreRelease" : true,
|
||||||
|
"preRelease" : {
|
||||||
|
"prefix": "rc"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would produce something similar to: `1.0.0-rc`
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Options Reference
|
||||||
|
|
||||||
|
Common options used for the commands.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The commands mostly all accept similar options, below is a list of those options and a description
|
||||||
|
of their usage.
|
||||||
|
|
||||||
|
### General Options
|
||||||
|
|
||||||
|
| Short | Long | Argument | Description |
|
||||||
|
| ----- | ------------------- | -------- | -------------------------------------------------------------------- |
|
||||||
|
| N/A | --print | N/A | Perform the command, but don't write any output files |
|
||||||
|
| N/A | --project-directory | <path> | The path to the root of your project, defaults to current directory |
|
||||||
|
| -h | --help | N/A | Show help for a command |
|
||||||
|
| -v | --verbose | N/A | Increase logging level, can be passed multiple times (example: -vvv) |
|
||||||
|
| N/A | --version | N/A | Show the version of the command line tool |
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Short | Long | Argument | Description |
|
||||||
|
| ----- | ---------------------------------- | ------------ | ----------------------------------------------------------------------------------- |
|
||||||
|
| -f | --configuration-file | <path> | The path to the configuration to use. |
|
||||||
|
| -m | --target-module | <name> | The target module name inside your project |
|
||||||
|
| -n | --target-file-name | <name> | The file name for the version to be found inside the module |
|
||||||
|
| -p | --target-file-path | <path> | Path to a version file in your project |
|
||||||
|
| N/A | --enable-git-tag/--disable-git-tag | N/A | Use the git-tag version strategy |
|
||||||
|
| N/A | --require-exact-match | N/A | Fail if a tag is not specifically set on the commit |
|
||||||
|
| N/A | --require-existing-semvar | N/A | Fail if an existing semvar is not found in the version file. |
|
||||||
|
| -c | --custom-command | <arguments> | Use a custom command strategy for the version (any options need to proceed a '--') |
|
||||||
|
| N/A | --commit-sha/--no-commit-sha | N/A | Use the commit sha with branch version or pre-release strategy |
|
||||||
|
| N/A | --require-configuration | N/A | Fail if a configuration file is not found |
|
||||||
|
| N/A | --precedence | <precedence> | The precedence for when a file exists (values: ['file', 'strategy'], default: file) |
|
||||||
|
|
||||||
|
> Note: Precedence is used as tie breaker if the version in the file does not agree with the version
|
||||||
|
> from the configured strategy. This can happen if a file was edited / bumped manually or the value
|
||||||
|
> returned from the external command is not similar to the version in the file. By default the file
|
||||||
|
> will take precedence over what is returned from the strategy.
|
||||||
|
|
||||||
|
#### Pre-Release Options
|
||||||
|
|
||||||
|
| Short | Long | Argument | Description |
|
||||||
|
| ----- | ---------------------------- | ----------- | ------------------------------------------------------------ |
|
||||||
|
| -d | --disable-pre-release | N/A | Disable pre-relase suffixes from being used |
|
||||||
|
| -b | --pre-release-branch-style | N/A | Use the branch and commit sha style for pre-release suffixes |
|
||||||
|
| N/A | --commit-sha/--no-commit-sha | N/A | Use the commit sha with branch pre-release strategy |
|
||||||
|
| -g | --pre-release-git-tag-style | N/A | Use the git tag style for pre-release suffixes |
|
||||||
|
| N/A | --pre-release-prefix | <prefix> | A prefix to use before a pre-release suffix |
|
||||||
|
| N/A | --custom-pre-release | <arguments> | Use custom command strategy for pre-release suffix |
|
||||||
|
|
||||||
|
> Note: When using one of the `--custom-*` options then any arguments passed will be used for
|
||||||
|
> arguments when calling your custom strategy, if the external tool you use requires options they
|
||||||
|
> must proceed a '--' otherwise you will get an error that an 'unexpected option' is being used.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Plugins Reference
|
||||||
|
|
||||||
|
Learn about using the provided package plugins.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
There are two provided plugins that can be used, this describes their usage.
|
||||||
|
|
||||||
|
### Build with Version
|
||||||
|
|
||||||
|
The `BuildWithVersion` plugin uses your project configuration to automatically generate a version
|
||||||
|
file when swift builds your project. You can use the plugin by declaring it as dependency in your
|
||||||
|
project.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// swift-tools-version: 5.10
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
platforms:[.macOS(.v13)],
|
||||||
|
dependencies: [
|
||||||
|
...,
|
||||||
|
.package(url: "https://github.com/m-housh/swift-bump-version.git", from: "0.2.0")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.executableTarget(
|
||||||
|
name: "<target name>",
|
||||||
|
dependencies: [...],
|
||||||
|
plugins: [
|
||||||
|
.plugin(name: "BuildWithVersionPlugin", package: "swift-bump-version")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Plugin
|
||||||
|
|
||||||
|
There is also a `BumpVersionPlugin` that allows you to run the `bump-version` tool without
|
||||||
|
installing the command-line tool on your system, however it does make the usage much more verbose.
|
||||||
|
|
||||||
|
Include as dependency in your project.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// swift-tools-version: 5.10
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
platforms:[.macOS(.v13)],
|
||||||
|
dependencies: [
|
||||||
|
...,
|
||||||
|
.package(url: "https://github.com/m-housh/swift-bump-version.git", from: "0.2.0")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
...
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can use the manual plugin.
|
||||||
|
|
||||||
|
```
|
||||||
|
swift package \
|
||||||
|
--disable-sandbox \
|
||||||
|
--allow-writing-to-package-directory \
|
||||||
|
bump-version \
|
||||||
|
bump \
|
||||||
|
--minor
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: Anything after the `'bump-version'` in the above get's passed directly to the bump-version
|
||||||
|
> command-line tool, so you can use this to run any of the provided commands, the above shows
|
||||||
|
> bumping the minor semvar as a reference example.
|
||||||
|
>
|
||||||
|
> See Also: <doc:CommandReference>
|
||||||
62
Sources/BumpVersion/Documentation.docc/BumpVersion.md
Normal file
62
Sources/BumpVersion/Documentation.docc/BumpVersion.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# ``BumpVersion``
|
||||||
|
|
||||||
|
@Metadata {
|
||||||
|
@DisplayName("bump-version")
|
||||||
|
@DocumentationExtension(mergeBehavior: override)
|
||||||
|
}
|
||||||
|
|
||||||
|
A command-line tool for managing swift application versions.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This tool aims to provide a way to manage application versions in your build
|
||||||
|
pipeline. It can be used as a stand-alone command-line tool or by using one of
|
||||||
|
the provided swift package plugins.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The command-line tool can be installed via homebrew.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew tap m-housh/formula
|
||||||
|
brew install bump-version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Plugins
|
||||||
|
|
||||||
|
Package plugins can be used in a swift package manager project.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// swift-tools-version: 5.10
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
platforms:[.macOS(.v13)],
|
||||||
|
dependencies: [
|
||||||
|
...,
|
||||||
|
.package(url: "https://github.com/m-housh/swift-bump-version.git", from: "0.2.0")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.executableTarget(
|
||||||
|
name: "<target name>",
|
||||||
|
dependencies: [...],
|
||||||
|
plugins: [
|
||||||
|
.plugin(name: "BuildWithVersionPlugin", package: "swift-bump-version")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
> See Also: <doc:PluginsReference>
|
||||||
|
|
||||||
|
## Topics
|
||||||
|
|
||||||
|
### Articles
|
||||||
|
|
||||||
|
- <doc:BasicConfiguration>
|
||||||
|
- <doc:ConfigurationReference>
|
||||||
|
- <doc:CommandReference>
|
||||||
|
- <doc:OptionsReference>
|
||||||
|
- <doc:PluginsReference>
|
||||||
191
Sources/BumpVersion/GlobalOptions.swift
Normal file
191
Sources/BumpVersion/GlobalOptions.swift
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
@_spi(Internal) import CliClient
|
||||||
|
import ConfigurationClient
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import Rainbow
|
||||||
|
|
||||||
|
// TODO: Add an option to not load project configuration.
|
||||||
|
|
||||||
|
struct GlobalOptions: ParsableArguments {
|
||||||
|
|
||||||
|
@OptionGroup
|
||||||
|
var configOptions: ConfigurationOptions
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: .customLong("project-directory"),
|
||||||
|
help: "The project directory. (default: current directory)"
|
||||||
|
)
|
||||||
|
var projectDirectory: String?
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .customLong("print"),
|
||||||
|
help: "Print's what would be written to a target version file."
|
||||||
|
)
|
||||||
|
var dryRun: Bool = false
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: "Increase logging level, can be passed multiple times (example: -vvv)."
|
||||||
|
)
|
||||||
|
var verbose: Int
|
||||||
|
|
||||||
|
@Argument(
|
||||||
|
help: """
|
||||||
|
Arguments / options used for custom pre-release, options / flags must proceed a '--' in
|
||||||
|
the command. These are ignored if the `--custom` or `--custom-pre-release` flag is not set.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var extraOptions: [String] = []
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfigurationOptions: ParsableArguments {
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: [.customShort("f"), .long],
|
||||||
|
help: "Specify the path to a configuration file. (default: .bump-version.json)",
|
||||||
|
completion: .file(extensions: ["json"])
|
||||||
|
)
|
||||||
|
var configurationFile: String?
|
||||||
|
|
||||||
|
@OptionGroup var targetOptions: TargetOptions
|
||||||
|
|
||||||
|
@OptionGroup var semvarOptions: SemVarOptions
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
inversion: .prefixedNo,
|
||||||
|
help: """
|
||||||
|
Include the short commit sha in version or pre-release branch style output.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var commitSha: Bool = true
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
help: """
|
||||||
|
Require a configuration file, otherwise fail.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var requireConfiguration: Bool = false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TargetOptions: ParsableArguments {
|
||||||
|
@Option(
|
||||||
|
name: [.customShort("p"), .long],
|
||||||
|
help: "Path to the version file, not required if module is set."
|
||||||
|
)
|
||||||
|
var targetFilePath: String?
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: [.customShort("m"), .long],
|
||||||
|
help: "The target module name or directory path, not required if path is set."
|
||||||
|
)
|
||||||
|
var targetModule: String?
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: [.customShort("n"), .long],
|
||||||
|
help: "The file name inside the target module. (defaults to: \"Version.swift\")."
|
||||||
|
)
|
||||||
|
var targetFileName: String?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreReleaseOptions: ParsableArguments {
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: ""
|
||||||
|
)
|
||||||
|
var disablePreRelease: Bool = false
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: [.customShort("b"), .customLong("pre-release-branch-style")],
|
||||||
|
help: """
|
||||||
|
Use branch name and commit sha for pre-release suffix, ignored if branch is set.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var useBranchAsPreRelease: Bool = false
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: [.customShort("g"), .customLong("pre-release-git-tag-style")],
|
||||||
|
help: """
|
||||||
|
Use `git describe --tags` for pre-release suffix, ignored if branch is set.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var useTagAsPreRelease: Bool = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: .long,
|
||||||
|
help: """
|
||||||
|
Add / use a pre-release prefix string.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var preReleasePrefix: String?
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
help: """
|
||||||
|
Apply custom pre-release suffix, using extra options / arguments passed in after a '--'.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var customPreRelease: Bool = false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SemVarOptions: ParsableArguments {
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
inversion: .prefixedEnableDisable,
|
||||||
|
help: "Use git-tag strategy for semvar."
|
||||||
|
)
|
||||||
|
var gitTag: Bool = true
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
help: "Require exact match for git tag strategy."
|
||||||
|
)
|
||||||
|
var requireExactMatch: Bool = false
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
help: """
|
||||||
|
Fail if an existing version file does not exist, \("ignored if:".yellow.bold) \("branch is set".italic).
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var requireExistingFile: Bool = false
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
help: "Fail if a semvar is not parsed from existing file or git tag, used if branch is not set."
|
||||||
|
)
|
||||||
|
var requireExistingSemvar: Bool = false
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: """
|
||||||
|
Custom command strategy, uses extra-options to call an external command.
|
||||||
|
The external command should return a semvar that is used.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var customCommand: Bool = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: .long,
|
||||||
|
help: """
|
||||||
|
Set the precence to prefer version from file or strategy.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var precedence: Configuration.SemVar.Precedence?
|
||||||
|
|
||||||
|
@OptionGroup var preRelease: PreReleaseOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Configuration.SemVar.Precedence: ExpressibleByArgument {
|
||||||
|
public init?(argument: String) {
|
||||||
|
self.init(rawValue: argument)
|
||||||
|
}
|
||||||
|
}
|
||||||
311
Sources/BumpVersion/Helpers/DocHelpers.swift
Normal file
311
Sources/BumpVersion/Helpers/DocHelpers.swift
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
This file contains helpers for generating the documentation for the commands.
|
||||||
|
|
||||||
|
*/
|
||||||
|
import ArgumentParser
|
||||||
|
import CliDoc
|
||||||
|
import Rainbow
|
||||||
|
|
||||||
|
protocol CommandRepresentable: AsyncParsableCommand {
|
||||||
|
static var commandName: String { get }
|
||||||
|
static var parentCommand: String? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CommandRepresentable {
|
||||||
|
|
||||||
|
static var parentCommand: String? { nil }
|
||||||
|
|
||||||
|
static func makeExample(
|
||||||
|
label: String,
|
||||||
|
example: String,
|
||||||
|
includesAppName: Bool = true
|
||||||
|
) -> AppExample {
|
||||||
|
.init(
|
||||||
|
label: label,
|
||||||
|
parentCommand: parentCommand,
|
||||||
|
commandName: commandName,
|
||||||
|
includesAppName: includesAppName,
|
||||||
|
example: example
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Abstract where Content == String {
|
||||||
|
static func `default`(_ string: String) -> Self {
|
||||||
|
.init { string.blue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Note<Content: TextNode>: TextNode {
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(
|
||||||
|
@TextBuilder _ content: () -> Content
|
||||||
|
) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some TextNode {
|
||||||
|
LabeledContent {
|
||||||
|
content.italic()
|
||||||
|
} label: {
|
||||||
|
"Note:".yellow.bold
|
||||||
|
}
|
||||||
|
.style(.vertical())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Note where Content == AnyTextNode {
|
||||||
|
|
||||||
|
static func `default`(
|
||||||
|
notes: [String],
|
||||||
|
usesConfigurationFileNote: Bool = true,
|
||||||
|
usesConfigurationMergingNote: Bool = true
|
||||||
|
) -> Self {
|
||||||
|
var notes = notes
|
||||||
|
|
||||||
|
if usesConfigurationFileNote {
|
||||||
|
notes.insert(
|
||||||
|
"Most options are not required when a configuration file is setup for your project.",
|
||||||
|
at: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if usesConfigurationMergingNote {
|
||||||
|
if usesConfigurationFileNote {
|
||||||
|
notes.insert(
|
||||||
|
"Any configuration options get merged with the loaded project configuration file.",
|
||||||
|
at: 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
notes.insert(
|
||||||
|
"Any configuration options get merged with the loaded project configuration file.",
|
||||||
|
at: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .init {
|
||||||
|
VStack {
|
||||||
|
notes.enumerated().map { "\($0 + 1). \($1)" }
|
||||||
|
}
|
||||||
|
.eraseToAnyTextNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Discussion where Content == AnyTextNode {
|
||||||
|
static func `default`<Preamble: TextNode, Trailing: TextNode>(
|
||||||
|
notes: [String] = [],
|
||||||
|
examples: [AppExample]? = nil,
|
||||||
|
usesExtraOptions: Bool = true,
|
||||||
|
usesConfigurationFileNote: Bool = true,
|
||||||
|
usesConfigurationMergingNote: Bool = true,
|
||||||
|
@TextBuilder preamble: () -> Preamble,
|
||||||
|
@TextBuilder trailing: () -> Trailing
|
||||||
|
) -> Self {
|
||||||
|
Discussion {
|
||||||
|
VStack {
|
||||||
|
preamble().italic()
|
||||||
|
|
||||||
|
Note.default(
|
||||||
|
notes: notes,
|
||||||
|
usesConfigurationFileNote: usesConfigurationFileNote,
|
||||||
|
usesConfigurationMergingNote: usesConfigurationMergingNote
|
||||||
|
)
|
||||||
|
|
||||||
|
if let examples {
|
||||||
|
ExampleSection.default(examples: examples, usesExtraOptions: usesExtraOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
trailing()
|
||||||
|
}
|
||||||
|
.separator(.newLine(count: 2))
|
||||||
|
.eraseToAnyTextNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func `default`<Preamble: TextNode>(
|
||||||
|
notes: [String] = [],
|
||||||
|
examples: [AppExample]? = nil,
|
||||||
|
usesExtraOptions: Bool = true,
|
||||||
|
usesConfigurationFileNote: Bool = true,
|
||||||
|
usesConfigurationMergingNote: Bool = true,
|
||||||
|
@TextBuilder preamble: () -> Preamble
|
||||||
|
) -> Self {
|
||||||
|
.default(
|
||||||
|
notes: notes,
|
||||||
|
examples: examples,
|
||||||
|
usesExtraOptions: usesExtraOptions,
|
||||||
|
usesConfigurationFileNote: usesConfigurationFileNote,
|
||||||
|
usesConfigurationMergingNote: usesConfigurationMergingNote,
|
||||||
|
preamble: preamble,
|
||||||
|
trailing: {
|
||||||
|
if usesExtraOptions {
|
||||||
|
ImportantNote.extraOptionsNote
|
||||||
|
} else {
|
||||||
|
Empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func `default`(
|
||||||
|
notes: [String] = [],
|
||||||
|
examples: [AppExample]? = nil,
|
||||||
|
usesExtraOptions: Bool = true,
|
||||||
|
usesConfigurationFileNote: Bool = true,
|
||||||
|
usesConfigurationMergingNote: Bool = true
|
||||||
|
) -> Self {
|
||||||
|
.default(
|
||||||
|
notes: notes,
|
||||||
|
examples: examples,
|
||||||
|
usesExtraOptions: usesExtraOptions,
|
||||||
|
usesConfigurationFileNote: usesConfigurationFileNote,
|
||||||
|
usesConfigurationMergingNote: usesConfigurationMergingNote,
|
||||||
|
preamble: { Empty() },
|
||||||
|
trailing: { Empty() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ExampleSection where Header == String, Label == String {
|
||||||
|
static func `default`(
|
||||||
|
examples: [AppExample] = [],
|
||||||
|
usesExtraOptions: Bool = true
|
||||||
|
) -> some TextNode {
|
||||||
|
var examples: [AppExample] = examples
|
||||||
|
if usesExtraOptions {
|
||||||
|
examples = examples.appendingExtraOptionsExample()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Self(
|
||||||
|
"Examples:",
|
||||||
|
label: "A few common usage examples.",
|
||||||
|
examples: examples.map(\.example)
|
||||||
|
)
|
||||||
|
.style(AppExampleSectionStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppExampleSectionStyle: ExampleSectionStyle {
|
||||||
|
|
||||||
|
func render(content: ExampleSectionConfiguration) -> some TextNode {
|
||||||
|
Section {
|
||||||
|
VStack {
|
||||||
|
content.examples.map { example in
|
||||||
|
VStack {
|
||||||
|
example.label.color(.green).bold()
|
||||||
|
ShellCommand(example.example).style(.default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.separator(.newLine(count: 2))
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
content.title.color(.blue).bold()
|
||||||
|
content.label.italic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppExample {
|
||||||
|
let label: String
|
||||||
|
let parentCommand: String?
|
||||||
|
let commandName: String
|
||||||
|
let includesAppName: Bool
|
||||||
|
let exampleText: String
|
||||||
|
|
||||||
|
init(
|
||||||
|
label: String,
|
||||||
|
parentCommand: String? = nil,
|
||||||
|
commandName: String,
|
||||||
|
includesAppName: Bool = true,
|
||||||
|
example exampleText: String
|
||||||
|
) {
|
||||||
|
self.label = label
|
||||||
|
self.parentCommand = parentCommand
|
||||||
|
self.commandName = commandName
|
||||||
|
self.includesAppName = includesAppName
|
||||||
|
self.exampleText = exampleText
|
||||||
|
}
|
||||||
|
|
||||||
|
var example: Example {
|
||||||
|
var exampleString = "\(commandName) \(exampleText)"
|
||||||
|
if let parentCommand {
|
||||||
|
exampleString = "\(parentCommand) \(exampleString)"
|
||||||
|
}
|
||||||
|
if includesAppName {
|
||||||
|
exampleString = "\(Application.commandName) \(exampleString)"
|
||||||
|
}
|
||||||
|
return (label: label, example: exampleString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Array where Element == AppExample {
|
||||||
|
|
||||||
|
func appendingExtraOptionsExample() -> Self {
|
||||||
|
guard let first = first else { return self }
|
||||||
|
var output = self
|
||||||
|
output.append(.init(
|
||||||
|
label: "Passing extra options to custom strategy.",
|
||||||
|
parentCommand: first.parentCommand,
|
||||||
|
commandName: first.commandName,
|
||||||
|
includesAppName: first.includesAppName,
|
||||||
|
example: "--custom-command -- git describe --tags --exact-match"
|
||||||
|
))
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImportantNote<Content: TextNode>: TextNode {
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(
|
||||||
|
@TextBuilder _ content: () -> Content
|
||||||
|
) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some TextNode {
|
||||||
|
LabeledContent {
|
||||||
|
content.italic()
|
||||||
|
} label: {
|
||||||
|
"Important Note:".red.bold
|
||||||
|
}
|
||||||
|
.style(.vertical())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImportantNote where Content == String {
|
||||||
|
static var extraOptionsNote: Self {
|
||||||
|
.init {
|
||||||
|
"""
|
||||||
|
Extra options / flags for custom strategies must proceed a `--` or you may get an undefined option error.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Usage where Content == AnyTextNode {
|
||||||
|
static func `default`(parentCommand: String? = nil, commandName: String?) -> Self {
|
||||||
|
var commandString = commandName == nil ? "" : "\(commandName!)"
|
||||||
|
if let parentCommand {
|
||||||
|
commandString = "\(parentCommand) \(commandString)"
|
||||||
|
}
|
||||||
|
commandString = commandString == "" ? "\(Application.commandName)" : "\(Application.commandName) \(commandString)"
|
||||||
|
|
||||||
|
return .init {
|
||||||
|
HStack {
|
||||||
|
commandString.blue
|
||||||
|
"[<options>]".green
|
||||||
|
"--"
|
||||||
|
"[<extra-options> ...]".cyan
|
||||||
|
}
|
||||||
|
.eraseToAnyTextNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
Sources/BumpVersion/Helpers/GlobalOptions+run.swift
Normal file
200
Sources/BumpVersion/Helpers/GlobalOptions+run.swift
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import CliClient
|
||||||
|
import ConfigurationClient
|
||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import GitClient
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func withSetupDependencies<T>(
|
||||||
|
_ operation: () async throws -> T
|
||||||
|
) async throws -> T {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.fileClient = .liveValue
|
||||||
|
$0.gitClient = .liveValue
|
||||||
|
$0.cliClient = .liveValue
|
||||||
|
$0.configurationClient = .liveValue
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliClient.SharedOptions {
|
||||||
|
func runClient<T>(
|
||||||
|
_ keyPath: KeyPath<CliClient, @Sendable (Self) async throws -> T>
|
||||||
|
) async throws -> T {
|
||||||
|
try await withSetupDependencies {
|
||||||
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
return try await cliClient[keyPath: keyPath](self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runClient<A, T>(
|
||||||
|
_ keyPath: KeyPath<CliClient, @Sendable (A, Self) async throws -> T>,
|
||||||
|
args: A
|
||||||
|
) async throws -> T {
|
||||||
|
try await withSetupDependencies {
|
||||||
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
return try await cliClient[keyPath: keyPath](args, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GlobalOptions {
|
||||||
|
|
||||||
|
func run(
|
||||||
|
_ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> String>,
|
||||||
|
command: String
|
||||||
|
) async throws {
|
||||||
|
let output = try await shared(command: command).runClient(keyPath)
|
||||||
|
print(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run<T>(
|
||||||
|
_ keyPath: KeyPath<CliClient, @Sendable (T, CliClient.SharedOptions) async throws -> String>,
|
||||||
|
command: String,
|
||||||
|
args: T
|
||||||
|
) async throws {
|
||||||
|
let output = try await shared(command: command).runClient(keyPath, args: args)
|
||||||
|
print(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shared(command: String) throws -> CliClient.SharedOptions {
|
||||||
|
try configOptions.shared(
|
||||||
|
command: command,
|
||||||
|
dryRun: dryRun,
|
||||||
|
extraOptions: extraOptions,
|
||||||
|
gitDirectory: projectDirectory,
|
||||||
|
verbose: verbose
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TargetOptions {
|
||||||
|
func configTarget() throws -> Configuration.Target? {
|
||||||
|
guard let targetFilePath else {
|
||||||
|
guard let targetModule else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return .init(module: .init(targetModule, fileName: targetFileName))
|
||||||
|
}
|
||||||
|
return .init(path: targetFilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PreReleaseOptions {
|
||||||
|
|
||||||
|
func configPreReleaseStrategy(
|
||||||
|
includeCommitSha: Bool,
|
||||||
|
extraOptions: [String]
|
||||||
|
) throws -> Configuration.PreRelease? {
|
||||||
|
if useBranchAsPreRelease {
|
||||||
|
return .init(prefix: preReleasePrefix, strategy: .branch(includeCommitSha: includeCommitSha))
|
||||||
|
} else if useTagAsPreRelease {
|
||||||
|
return .init(prefix: preReleasePrefix, strategy: .gitTag)
|
||||||
|
} else if customPreRelease {
|
||||||
|
guard extraOptions.count > 0 else {
|
||||||
|
throw ExtraOptionsEmpty()
|
||||||
|
}
|
||||||
|
return .init(prefix: preReleasePrefix, strategy: .command(arguments: extraOptions))
|
||||||
|
} else if let preReleasePrefix {
|
||||||
|
return .init(prefix: preReleasePrefix, strategy: nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SemVarOptions {
|
||||||
|
|
||||||
|
func parseStrategy(extraOptions: [String]) throws -> Configuration.SemVar.Strategy? {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
guard customCommand else {
|
||||||
|
guard gitTag else { return nil }
|
||||||
|
return .gitTag(exactMatch: requireExactMatch)
|
||||||
|
}
|
||||||
|
guard extraOptions.count > 0 else {
|
||||||
|
logger.error("""
|
||||||
|
Extra options are empty, this does not make sense when using a custom command
|
||||||
|
strategy.
|
||||||
|
""")
|
||||||
|
throw ExtraOptionsEmpty()
|
||||||
|
}
|
||||||
|
return .command(arguments: extraOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configSemVarOptions(
|
||||||
|
includeCommitSha: Bool,
|
||||||
|
extraOptions: [String]
|
||||||
|
) throws -> Configuration.SemVar {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
if customCommand && preRelease.customPreRelease {
|
||||||
|
logger.warning("""
|
||||||
|
Custom pre-release can not be used at same time as custom command.
|
||||||
|
Ignoring pre-release...
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace("precedence: \(String(describing: precedence))")
|
||||||
|
|
||||||
|
return try .init(
|
||||||
|
allowPreRelease: !preRelease.disablePreRelease,
|
||||||
|
precedence: precedence,
|
||||||
|
preRelease: customCommand ? nil : preRelease.configPreReleaseStrategy(
|
||||||
|
includeCommitSha: includeCommitSha,
|
||||||
|
extraOptions: extraOptions
|
||||||
|
),
|
||||||
|
// Use nil here if false, which makes them not get used in json / file output, which makes
|
||||||
|
// user config smaller.
|
||||||
|
requireExistingFile: requireExistingFile ? true : nil,
|
||||||
|
requireExistingSemVar: requireExistingSemvar ? true : nil,
|
||||||
|
strategy: parseStrategy(extraOptions: extraOptions)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfigurationOptions {
|
||||||
|
|
||||||
|
func target() throws -> Configuration.Target? {
|
||||||
|
try targetOptions.configTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
func semvarOptions(
|
||||||
|
extraOptions: [String]
|
||||||
|
) throws -> Configuration.SemVar {
|
||||||
|
try semvarOptions.configSemVarOptions(
|
||||||
|
includeCommitSha: commitSha,
|
||||||
|
extraOptions: extraOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configurationToMerge(extraOptions: [String]) throws -> Configuration {
|
||||||
|
try .init(
|
||||||
|
target: target(),
|
||||||
|
strategy: semvarOptions.gitTag
|
||||||
|
? .semvar(semvarOptions(extraOptions: extraOptions))
|
||||||
|
: .branch(includeCommitSha: commitSha)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shared(
|
||||||
|
command: String,
|
||||||
|
dryRun: Bool = true,
|
||||||
|
extraOptions: [String] = [],
|
||||||
|
gitDirectory: String? = nil,
|
||||||
|
verbose: Int = 0
|
||||||
|
) throws -> CliClient.SharedOptions {
|
||||||
|
try .init(
|
||||||
|
allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease,
|
||||||
|
dryRun: dryRun,
|
||||||
|
projectDirectory: gitDirectory,
|
||||||
|
loggingOptions: .init(command: command, verbose: verbose),
|
||||||
|
configurationToMerge: configurationToMerge(extraOptions: extraOptions),
|
||||||
|
configurationFile: configurationFile,
|
||||||
|
requireConfigurationFile: requireConfiguration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExtraOptionsEmpty: Error {}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
// Do not set this variable, it is set during the build process.
|
// Do not set this variable, it is set during the build process.
|
||||||
let VERSION: String? = "0.1.0"
|
let VERSION: String? = "0.2.0-rc-5-ga26d869"
|
||||||
104
Sources/CliClient/CliClient.swift
Normal file
104
Sources/CliClient/CliClient.swift
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import ConfigurationClient
|
||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import GitClient
|
||||||
|
import LoggingExtensions
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
public extension DependencyValues {
|
||||||
|
|
||||||
|
/// The cli-client that runs the command line tool commands.
|
||||||
|
var cliClient: CliClient {
|
||||||
|
get { self[CliClient.self] }
|
||||||
|
set { self[CliClient.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles the command-line commands.
|
||||||
|
@DependencyClient
|
||||||
|
public struct CliClient: Sendable {
|
||||||
|
|
||||||
|
/// Build and update the version based on the git tag, or branch + sha.
|
||||||
|
public var build: @Sendable (SharedOptions) async throws -> String
|
||||||
|
|
||||||
|
/// Bump the existing version.
|
||||||
|
public var bump: @Sendable (BumpOption, SharedOptions) async throws -> String
|
||||||
|
|
||||||
|
/// Generate a version file with an optional version that can be set manually.
|
||||||
|
public var generate: @Sendable (SharedOptions) async throws -> String
|
||||||
|
|
||||||
|
/// Parse the configuration options.
|
||||||
|
public var parsedConfiguration: @Sendable (SharedOptions) async throws -> Configuration
|
||||||
|
|
||||||
|
public enum BumpOption: Sendable, CaseIterable {
|
||||||
|
case major, minor, patch, preRelease
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents options that are used by all the commands.
|
||||||
|
public struct SharedOptions: Equatable, Sendable {
|
||||||
|
|
||||||
|
/// Whether to allow pre-release suffixes.
|
||||||
|
let allowPreReleaseTag: Bool
|
||||||
|
|
||||||
|
/// Flag on if we write to files or not.
|
||||||
|
let dryRun: Bool
|
||||||
|
|
||||||
|
/// Specify a path to the project directory.
|
||||||
|
let projectDirectory: String?
|
||||||
|
|
||||||
|
/// The logging options to use.
|
||||||
|
let loggingOptions: LoggingOptions
|
||||||
|
|
||||||
|
/// Configuration that gets merged with the loaded (or default) configuration.
|
||||||
|
let configurationToMerge: Configuration?
|
||||||
|
|
||||||
|
/// Path to the configuration file to load.
|
||||||
|
let configurationFile: String?
|
||||||
|
|
||||||
|
/// Fail if a configuration file is not found.
|
||||||
|
let requireConfigurationFile: Bool
|
||||||
|
|
||||||
|
public init(
|
||||||
|
allowPreReleaseTag: Bool = true,
|
||||||
|
dryRun: Bool = false,
|
||||||
|
projectDirectory: String? = nil,
|
||||||
|
loggingOptions: LoggingOptions,
|
||||||
|
configurationToMerge: Configuration? = nil,
|
||||||
|
configurationFile: String? = nil,
|
||||||
|
requireConfigurationFile: Bool = false
|
||||||
|
) {
|
||||||
|
self.allowPreReleaseTag = allowPreReleaseTag
|
||||||
|
self.dryRun = dryRun
|
||||||
|
self.projectDirectory = projectDirectory
|
||||||
|
self.loggingOptions = loggingOptions
|
||||||
|
self.configurationFile = configurationFile
|
||||||
|
self.configurationToMerge = configurationToMerge
|
||||||
|
self.requireConfigurationFile = requireConfigurationFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliClient: DependencyKey {
|
||||||
|
|
||||||
|
public static let testValue: CliClient = Self()
|
||||||
|
|
||||||
|
public static func live(environment: [String: String]) -> Self {
|
||||||
|
.init(
|
||||||
|
build: { try await $0.build(environment) },
|
||||||
|
bump: { try await $1.bump($0) },
|
||||||
|
generate: { try await $0.generate() },
|
||||||
|
parsedConfiguration: { options in
|
||||||
|
try await options.loggingOptions.withLogger {
|
||||||
|
try await options.withMergedConfiguration { $0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var liveValue: CliClient {
|
||||||
|
.live(environment: ProcessInfo.processInfo.environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Sources/CliClient/CliClientError.swift
Normal file
12
Sources/CliClient/CliClientError.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import ConfigurationClient
|
||||||
|
|
||||||
|
enum CliClientError: Error {
|
||||||
|
case gitDirectoryNotFound
|
||||||
|
case fileExists(path: String)
|
||||||
|
case fileDoesNotExist(path: String)
|
||||||
|
case failedToParseVersionFile
|
||||||
|
case semVarNotFound(message: String)
|
||||||
|
case strategyNotFound(configuration: Configuration)
|
||||||
|
case preReleaseParsingError(String)
|
||||||
|
case versionStringNotFound
|
||||||
|
}
|
||||||
@@ -7,19 +7,19 @@ Learn how to integrate the plugins into your project
|
|||||||
Use the plugins by including as a package to your project and declaring in the `plugins` section of
|
Use the plugins by including as a package to your project and declaring in the `plugins` section of
|
||||||
your target.
|
your target.
|
||||||
|
|
||||||
> Note: You must use swift-tools version 5.6 or greater for package plugins and
|
> Note: You must use swift-tools version 5.6 or greater for package plugins and target `macOS(.v13)`
|
||||||
> target `macOS(.v10_15)` or greater.
|
> or greater.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// swift-tools-version: 5.7
|
// swift-tools-version: 5.10
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
platforms:[.macOS(.v10_15)],
|
platforms:[.macOS(.v13)],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
...,
|
...,
|
||||||
.package(url: "https://github.com/m-housh/swift-cli-version.git", from: "0.1.0")
|
.package(url: "https://github.com/m-housh/swift-cli-version.git", from: "0.2.0")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
@@ -33,8 +33,8 @@ let package = Package(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
The above example uses the build tool plugin. The `BuildWithVersionPlugin` will give you access
|
The above example uses the build tool plugin. The `BuildWithVersionPlugin` will give you access to a
|
||||||
to a `VERSION` variable in your project that you can use to supply the version of the tool.
|
`VERSION` variable in your project that you can use to supply the version of the tool.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
@@ -60,9 +60,9 @@ not declared in your source files.
|
|||||||

|

|
||||||
|
|
||||||
> Note: If your `DerivedData` folder lives in a directory that is a mounted volume / or somewhere
|
> Note: If your `DerivedData` folder lives in a directory that is a mounted volume / or somewhere
|
||||||
> that is not under your home folder then you may get build failures using the build tool
|
> that is not under your home folder then you may get build failures using the build tool plugin, it
|
||||||
> plugin, it will work if you build from the command line and pass the `--disable-sandbox` flag to the
|
> will work if you build from the command line and pass the `--disable-sandbox` flag to the build
|
||||||
> build command or use one of the manual methods.
|
> command or use one of the manual methods.
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
# Manual Plugins
|
# Manual Plugins
|
||||||
|
|
||||||
There are two plugins that are included that can be ran manually, if the build tool plugin does not fit
|
There are two plugins that are included that can be ran manually, if the build tool plugin does not
|
||||||
your use case.
|
fit your use case.
|
||||||
|
|
||||||
## Generate Version
|
## Generate Version
|
||||||
|
|
||||||
The `generate-version` plugin will create a `Version.swift` file in the given target. You can
|
The `generate-version` plugin will create a `Version.swift` file in the given target. You can run it
|
||||||
run it by running the following command.
|
by running the following command.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
swift package --disable-sandbox \
|
swift package --disable-sandbox \
|
||||||
@@ -32,21 +32,22 @@ swift package --disable-sandbox \
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
Both manual versions also allow the following options to customize the operation, the
|
Both manual versions also allow the following options to customize the operation, the options need
|
||||||
options need to come after the plugin name.
|
to come after the plugin name.
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| ------ | ----------- |
|
| ---------- | --------------------------------------------------------------------- |
|
||||||
| --dry-run | Do not write to any files, but describe where values would be written |
|
| --print | Do not write to any files, but describe where values would be written |
|
||||||
| --filename | Override the file name to be written in the target directory |
|
| --filename | Override the file name to be written in the target directory |
|
||||||
| --verbose | Increase the logging output |
|
| --verbose | Increase the logging output |
|
||||||
|
|
||||||
### Example with options
|
### Example with options
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
swift package \
|
swift package \
|
||||||
--allow-writing-to-package-directory \
|
--allow-writing-to-package-directory \
|
||||||
generate-version \
|
generate-version \
|
||||||
--dry-run \
|
--print \
|
||||||
--verbose \
|
--verbose \
|
||||||
<target>
|
<target>
|
||||||
```
|
```
|
||||||
18
Sources/CliClient/Documentation.docc/CliClient.md
Normal file
18
Sources/CliClient/Documentation.docc/CliClient.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# ``CliClient``
|
||||||
|
|
||||||
|
Derive a version for a command-line tool from git tags or a git sha.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
[Github Repo](https://github.com/m-housh/swift-cli-version)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This tool exposes several plugins that can be used to derive a version for a command line program at
|
||||||
|
build time or by manually running the plugin. The version is generally derived from git tags,
|
||||||
|
however it can be configured to run custom commands.
|
||||||
|
|
||||||
|
## Articles
|
||||||
|
|
||||||
|
- <doc:GettingStarted>
|
||||||
|
- <doc:ManualPlugins>
|
||||||
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 206 KiB |
185
Sources/CliClient/Internal/CliClient+run.swift
Normal file
185
Sources/CliClient/Internal/CliClient+run.swift
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import ConfigurationClient
|
||||||
|
import CustomDump
|
||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import GitClient
|
||||||
|
import LoggingExtensions
|
||||||
|
|
||||||
|
extension CliClient.SharedOptions {
|
||||||
|
|
||||||
|
/// All cli-client calls should run through this, it set's up logging,
|
||||||
|
/// loads configuration, and generates the current version based on the
|
||||||
|
/// configuration.
|
||||||
|
@discardableResult
|
||||||
|
func run(
|
||||||
|
_ operation: (VersionContainer) async throws -> Void
|
||||||
|
) async rethrows -> String {
|
||||||
|
try await loggingOptions.withLogger {
|
||||||
|
// Load the default configuration, if it exists.
|
||||||
|
try await withMergedConfiguration { configuration in
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
guard let strategy = configuration.strategy else {
|
||||||
|
throw CliClientError.strategyNotFound(configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.dump(configuration, level: .trace) {
|
||||||
|
"\nConfiguration: \($0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will fail if the target url is not set properly.
|
||||||
|
let targetUrl = try configuration.targetUrl(projectDirectory: projectDirectory)
|
||||||
|
logger.debug("Target: \(targetUrl.cleanFilePath)")
|
||||||
|
|
||||||
|
// Perform the operation, which generates the new version and writes it.
|
||||||
|
try await operation(
|
||||||
|
.load(projectDirectory: projectDirectory, strategy: strategy, url: targetUrl)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return the file path we wrote the version to.
|
||||||
|
return targetUrl.cleanFilePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merges any configuration set via the passed in options.
|
||||||
|
@discardableResult
|
||||||
|
func withMergedConfiguration<T>(
|
||||||
|
operation: (Configuration) async throws -> T
|
||||||
|
) async throws -> T {
|
||||||
|
@Dependency(\.configurationClient) var configurationClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
if configurationToMerge?.strategy?.branch != nil {
|
||||||
|
logger.trace("Merging branch strategy.")
|
||||||
|
// strategy = .branch(branch)
|
||||||
|
} else if let semvar = configurationToMerge?.strategy?.semvar {
|
||||||
|
logger.dump(semvar, level: .trace) {
|
||||||
|
"Merging semvar strategy:\n\($0)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await configurationClient.withConfiguration(
|
||||||
|
path: configurationFile,
|
||||||
|
merging: configurationToMerge,
|
||||||
|
strict: requireConfigurationFile,
|
||||||
|
operation: operation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(_ string: String, to url: URL) async throws {
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
if !dryRun {
|
||||||
|
try await fileClient.write(string: string, to: url)
|
||||||
|
} else {
|
||||||
|
logger.debug("Skipping, due to dry-run being passed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(_ currentVersion: VersionContainer) async throws {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
logger.trace("Begin writing version.")
|
||||||
|
|
||||||
|
// let hasChanges: Bool
|
||||||
|
let targetUrl: URL
|
||||||
|
let usesOptionalType: Bool
|
||||||
|
let versionString: String?
|
||||||
|
|
||||||
|
switch currentVersion {
|
||||||
|
case let .branch(branch):
|
||||||
|
// hasChanges = branch.hasChanges
|
||||||
|
targetUrl = branch.targetUrl
|
||||||
|
usesOptionalType = branch.usesOptionalType
|
||||||
|
versionString = branch.versionString
|
||||||
|
case let .semvar(semvar):
|
||||||
|
// hasChanges = semvar.hasChanges
|
||||||
|
targetUrl = semvar.targetUrl
|
||||||
|
usesOptionalType = semvar.usesOptionalType
|
||||||
|
versionString = semvar.versionString(withPreRelease: allowPreReleaseTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if !hasChanges {
|
||||||
|
// logger.debug("No changes from loaded version, not writing next version.")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
guard let versionString else {
|
||||||
|
throw CliClientError.versionStringNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag)
|
||||||
|
|
||||||
|
if !dryRun {
|
||||||
|
logger.debug("Version: \(versionString)")
|
||||||
|
} else {
|
||||||
|
logger.info("Version: \(versionString)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = usesOptionalType ? Template.optional(versionString) : Template.nonOptional(versionString)
|
||||||
|
logger.trace("Template string: \(template)")
|
||||||
|
|
||||||
|
try await write(template, to: targetUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliClient.SharedOptions {
|
||||||
|
|
||||||
|
func build(_ environment: [String: String]) async throws -> String {
|
||||||
|
try await run { currentVersion in
|
||||||
|
try await write(currentVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bump(_ type: CliClient.BumpOption) async throws -> String {
|
||||||
|
return try await run { container in
|
||||||
|
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
switch container {
|
||||||
|
case .branch: // When we did not parse a semvar, just write whatever we parsed for the current version.
|
||||||
|
logger.debug("Failed to parse semvar, but got current version string.")
|
||||||
|
try await write(container)
|
||||||
|
|
||||||
|
case let .semvar(semvar):
|
||||||
|
|
||||||
|
let version: SemVar?
|
||||||
|
|
||||||
|
switch semvar.precedence ?? .default {
|
||||||
|
case .file:
|
||||||
|
version = semvar.loadedVersion ?? semvar.strategyVersion
|
||||||
|
case .strategy:
|
||||||
|
version = semvar.strategyVersion ?? semvar.loadedVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// let version = semvar.loadedVersion ?? semvar.nextVersion
|
||||||
|
guard let version else {
|
||||||
|
throw CliClientError.semVarNotFound(message: "Failed to parse a valid semvar to bump.")
|
||||||
|
}
|
||||||
|
logger.dump(version, level: .debug) { "Version prior to bumping:\n\($0)" }
|
||||||
|
let bumped = version.bump(type)
|
||||||
|
logger.dump(bumped, level: .trace) { "Bumped version:\n\($0)" }
|
||||||
|
try await write(.semvar(semvar.withUpdateNextVersion(bumped)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate() async throws -> String {
|
||||||
|
try await run { currentVersion in
|
||||||
|
try await write(currentVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CurrentVersionContainer where Version == SemVar {
|
||||||
|
func withUpdateNextVersion(_ next: SemVar) -> Self {
|
||||||
|
.init(
|
||||||
|
targetUrl: targetUrl,
|
||||||
|
usesOptionalType: usesOptionalType,
|
||||||
|
loadedVersion: loadedVersion,
|
||||||
|
precedence: .strategy, // make sure to use the next version, since it was specified, as this is called from `bump`.
|
||||||
|
strategyVersion: next
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Sources/CliClient/Internal/Configuration+targetUrl.swift
Normal file
57
Sources/CliClient/Internal/Configuration+targetUrl.swift
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import ConfigurationClient
|
||||||
|
import CustomDump
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import GitClient
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
extension Configuration {
|
||||||
|
func targetUrl(projectDirectory: String?) throws -> URL {
|
||||||
|
guard let target else {
|
||||||
|
throw ConfigurationParsingError.targetNotFound
|
||||||
|
}
|
||||||
|
return try target.url(projectDirectory: projectDirectory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Configuration.Target {
|
||||||
|
func url(projectDirectory: String?) throws -> URL {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
let filePath: String
|
||||||
|
|
||||||
|
if let path {
|
||||||
|
filePath = path
|
||||||
|
} else {
|
||||||
|
guard let module else {
|
||||||
|
throw ConfigurationParsingError.pathOrModuleNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = module.name
|
||||||
|
logger.debug("module.name: \(path)")
|
||||||
|
|
||||||
|
if path.hasPrefix("./") {
|
||||||
|
path = String(path.dropFirst(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !path.hasPrefix("Sources") {
|
||||||
|
logger.debug("no prefix")
|
||||||
|
path = "Sources/\(path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = "\(path)/\(module.fileNameOrDefault)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if let projectDirectory {
|
||||||
|
return URL(filePath: "\(projectDirectory)/\(filePath)")
|
||||||
|
}
|
||||||
|
return URL(filePath: filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConfigurationParsingError: Error {
|
||||||
|
case targetNotFound
|
||||||
|
case pathOrModuleNotSet
|
||||||
|
case versionStrategyError(message: String)
|
||||||
|
case versionStrategyNotFound
|
||||||
|
}
|
||||||
72
Sources/CliClient/Internal/FileClient+semVar.swift
Normal file
72
Sources/CliClient/Internal/FileClient+semVar.swift
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import GitClient
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension FileClient {
|
||||||
|
func branch(
|
||||||
|
file: URL,
|
||||||
|
projectDirectory: String?,
|
||||||
|
requireExistingFile: Bool
|
||||||
|
) async throws -> (string: String, usesOptionalType: Bool)? {
|
||||||
|
let loaded = try? await getVersionString(fileUrl: file, projectDirectory: projectDirectory)
|
||||||
|
guard let loaded else {
|
||||||
|
if requireExistingFile {
|
||||||
|
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (loaded.0, loaded.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func semvar(
|
||||||
|
file: URL,
|
||||||
|
projectDirectory: String?,
|
||||||
|
requireExistingFile: Bool
|
||||||
|
) async throws -> (semVar: SemVar?, usesOptionalType: Bool)? {
|
||||||
|
let loaded = try? await getVersionString(fileUrl: file, projectDirectory: projectDirectory)
|
||||||
|
guard let loaded else {
|
||||||
|
if requireExistingFile {
|
||||||
|
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let semvar = SemVar(string: loaded.0)
|
||||||
|
return (semvar, loaded.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getVersionString(
|
||||||
|
fileUrl: URL,
|
||||||
|
projectDirectory: String?
|
||||||
|
) async throws -> (version: String, usesOptionalType: Bool) {
|
||||||
|
@Dependency(\.gitClient) var gitClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
let targetUrl = fileUrl
|
||||||
|
|
||||||
|
guard fileExists(targetUrl) else {
|
||||||
|
throw CliClientError.fileDoesNotExist(path: fileUrl.cleanFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = try await read(targetUrl)
|
||||||
|
let versionLine = contents.split(separator: "\n")
|
||||||
|
.first { $0.hasPrefix("let VERSION:") }
|
||||||
|
|
||||||
|
guard let versionLine else {
|
||||||
|
throw CliClientError.failedToParseVersionFile
|
||||||
|
}
|
||||||
|
logger.debug("Version line: \(versionLine)")
|
||||||
|
|
||||||
|
let isOptional = versionLine.contains("String?")
|
||||||
|
logger.trace("Uses optional: \(isOptional)")
|
||||||
|
|
||||||
|
let versionString = versionLine.split(separator: "let VERSION: \(isOptional ? "String?" : "String") = ").last
|
||||||
|
guard let versionString else {
|
||||||
|
throw CliClientError.failedToParseVersionFile
|
||||||
|
}
|
||||||
|
logger.trace("Parsed version string: \(versionString)")
|
||||||
|
return (String(versionString), isOptional)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
137
Sources/CliClient/Internal/SemVar.swift
Normal file
137
Sources/CliClient/Internal/SemVar.swift
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import GitClient
|
||||||
|
|
||||||
|
// Container for sem-var version.
|
||||||
|
@_spi(Internal)
|
||||||
|
public struct SemVar: CustomStringConvertible, Equatable, Sendable {
|
||||||
|
/// The major version.
|
||||||
|
public let major: Int
|
||||||
|
/// The minor version.
|
||||||
|
public let minor: Int
|
||||||
|
/// The patch version.
|
||||||
|
public let patch: Int
|
||||||
|
/// Extra pre-release tag.
|
||||||
|
public let preRelease: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
major: Int,
|
||||||
|
minor: Int,
|
||||||
|
patch: Int,
|
||||||
|
preRelease: String? = nil
|
||||||
|
) {
|
||||||
|
self.major = major
|
||||||
|
self.minor = minor
|
||||||
|
self.patch = patch
|
||||||
|
self.preRelease = preRelease
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(preRelease: String? = nil) {
|
||||||
|
self.init(
|
||||||
|
major: 0,
|
||||||
|
minor: 0,
|
||||||
|
patch: 0,
|
||||||
|
preRelease: preRelease
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(string: String) {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
logger.trace("Parsing semvar from: \(string)")
|
||||||
|
|
||||||
|
let parts = string.split(separator: ".")
|
||||||
|
logger.trace("parts: \(parts)")
|
||||||
|
guard parts.count >= 3 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let major = Int(String(parts[0].replacingOccurrences(of: "\"", with: "")))
|
||||||
|
logger.trace("major: \(String(describing: major))")
|
||||||
|
|
||||||
|
let minor = Int(String(parts[1]))
|
||||||
|
logger.trace("minor: \(String(describing: minor))")
|
||||||
|
|
||||||
|
let patchParts = parts[2].replacingOccurrences(of: "\"", with: "").split(separator: "-")
|
||||||
|
logger.trace("patchParts: \(String(describing: patchParts))")
|
||||||
|
|
||||||
|
let patch = Int(patchParts.first ?? "0")
|
||||||
|
logger.trace("patch: \(String(describing: patch))")
|
||||||
|
|
||||||
|
let preRelease = String(patchParts.dropFirst().joined(separator: "-"))
|
||||||
|
logger.trace("preRelease: \(String(describing: preRelease))")
|
||||||
|
|
||||||
|
self.init(
|
||||||
|
major: major ?? 0,
|
||||||
|
minor: minor ?? 0,
|
||||||
|
patch: patch ?? 0,
|
||||||
|
preRelease: preRelease
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var description: String { versionString() }
|
||||||
|
|
||||||
|
// Create a version string, optionally appending a suffix.
|
||||||
|
public func versionString(withPreReleaseTag: Bool = true) -> String {
|
||||||
|
let string = "\(major).\(minor).\(patch)"
|
||||||
|
|
||||||
|
guard withPreReleaseTag else { return string }
|
||||||
|
|
||||||
|
guard let suffix = preRelease, suffix.count > 0 else {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
if !suffix.hasPrefix("-") {
|
||||||
|
return "\(string)-\(suffix)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(string)\(suffix)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bumps the sem-var by the given option (major, minor, patch)
|
||||||
|
public func bump(_ option: CliClient.BumpOption, preRelease: String? = nil) -> Self {
|
||||||
|
switch option {
|
||||||
|
case .major:
|
||||||
|
return .init(
|
||||||
|
major: major + 1,
|
||||||
|
minor: 0,
|
||||||
|
patch: 0,
|
||||||
|
preRelease: preRelease
|
||||||
|
)
|
||||||
|
case .minor:
|
||||||
|
return .init(
|
||||||
|
major: major,
|
||||||
|
minor: minor + 1,
|
||||||
|
patch: 0,
|
||||||
|
preRelease: preRelease
|
||||||
|
)
|
||||||
|
case .patch:
|
||||||
|
return .init(
|
||||||
|
major: major,
|
||||||
|
minor: minor,
|
||||||
|
patch: patch + 1,
|
||||||
|
preRelease: preRelease
|
||||||
|
)
|
||||||
|
case .preRelease:
|
||||||
|
guard let preRelease else { return self }
|
||||||
|
return applyingPreRelease(preRelease)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func applyingPreRelease(_ preRelease: String) -> Self {
|
||||||
|
.init(
|
||||||
|
major: major,
|
||||||
|
minor: minor,
|
||||||
|
patch: patch,
|
||||||
|
preRelease: preRelease
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension GitClient.Version {
|
||||||
|
var semVar: SemVar? {
|
||||||
|
.init(string: description)
|
||||||
|
}
|
||||||
|
}
|
||||||
90
Sources/CliClient/Internal/Semvar+nextVersion.swift
Normal file
90
Sources/CliClient/Internal/Semvar+nextVersion.swift
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import ConfigurationClient
|
||||||
|
import Foundation
|
||||||
|
import GitClient
|
||||||
|
import LoggingExtensions
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
extension SemVar {
|
||||||
|
static func nextVersion(
|
||||||
|
configuration: Configuration.SemVar,
|
||||||
|
projectDirectory: String?
|
||||||
|
) async throws -> Self? {
|
||||||
|
@Dependency(\.asyncShellClient) var asyncShellClient
|
||||||
|
@Dependency(\.gitClient) var gitClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
guard let strategy = configuration.strategy else { return nil }
|
||||||
|
|
||||||
|
let semvarString: String
|
||||||
|
|
||||||
|
switch strategy {
|
||||||
|
case let .gitTag(exactMatch: exactMatch):
|
||||||
|
logger.trace("Loading semvar gitTag strategy...")
|
||||||
|
|
||||||
|
semvarString = try await gitClient.version(.init(
|
||||||
|
gitDirectory: projectDirectory,
|
||||||
|
style: .tag(exactMatch: exactMatch ?? false)
|
||||||
|
)).description
|
||||||
|
|
||||||
|
case let .command(arguments: arguments):
|
||||||
|
logger.trace("Loading semvar custom command strategy: \(arguments)")
|
||||||
|
semvarString = try await asyncShellClient.background(.init(arguments))
|
||||||
|
}
|
||||||
|
|
||||||
|
var preReleaseString: String?
|
||||||
|
if let preRelease = configuration.preRelease,
|
||||||
|
configuration.allowPreRelease ?? true
|
||||||
|
{
|
||||||
|
preReleaseString = try await preRelease.get(projectDirectory: projectDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
let semvar = SemVar(string: semvarString)
|
||||||
|
|
||||||
|
if let preReleaseString {
|
||||||
|
return semvar?.applyingPreRelease(preReleaseString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return semvar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Configuration.PreRelease {
|
||||||
|
|
||||||
|
func get(projectDirectory: String?) async throws -> String? {
|
||||||
|
@Dependency(\.asyncShellClient) var asyncShellClient
|
||||||
|
@Dependency(\.gitClient) var gitClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
var allowsPrefix = true
|
||||||
|
var preReleaseString: String
|
||||||
|
|
||||||
|
guard let strategy else { return nil }
|
||||||
|
switch strategy {
|
||||||
|
case let .branch(includeCommitSha: includeCommitSha):
|
||||||
|
logger.trace("Loading pre-relase branch strategy...")
|
||||||
|
preReleaseString = try await gitClient.version(.init(
|
||||||
|
gitDirectory: projectDirectory,
|
||||||
|
style: .branch(commitSha: includeCommitSha)
|
||||||
|
)).description
|
||||||
|
|
||||||
|
case .gitTag:
|
||||||
|
logger.trace("Loading pre-relase gitTag strategy...")
|
||||||
|
preReleaseString = try await gitClient.version(.init(
|
||||||
|
gitDirectory: projectDirectory,
|
||||||
|
style: .tag(exactMatch: false)
|
||||||
|
)).description
|
||||||
|
|
||||||
|
case let .command(arguments: arguments, allowPrefix: allowPrefix):
|
||||||
|
logger.trace("Loading pre-relase custom command strategy...")
|
||||||
|
allowsPrefix = allowPrefix ?? false
|
||||||
|
preReleaseString = try await asyncShellClient.background(.init(arguments))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let prefix, allowsPrefix {
|
||||||
|
preReleaseString = "\(prefix)-\(preReleaseString)"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace("Pre-release string: \(preReleaseString)")
|
||||||
|
return preReleaseString
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Sources/CliClient/Internal/Template.swift
Normal file
31
Sources/CliClient/Internal/Template.swift
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
@_spi(Internal)
|
||||||
|
public struct Template: Sendable {
|
||||||
|
let type: TemplateType
|
||||||
|
let version: String?
|
||||||
|
|
||||||
|
enum TemplateType: String, Sendable {
|
||||||
|
case optionalString = "String?"
|
||||||
|
case string = "String"
|
||||||
|
}
|
||||||
|
|
||||||
|
var value: String {
|
||||||
|
let versionString = version != nil ? "\"\(version!)\"" : "nil"
|
||||||
|
return """
|
||||||
|
// Do not set this variable, it is set during the build process.
|
||||||
|
let VERSION: \(type.rawValue) = \(versionString)
|
||||||
|
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func build(_ version: String? = nil) -> String {
|
||||||
|
nonOptional(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func nonOptional(_ version: String? = nil) -> String {
|
||||||
|
Self(type: .string, version: version).value
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func optional(_ version: String? = nil) -> String {
|
||||||
|
Self(type: .optionalString, version: version).value
|
||||||
|
}
|
||||||
|
}
|
||||||
172
Sources/CliClient/Internal/VersionContainer.swift
Normal file
172
Sources/CliClient/Internal/VersionContainer.swift
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import ConfigurationClient
|
||||||
|
import CustomDump
|
||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import LoggingExtensions
|
||||||
|
|
||||||
|
enum VersionContainer: Sendable {
|
||||||
|
case branch(CurrentVersionContainer<String>)
|
||||||
|
case semvar(CurrentVersionContainer<SemVar>)
|
||||||
|
|
||||||
|
static func load(
|
||||||
|
projectDirectory: String?,
|
||||||
|
strategy: Configuration.VersionStrategy,
|
||||||
|
url: URL
|
||||||
|
) async throws -> Self {
|
||||||
|
switch strategy {
|
||||||
|
case let .branch(includeCommitSha: includeCommitSha):
|
||||||
|
return try await .branch(.load(
|
||||||
|
branch: .init(includeCommitSha: includeCommitSha),
|
||||||
|
projectDirectory: projectDirectory,
|
||||||
|
url: url
|
||||||
|
))
|
||||||
|
case .semvar:
|
||||||
|
return try await .semvar(.load(
|
||||||
|
semvar: strategy.semvar!,
|
||||||
|
projectDirectory: projectDirectory,
|
||||||
|
url: url
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add a precedence field for which version to prefer, should also be specified in
|
||||||
|
// configuration.
|
||||||
|
struct CurrentVersionContainer<Version> {
|
||||||
|
let targetUrl: URL
|
||||||
|
let usesOptionalType: Bool
|
||||||
|
let loadedVersion: Version?
|
||||||
|
let precedence: Configuration.SemVar.Precedence?
|
||||||
|
let strategyVersion: Version?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CurrentVersionContainer: Equatable where Version: Equatable {
|
||||||
|
|
||||||
|
var hasChanges: Bool {
|
||||||
|
switch (loadedVersion, strategyVersion) {
|
||||||
|
case (.none, .none):
|
||||||
|
return false
|
||||||
|
case (.some, .none),
|
||||||
|
(.none, .some):
|
||||||
|
return true
|
||||||
|
case let (.some(loaded), .some(next)):
|
||||||
|
return loaded == next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CurrentVersionContainer: Sendable where Version: Sendable {}
|
||||||
|
|
||||||
|
extension CurrentVersionContainer where Version == String {
|
||||||
|
|
||||||
|
static func load(
|
||||||
|
branch: Configuration.Branch,
|
||||||
|
projectDirectory: String?,
|
||||||
|
url: URL
|
||||||
|
) async throws -> Self {
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
@Dependency(\.gitClient) var gitClient
|
||||||
|
|
||||||
|
let loaded = try await fileClient.branch(
|
||||||
|
file: url,
|
||||||
|
projectDirectory: projectDirectory,
|
||||||
|
requireExistingFile: false
|
||||||
|
)
|
||||||
|
|
||||||
|
let next = try await gitClient.version(.init(
|
||||||
|
gitDirectory: projectDirectory,
|
||||||
|
style: .branch(commitSha: branch.includeCommitSha)
|
||||||
|
))
|
||||||
|
|
||||||
|
return .init(
|
||||||
|
targetUrl: url,
|
||||||
|
usesOptionalType: loaded?.1 ?? true,
|
||||||
|
loadedVersion: loaded?.0,
|
||||||
|
precedence: nil,
|
||||||
|
strategyVersion: next.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionString: String? {
|
||||||
|
loadedVersion ?? strategyVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CurrentVersionContainer where Version == SemVar {
|
||||||
|
|
||||||
|
// TODO: Update to use precedence and not fetch `nextVersion` if we loaded a file version.
|
||||||
|
static func load(semvar: Configuration.SemVar, projectDirectory: String?, url: URL) async throws -> Self {
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
logger.trace("Begin loading semvar from: \(url.cleanFilePath)")
|
||||||
|
|
||||||
|
async let (loaded, usesOptionalType) = try await loadCurrentVersion(
|
||||||
|
semvar: semvar,
|
||||||
|
projectDirectory: projectDirectory,
|
||||||
|
url: url
|
||||||
|
)
|
||||||
|
async let next = try await loadNextVersion(semvar: semvar, projectDirectory: projectDirectory)
|
||||||
|
|
||||||
|
return try await .init(
|
||||||
|
targetUrl: url,
|
||||||
|
usesOptionalType: usesOptionalType,
|
||||||
|
loadedVersion: loaded,
|
||||||
|
precedence: semvar.precedence,
|
||||||
|
strategyVersion: next
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func loadCurrentVersion(
|
||||||
|
semvar: Configuration.SemVar,
|
||||||
|
projectDirectory: String?,
|
||||||
|
url: URL
|
||||||
|
) async throws -> (SemVar?, Bool) {
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
logger.trace("Begin loading current version from: \(url.cleanFilePath)")
|
||||||
|
|
||||||
|
let loadedOptional = try await fileClient.semvar(
|
||||||
|
file: url,
|
||||||
|
projectDirectory: projectDirectory,
|
||||||
|
requireExistingFile: semvar.requireExistingFile ?? false
|
||||||
|
)
|
||||||
|
guard let loadedStrong = loadedOptional else {
|
||||||
|
if semvar.requireExistingFile ?? false {
|
||||||
|
throw CliClientError.semVarNotFound(message: "Required by configuration's 'requireExistingFile' variable.")
|
||||||
|
}
|
||||||
|
return (nil, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (loaded, usesOptionalType) = loadedStrong
|
||||||
|
|
||||||
|
logger.dump(loaded) { "Loaded version:\n\($0)" }
|
||||||
|
return (loaded, usesOptionalType)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func loadNextVersion(semvar: Configuration.SemVar, projectDirectory: String?) async throws -> SemVar? {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
let next = try await SemVar.nextVersion(
|
||||||
|
configuration: semvar,
|
||||||
|
projectDirectory: projectDirectory
|
||||||
|
)
|
||||||
|
logger.dump(next) { "Next version:\n\($0)" }
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionString(withPreRelease: Bool) -> String? {
|
||||||
|
let version: SemVar?
|
||||||
|
|
||||||
|
switch precedence ?? .default {
|
||||||
|
case .file:
|
||||||
|
version = loadedVersion ?? strategyVersion
|
||||||
|
case .strategy:
|
||||||
|
version = strategyVersion ?? loadedVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
return version?.versionString(withPreReleaseTag: withPreRelease)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# ``CliVersion``
|
|
||||||
|
|
||||||
Derive a version for a command line tool from git tags or a git sha.
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
[Github Repo](https://github.com/m-housh/swift-cli-version)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This tool exposes several plugins that can be used to derive a version for a command line program at
|
|
||||||
build time or by manually running the plugin. The version is derived from git tags and falling back to
|
|
||||||
the branch and git sha if a tag is not set for the current worktree state.
|
|
||||||
|
|
||||||
## Articles
|
|
||||||
|
|
||||||
- <doc:GettingStarted>
|
|
||||||
- <doc:ManualPlugins>
|
|
||||||
|
|
||||||
## Api
|
|
||||||
|
|
||||||
- ``FileClient``
|
|
||||||
- ``GitVersionClient``
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import Dependencies
|
|
||||||
import Foundation
|
|
||||||
#if canImport(FoundationNetworking)
|
|
||||||
import FoundationNetworking
|
|
||||||
#endif
|
|
||||||
import XCTestDynamicOverlay
|
|
||||||
|
|
||||||
/// Represents the interactions with the file system. It is able
|
|
||||||
/// to read from and write to files.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ```swift
|
|
||||||
/// @Dependency(\.fileClient) var fileClient
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
public struct FileClient {
|
|
||||||
|
|
||||||
/// Write `Data` to a file `URL`.
|
|
||||||
public private(set) var write: (Data, URL) throws -> Void
|
|
||||||
|
|
||||||
/// Create a new ``GitVersion/FileClient`` instance.
|
|
||||||
///
|
|
||||||
/// This is generally not interacted with directly, instead access as a dependency.
|
|
||||||
///
|
|
||||||
///```swift
|
|
||||||
/// @Dependency(\.fileClient) var fileClient
|
|
||||||
///```
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - write: Write the data to a file.
|
|
||||||
public init(
|
|
||||||
write: @escaping (Data, URL) throws -> Void
|
|
||||||
) {
|
|
||||||
self.write = write
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write's the the string to a file path.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - string: The string to write to the file.
|
|
||||||
/// - path: The file path.
|
|
||||||
public func write(string: String, to path: String) throws {
|
|
||||||
let url = try url(for: path)
|
|
||||||
try self.write(string: string, to: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write's the the string to a file path.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - string: The string to write to the file.
|
|
||||||
/// - url: The file url.
|
|
||||||
public func write(string: String, to url: URL) throws {
|
|
||||||
try self.write(Data(string.utf8), url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension FileClient: DependencyKey {
|
|
||||||
|
|
||||||
/// A ``FileClient`` that does not do anything.
|
|
||||||
public static let noop = FileClient.init(
|
|
||||||
write: { _, _ in }
|
|
||||||
)
|
|
||||||
|
|
||||||
/// An `unimplemented` ``FileClient``.
|
|
||||||
public static let testValue = FileClient(
|
|
||||||
write: unimplemented("\(Self.self).write")
|
|
||||||
)
|
|
||||||
|
|
||||||
/// The live ``FileClient``
|
|
||||||
public static let liveValue = FileClient(
|
|
||||||
write: { try $0.write(to: $1, options: .atomic) }
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DependencyValues {
|
|
||||||
|
|
||||||
/// Access a basic ``FileClient`` that can read / write data to the file system.
|
|
||||||
///
|
|
||||||
public var fileClient: FileClient {
|
|
||||||
get { self[FileClient.self] }
|
|
||||||
set { self[FileClient.self] = newValue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private
|
|
||||||
fileprivate func url(for path: String) throws -> URL {
|
|
||||||
#if os(Linux)
|
|
||||||
return URL(fileURLWithPath: path)
|
|
||||||
#else
|
|
||||||
if #available(macOS 13.0, *) {
|
|
||||||
return URL(filePath: path)
|
|
||||||
} else {
|
|
||||||
// Fallback on earlier versions
|
|
||||||
return URL(fileURLWithPath: path)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
#if canImport(FoundationNetworking)
|
|
||||||
import FoundationNetworking
|
|
||||||
#endif
|
|
||||||
import ShellClient
|
|
||||||
import XCTestDynamicOverlay
|
|
||||||
|
|
||||||
/// A client that can retrieve the current version from a git directory.
|
|
||||||
/// It will use the current `tag`, or if the current git tree does not
|
|
||||||
/// point to a commit that is tagged, it will use the `branch git-sha` as
|
|
||||||
/// the version.
|
|
||||||
///
|
|
||||||
/// This is often not used directly, instead it is used with one of the plugins
|
|
||||||
/// that is supplied with this library. The use case is to set the version of a command line
|
|
||||||
/// tool based on the current git tag.
|
|
||||||
///
|
|
||||||
public struct GitVersionClient {
|
|
||||||
|
|
||||||
/// The closure to run that returns the current version from a given
|
|
||||||
/// git directory.
|
|
||||||
private var currentVersion: (String?) throws -> String
|
|
||||||
|
|
||||||
/// Create a new ``GitVersionClient`` instance.
|
|
||||||
///
|
|
||||||
/// This is normally not interacted with directly, instead access the client through the dependency system.
|
|
||||||
/// ```swift
|
|
||||||
/// @Dependency(\.gitVersionClient)
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - currentVersion: The closure that returns the current version.
|
|
||||||
///
|
|
||||||
public init(currentVersion: @escaping (String?) throws -> String) {
|
|
||||||
self.currentVersion = currentVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current version from the `git tag` in the given directory.
|
|
||||||
/// If a directory is not passed in, then we will use the current working directory.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - gitDirectory: The directory to run the command in.
|
|
||||||
public func currentVersion(in gitDirectory: String? = nil) throws -> String {
|
|
||||||
try self.currentVersion(gitDirectory)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Override the `currentVersion` command and return the passed in version string.
|
|
||||||
///
|
|
||||||
/// This is useful for testing purposes.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - version: The version string to return when `currentVersion` is called.
|
|
||||||
public mutating func override(with version: String) {
|
|
||||||
self.currentVersion = { _ in version }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GitVersionClient: TestDependencyKey {
|
|
||||||
|
|
||||||
/// The ``GitVersionClient`` used in test / debug builds.
|
|
||||||
public static let testValue = GitVersionClient(
|
|
||||||
currentVersion: unimplemented("\(Self.self).currentVersion", placeholder: "")
|
|
||||||
)
|
|
||||||
|
|
||||||
/// The ``GitVersionClient`` used in release builds.
|
|
||||||
public static var liveValue: GitVersionClient {
|
|
||||||
.init(currentVersion: { gitDirectory in
|
|
||||||
try GitVersion(workingDirectory: gitDirectory).currentVersion()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DependencyValues {
|
|
||||||
|
|
||||||
/// A ``GitVersionClient`` that can retrieve the current version from a
|
|
||||||
/// git directory.
|
|
||||||
public var gitVersionClient: GitVersionClient {
|
|
||||||
get { self[GitVersionClient.self] }
|
|
||||||
set { self[GitVersionClient.self] = newValue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ShellCommand {
|
|
||||||
public static func gitCurrentSha(gitDirectory: String? = nil) -> Self {
|
|
||||||
GitVersion(workingDirectory: gitDirectory).command(for: .commit)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func gitCurrentBranch(gitDirectory: String? = nil) -> Self {
|
|
||||||
GitVersion(workingDirectory: gitDirectory).command(for: .branch)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func gitCurrentTag(gitDirectory: String? = nil) -> Self {
|
|
||||||
GitVersion(workingDirectory: gitDirectory).command(for: .describe)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private
|
|
||||||
fileprivate struct GitVersion {
|
|
||||||
@Dependency(\.logger) var logger: Logger
|
|
||||||
@Dependency(\.shellClient) var shell: ShellClient
|
|
||||||
|
|
||||||
let workingDirectory: String?
|
|
||||||
|
|
||||||
func currentVersion() throws -> String {
|
|
||||||
logger.debug("\("Fetching current version".bold)")
|
|
||||||
do {
|
|
||||||
logger.debug("Checking for tag.")
|
|
||||||
return try run(command: command(for: .describe))
|
|
||||||
} catch {
|
|
||||||
logger.debug("\("No tag found, deferring to branch & git sha".red)")
|
|
||||||
let branch = try run(command: command(for: .branch))
|
|
||||||
let commit = try run(command: command(for: .commit))
|
|
||||||
return "\(branch) \(commit)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal func command(for argument: VersionArgs) -> ShellCommand {
|
|
||||||
.init(
|
|
||||||
shell: .env,
|
|
||||||
environment: nil,
|
|
||||||
in: workingDirectory ?? FileManager.default.currentDirectoryPath,
|
|
||||||
argument.arguments
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate extension GitVersion {
|
|
||||||
func run(command: ShellCommand) throws -> String {
|
|
||||||
try shell.background(command, trimmingCharactersIn: .whitespacesAndNewlines)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum VersionArgs {
|
|
||||||
case branch
|
|
||||||
case commit
|
|
||||||
case describe
|
|
||||||
|
|
||||||
var arguments: [Args] {
|
|
||||||
switch self {
|
|
||||||
case .branch:
|
|
||||||
return [.git, .symbolicRef, .quiet, .short, .head]
|
|
||||||
case .commit:
|
|
||||||
return [.git, .revParse, .short, .head]
|
|
||||||
case .describe:
|
|
||||||
return [.git, .describe, .tags, .exactMatch]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Args: String, CustomStringConvertible {
|
|
||||||
case git
|
|
||||||
case describe
|
|
||||||
case tags = "--tags"
|
|
||||||
case exactMatch = "--exact-match"
|
|
||||||
case quiet = "--quiet"
|
|
||||||
case symbolicRef = "symbolic-ref"
|
|
||||||
case revParse = "rev-parse"
|
|
||||||
case short = "--short"
|
|
||||||
case head = "HEAD"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension RawRepresentable where RawValue == String, Self: CustomStringConvertible {
|
|
||||||
var description: String { rawValue }
|
|
||||||
}
|
|
||||||
33
Sources/ConfigurationClient/Coders.swift
Normal file
33
Sources/ConfigurationClient/Coders.swift
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension DependencyValues {
|
||||||
|
var coders: Coders {
|
||||||
|
get { self[Coders.self] }
|
||||||
|
set { self[Coders.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DependencyClient
|
||||||
|
public struct Coders: Sendable {
|
||||||
|
public var jsonDecoder: @Sendable () -> JSONDecoder = { .init() }
|
||||||
|
public var jsonEncoder: @Sendable () -> JSONEncoder = { .init() }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Coders: DependencyKey {
|
||||||
|
public static var testValue: Coders {
|
||||||
|
.init(
|
||||||
|
jsonDecoder: { .init() },
|
||||||
|
jsonEncoder: { defaultJsonEncoder }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var liveValue: Coders { .testValue }
|
||||||
|
|
||||||
|
private static let defaultJsonEncoder: JSONEncoder = {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||||
|
return encoder
|
||||||
|
}()
|
||||||
|
}
|
||||||
76
Sources/ConfigurationClient/Configuration+merge.swift
Normal file
76
Sources/ConfigurationClient/Configuration+merge.swift
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import LoggingExtensions
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration {
|
||||||
|
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
mergingTarget(other?.target).mergingStrategy(other?.strategy)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mergingTarget(_ otherTarget: Configuration.Target?) -> Self {
|
||||||
|
.init(
|
||||||
|
target: otherTarget ?? target,
|
||||||
|
strategy: strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self {
|
||||||
|
.init(
|
||||||
|
target: target,
|
||||||
|
strategy: strategy?.merging(otherStrategy)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration.PreRelease {
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
return .init(
|
||||||
|
prefix: other?.prefix ?? prefix,
|
||||||
|
strategy: other?.strategy ?? strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration.Branch {
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration.SemVar {
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
logger.dump(other, level: .trace) { "Merging semvar:\n\($0)" }
|
||||||
|
|
||||||
|
return .init(
|
||||||
|
allowPreRelease: other?.allowPreRelease ?? allowPreRelease,
|
||||||
|
precedence: other?.precedence ?? precedence,
|
||||||
|
preRelease: preRelease == nil ? other?.preRelease : preRelease!.merging(other?.preRelease),
|
||||||
|
requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
|
||||||
|
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar,
|
||||||
|
strategy: other?.strategy ?? strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration.VersionStrategy {
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
guard let other else { return self }
|
||||||
|
|
||||||
|
switch other {
|
||||||
|
case .branch:
|
||||||
|
guard let branch else { return other }
|
||||||
|
return .branch(branch.merging(other.branch))
|
||||||
|
case .semvar:
|
||||||
|
guard let semvar else { return other }
|
||||||
|
return .semvar(semvar.merging(other.semvar))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
329
Sources/ConfigurationClient/Configuration.swift
Normal file
329
Sources/ConfigurationClient/Configuration.swift
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import CustomDump
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// TODO: Add pre-release strategy that just bumps an integer.
|
||||||
|
|
||||||
|
/// Represents configuration that can be set via a file, generally in the root of the
|
||||||
|
/// project directory.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
public struct Configuration: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// The target for the version.
|
||||||
|
public let target: Target?
|
||||||
|
|
||||||
|
/// The strategy used for deriving the version.
|
||||||
|
public let strategy: VersionStrategy?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
target: Target? = nil,
|
||||||
|
strategy: VersionStrategy? = .semvar(.init())
|
||||||
|
) {
|
||||||
|
self.target = target
|
||||||
|
self.strategy = strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var `default`: Self { .mock(module: "<my-tool>") }
|
||||||
|
|
||||||
|
public static func mock(module: String = "cli-version") -> Self {
|
||||||
|
.init(
|
||||||
|
target: .init(module: .init(module)),
|
||||||
|
strategy: .semvar(.init(strategy: .gitTag(exactMatch: false)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var customPreRelease: Self {
|
||||||
|
.init(
|
||||||
|
target: .init(module: .init("cli-version")),
|
||||||
|
strategy: .semvar(.init(
|
||||||
|
preRelease: .init(prefix: "rc", strategy: .branch())
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Configuration {
|
||||||
|
|
||||||
|
/// Represents the target where we will bump the version in.
|
||||||
|
///
|
||||||
|
/// This can either be a path to a version file or a module used to
|
||||||
|
/// locate the version file.
|
||||||
|
struct Target: Codable, Equatable, Sendable, CustomDumpReflectable {
|
||||||
|
|
||||||
|
/// The path to a version file.
|
||||||
|
public let path: String?
|
||||||
|
|
||||||
|
/// A module to find the version file in.
|
||||||
|
public let module: Module?
|
||||||
|
|
||||||
|
/// Create a target for the given path.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - path: The path to the version file.
|
||||||
|
public init(path: String) {
|
||||||
|
self.path = path
|
||||||
|
self.module = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a target for the given module.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - module: The module for the version file.
|
||||||
|
public init(module: Module) {
|
||||||
|
self.path = nil
|
||||||
|
self.module = module
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a module target for a version file.
|
||||||
|
///
|
||||||
|
public struct Module: Codable, Equatable, Sendable, CustomDumpReflectable {
|
||||||
|
|
||||||
|
/// The module directory name.
|
||||||
|
public let name: String
|
||||||
|
|
||||||
|
/// The version file name located in the module directory.
|
||||||
|
public let fileName: String?
|
||||||
|
|
||||||
|
/// Create a new module target.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The module directory name.
|
||||||
|
/// - fileName: The file name located in the module directory.
|
||||||
|
public init(
|
||||||
|
_ name: String,
|
||||||
|
fileName: String? = nil
|
||||||
|
) {
|
||||||
|
self.name = name
|
||||||
|
self.fileName = fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
public var fileNameOrDefault: String {
|
||||||
|
fileName ?? "Version.swift"
|
||||||
|
}
|
||||||
|
|
||||||
|
public var customDumpMirror: Mirror {
|
||||||
|
.init(
|
||||||
|
self,
|
||||||
|
children: [
|
||||||
|
"name": name,
|
||||||
|
"fileName": fileNameOrDefault
|
||||||
|
],
|
||||||
|
displayStyle: .struct
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public var customDumpMirror: Mirror {
|
||||||
|
guard let module else {
|
||||||
|
guard let path else { return .init(reflecting: self) }
|
||||||
|
return .init(
|
||||||
|
self,
|
||||||
|
children: [
|
||||||
|
"path": path
|
||||||
|
],
|
||||||
|
displayStyle: .struct
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return .init(
|
||||||
|
self,
|
||||||
|
children: [
|
||||||
|
"module": module
|
||||||
|
],
|
||||||
|
displayStyle: .struct
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strategy used to generate a version.
|
||||||
|
///
|
||||||
|
/// Typically a `SemVar` strategy or `Branch`.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
enum VersionStrategy: Codable, Equatable, Sendable, CustomDumpReflectable {
|
||||||
|
case branch(includeCommitSha: Bool = true)
|
||||||
|
|
||||||
|
case semvar(
|
||||||
|
allowPreRelease: Bool? = nil,
|
||||||
|
precedence: SemVar.Precedence? = nil,
|
||||||
|
preRelease: PreRelease? = nil,
|
||||||
|
requireExistingFile: Bool? = nil,
|
||||||
|
requireExistingSemVar: Bool? = nil,
|
||||||
|
strategy: SemVar.Strategy? = nil
|
||||||
|
)
|
||||||
|
|
||||||
|
public var branch: Branch? {
|
||||||
|
guard case let .branch(includeCommitSha) = self
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
return .init(includeCommitSha: includeCommitSha)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var semvar: SemVar? {
|
||||||
|
guard case let .semvar(
|
||||||
|
allowPreRelease,
|
||||||
|
precedence,
|
||||||
|
preRelease,
|
||||||
|
requireExistingFile, requireExistingSemVar, strategy
|
||||||
|
) = self
|
||||||
|
else { return nil }
|
||||||
|
return .init(
|
||||||
|
allowPreRelease: allowPreRelease,
|
||||||
|
precedence: precedence,
|
||||||
|
preRelease: preRelease,
|
||||||
|
requireExistingFile: requireExistingFile ?? false,
|
||||||
|
requireExistingSemVar: requireExistingSemVar ?? false,
|
||||||
|
strategy: strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func branch(_ branch: Branch) -> Self {
|
||||||
|
.branch(includeCommitSha: branch.includeCommitSha)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func semvar(_ value: SemVar) -> Self {
|
||||||
|
.semvar(
|
||||||
|
allowPreRelease: value.allowPreRelease,
|
||||||
|
precedence: value.precedence,
|
||||||
|
preRelease: value.preRelease,
|
||||||
|
requireExistingFile: value.requireExistingFile,
|
||||||
|
requireExistingSemVar: value.requireExistingSemVar,
|
||||||
|
strategy: value.strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var customDumpMirror: Mirror {
|
||||||
|
switch self {
|
||||||
|
case .branch:
|
||||||
|
return .init(self, children: ["branch": branch!], displayStyle: .struct)
|
||||||
|
case .semvar:
|
||||||
|
return .init(self, children: ["semvar": semvar!], displayStyle: .struct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a branch version or pre-release strategy.
|
||||||
|
///
|
||||||
|
/// This derives the version or pre-release suffix from the branch name and
|
||||||
|
/// optionally the short version of the commit sha.
|
||||||
|
struct Branch: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// Include the commit sha in the output for this strategy.
|
||||||
|
public let includeCommitSha: Bool
|
||||||
|
|
||||||
|
/// Create a new branch strategy.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - includeCommitSha: Whether to include the commit sha.
|
||||||
|
public init(includeCommitSha: Bool = true) {
|
||||||
|
self.includeCommitSha = includeCommitSha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents version strategy for pre-release.
|
||||||
|
///
|
||||||
|
/// This appends a suffix to the version that get's generated from the version strategy.
|
||||||
|
/// For example: `1.0.0-rc-1`
|
||||||
|
///
|
||||||
|
struct PreRelease: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let prefix: String?
|
||||||
|
public let strategy: Strategy?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
prefix: String? = nil,
|
||||||
|
strategy: Strategy? = nil
|
||||||
|
) {
|
||||||
|
self.prefix = prefix
|
||||||
|
self.strategy = strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Strategy: Codable, Equatable, Sendable {
|
||||||
|
case branch(includeCommitSha: Bool = true)
|
||||||
|
case command(arguments: [String], allowPrefix: Bool? = nil)
|
||||||
|
case gitTag
|
||||||
|
|
||||||
|
public var branch: Branch? {
|
||||||
|
guard case let .branch(includeCommitSha) = self
|
||||||
|
else { return nil }
|
||||||
|
return .init(includeCommitSha: includeCommitSha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a semvar version strategy.
|
||||||
|
///
|
||||||
|
/// ## Example: 1.0.0
|
||||||
|
///
|
||||||
|
struct SemVar: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// Allow semvar to include a pre-release suffix.
|
||||||
|
public let allowPreRelease: Bool?
|
||||||
|
|
||||||
|
/// Set the precedence of version loaded from file versus
|
||||||
|
/// the version returned by the strategy.
|
||||||
|
///
|
||||||
|
/// These can not always agree / reflect the same version,
|
||||||
|
/// so the default is to give the file version precedence over
|
||||||
|
/// the strategy.
|
||||||
|
public let precedence: Precedence?
|
||||||
|
|
||||||
|
/// Optional pre-releas suffix strategy.
|
||||||
|
public let preRelease: PreRelease?
|
||||||
|
|
||||||
|
/// Fail if an existing version file does not exist in the target.
|
||||||
|
public let requireExistingFile: Bool?
|
||||||
|
|
||||||
|
/// Fail if an existing semvar is not parsed from the file or version generation strategy.
|
||||||
|
public let requireExistingSemVar: Bool?
|
||||||
|
|
||||||
|
/// The strategy used to derive a version for the project.
|
||||||
|
public let strategy: Strategy?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
allowPreRelease: Bool? = true,
|
||||||
|
precedence: Precedence? = nil,
|
||||||
|
preRelease: PreRelease? = nil,
|
||||||
|
requireExistingFile: Bool? = false,
|
||||||
|
requireExistingSemVar: Bool? = false,
|
||||||
|
strategy: Strategy? = nil
|
||||||
|
) {
|
||||||
|
self.allowPreRelease = allowPreRelease
|
||||||
|
self.precedence = precedence
|
||||||
|
self.preRelease = preRelease
|
||||||
|
self.requireExistingFile = requireExistingFile
|
||||||
|
self.requireExistingSemVar = requireExistingSemVar
|
||||||
|
self.strategy = strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a strategy to derive a version for a project.
|
||||||
|
public enum Strategy: Codable, Equatable, Sendable {
|
||||||
|
/// A custom external command that should return a string that
|
||||||
|
/// can be parsed as a semvar.
|
||||||
|
case command(arguments: [String])
|
||||||
|
|
||||||
|
/// Use `git describe --tags` optionally as an exact match.
|
||||||
|
case gitTag(exactMatch: Bool? = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the precedence for which version to use when a file
|
||||||
|
/// exists, as they don't always agree. For example, a file could be edited
|
||||||
|
/// manually or the tag doesn't represent what is parsed from calling the
|
||||||
|
/// strategy.
|
||||||
|
///
|
||||||
|
/// The default is to defer to the file (if it exists) as having precedence over
|
||||||
|
/// the strategy.
|
||||||
|
public enum Precedence: String, CaseIterable, Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// Give the file precedence over the strategy.
|
||||||
|
case file
|
||||||
|
/// Give the strategy precedence over the file.
|
||||||
|
case strategy
|
||||||
|
|
||||||
|
/// The default precedence.
|
||||||
|
public static var `default`: Self { .file }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
133
Sources/ConfigurationClient/ConfigurationClient.swift
Normal file
133
Sources/ConfigurationClient/ConfigurationClient.swift
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// TODO: Add a method to get a semvar / handle a version strategy's ??
|
||||||
|
|
||||||
|
public extension DependencyValues {
|
||||||
|
|
||||||
|
/// Perform operations with configuration files.
|
||||||
|
var configurationClient: ConfigurationClient {
|
||||||
|
get { self[ConfigurationClient.self] }
|
||||||
|
set { self[ConfigurationClient.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles interactions with configuration files.
|
||||||
|
@DependencyClient
|
||||||
|
public struct ConfigurationClient: Sendable {
|
||||||
|
|
||||||
|
fileprivate enum Constants {
|
||||||
|
static let defaultFileNameWithoutExtension = ".bump-version"
|
||||||
|
static let defaultExtension = "json"
|
||||||
|
static var defaultFileName: String { "\(defaultFileNameWithoutExtension).\(defaultExtension)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default file name for a configuration file.
|
||||||
|
public var defaultFileName: @Sendable () -> String = { "test.json" }
|
||||||
|
|
||||||
|
/// Find a configuration file in the given directory or in current working directory.
|
||||||
|
public var find: @Sendable (URL?) async throws -> URL?
|
||||||
|
|
||||||
|
/// Load a configuration file.
|
||||||
|
public var load: @Sendable (URL) async throws -> Configuration
|
||||||
|
|
||||||
|
/// Write a configuration file.
|
||||||
|
public var write: @Sendable (Configuration, URL) async throws -> Void
|
||||||
|
|
||||||
|
/// Find a configuration file and load it if found.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - url: The optional path to the configuration file.
|
||||||
|
/// - strict: Fail if a configuration file is not found, otherwise return default configuration.
|
||||||
|
public func findAndLoad(_ url: URL? = nil, strict: Bool = true) async throws -> Configuration {
|
||||||
|
guard let url = try? await find(url) else {
|
||||||
|
if strict {
|
||||||
|
throw ConfigurationClientError.configurationNotFound
|
||||||
|
}
|
||||||
|
return .default
|
||||||
|
}
|
||||||
|
|
||||||
|
let loaded = try? await load(url)
|
||||||
|
guard let loaded else {
|
||||||
|
if strict {
|
||||||
|
throw ConfigurationClientError.configurationNotFound
|
||||||
|
}
|
||||||
|
return .default
|
||||||
|
}
|
||||||
|
return loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads configuration from the given path, or searches for the default file and loads it.
|
||||||
|
/// Optionally merges other configuration, then perform an operation with the loaded configuration.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - path: Optional file path of the configuration to load.
|
||||||
|
/// - other: Optional configuration to merge with the loaded configuration.
|
||||||
|
/// - operation: The operation to perform with the loaded configuration.
|
||||||
|
@discardableResult
|
||||||
|
public func withConfiguration<T>(
|
||||||
|
path: String?,
|
||||||
|
merging other: Configuration? = nil,
|
||||||
|
strict: Bool = true,
|
||||||
|
operation: (Configuration) async throws -> T
|
||||||
|
) async throws -> T {
|
||||||
|
let configuration = try await findAndLoad(
|
||||||
|
path != nil ? URL(filePath: path!) : nil,
|
||||||
|
strict: strict
|
||||||
|
)
|
||||||
|
return try await operation(configuration.merging(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfigurationClient: DependencyKey {
|
||||||
|
public static let testValue: ConfigurationClient = Self()
|
||||||
|
|
||||||
|
public static var liveValue: ConfigurationClient {
|
||||||
|
.init(
|
||||||
|
defaultFileName: { "\(Constants.defaultFileNameWithoutExtension).json" },
|
||||||
|
find: { try await findConfiguration($0) },
|
||||||
|
load: { try await loadConfiguration($0) },
|
||||||
|
write: { try await writeConfiguration($0, to: $1) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findConfiguration(_ url: URL?) async throws -> URL? {
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
|
||||||
|
var url: URL! = url
|
||||||
|
if url == nil {
|
||||||
|
url = try await URL(filePath: fileClient.currentDirectory())
|
||||||
|
}
|
||||||
|
|
||||||
|
if try await fileClient.isDirectory(url.cleanFilePath) {
|
||||||
|
url = url.appending(path: ConfigurationClient.Constants.defaultFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileClient.fileExists(url) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadConfiguration(_ url: URL) async throws -> Configuration {
|
||||||
|
@Dependency(\.coders.jsonDecoder) var jsonDecoder
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
|
||||||
|
let string = try await fileClient.read(url.cleanFilePath)
|
||||||
|
return try jsonDecoder().decode(Configuration.self, from: Data(string.utf8))
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConfigurationClientError: Error {
|
||||||
|
case configurationNotFound
|
||||||
|
case invalidConfigurationDirectory(path: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeConfiguration(_ configuration: Configuration, to url: URL) async throws {
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
@Dependency(\.coders.jsonEncoder) var jsonEncoder
|
||||||
|
let data = try jsonEncoder().encode(configuration)
|
||||||
|
try await fileClient.write(data, url)
|
||||||
|
}
|
||||||
4
Sources/ConfigurationClient/Constants.swift
Normal file
4
Sources/ConfigurationClient/Constants.swift
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension ConfigurationClient {
|
||||||
|
}
|
||||||
138
Sources/FileClient/FileClient.swift
Normal file
138
Sources/FileClient/FileClient.swift
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension DependencyValues {
|
||||||
|
|
||||||
|
/// Access a basic ``FileClient`` that can read / write data to the file system.
|
||||||
|
///
|
||||||
|
var fileClient: FileClient {
|
||||||
|
get { self[FileClient.self] }
|
||||||
|
set { self[FileClient.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the interactions with the file system. It is able
|
||||||
|
/// to read from and write to files.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// @Dependency(\.fileClient) var fileClient
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
@DependencyClient
|
||||||
|
public struct FileClient: Sendable {
|
||||||
|
|
||||||
|
/// Return the current working directory.
|
||||||
|
public var currentDirectory: @Sendable () async throws -> String
|
||||||
|
|
||||||
|
/// Check if a file exists at the given url.
|
||||||
|
public var fileExists: @Sendable (URL) -> Bool = { _ in true }
|
||||||
|
|
||||||
|
/// Check if a url is a directory.
|
||||||
|
public var isDirectory: @Sendable (String) async throws -> Bool
|
||||||
|
|
||||||
|
/// Read the contents of a file.
|
||||||
|
public var read: @Sendable (URL) async throws -> String
|
||||||
|
|
||||||
|
/// Write `Data` to a file `URL`.
|
||||||
|
public var write: @Sendable (Data, URL) async throws -> Void
|
||||||
|
|
||||||
|
/// Read the contents of a file at the given path.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - path: The file path to read from.
|
||||||
|
public func read(_ path: String) async throws -> String {
|
||||||
|
try await read(url(for: path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write's the the string to a file path.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - string: The string to write to the file.
|
||||||
|
/// - path: The file path.
|
||||||
|
public func write(string: String, to path: String) async throws {
|
||||||
|
try await write(string: string, to: url(for: path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write's the the string to a file path.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - string: The string to write to the file.
|
||||||
|
/// - url: The file url.
|
||||||
|
public func write(string: String, to url: URL) async throws {
|
||||||
|
try await write(Data(string.utf8), url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FileClient: DependencyKey {
|
||||||
|
|
||||||
|
/// A ``FileClient`` that does not do anything.
|
||||||
|
public static let noop = FileClient(
|
||||||
|
currentDirectory: { "./" },
|
||||||
|
fileExists: { _ in true },
|
||||||
|
isDirectory: { _ in true },
|
||||||
|
read: { _ in "" },
|
||||||
|
write: { _, _ in }
|
||||||
|
)
|
||||||
|
|
||||||
|
/// An `unimplemented` ``FileClient``.
|
||||||
|
public static let testValue = FileClient()
|
||||||
|
|
||||||
|
/// The live ``FileClient``
|
||||||
|
public static let liveValue = FileClient(
|
||||||
|
currentDirectory: { FileManager.default.currentDirectoryPath },
|
||||||
|
fileExists: { FileManager.default.fileExists(atPath: $0.cleanFilePath) },
|
||||||
|
isDirectory: { path in
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
|
||||||
|
return isDirectory.boolValue
|
||||||
|
},
|
||||||
|
read: { try String(contentsOf: $0, encoding: .utf8) },
|
||||||
|
write: { try $0.write(to: $1, options: .atomic) }
|
||||||
|
)
|
||||||
|
|
||||||
|
public static func capturing(
|
||||||
|
_ captured: CapturingWrite
|
||||||
|
) -> Self {
|
||||||
|
.init(
|
||||||
|
currentDirectory: { "./" },
|
||||||
|
fileExists: { _ in true },
|
||||||
|
isDirectory: { _ in true },
|
||||||
|
read: { _ in "" },
|
||||||
|
write: { await captured.set($0, $1) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public actor CapturingWrite: Sendable {
|
||||||
|
public private(set) var data: Data?
|
||||||
|
public private(set) var url: URL?
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
func set(_ data: Data, _ url: URL) {
|
||||||
|
self.data = data
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension URL {
|
||||||
|
var cleanFilePath: String {
|
||||||
|
absoluteString.replacingOccurrences(of: "file://", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func url(for path: String) -> URL {
|
||||||
|
#if os(Linux)
|
||||||
|
return URL(fileURLWithPath: path)
|
||||||
|
#else
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
return URL(filePath: path)
|
||||||
|
} else {
|
||||||
|
// Fallback on earlier versions
|
||||||
|
return URL(fileURLWithPath: path)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
262
Sources/GitClient/GitClient.swift
Normal file
262
Sources/GitClient/GitClient.swift
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
public extension DependencyValues {
|
||||||
|
|
||||||
|
/// A ``GitVersionClient`` that can retrieve the current version from a
|
||||||
|
/// git directory.
|
||||||
|
var gitClient: GitClient {
|
||||||
|
get { self[GitClient.self] }
|
||||||
|
set { self[GitClient.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A client that can retrieve the current version from a git directory.
|
||||||
|
/// It will use the current `tag`, or if the current git tree does not
|
||||||
|
/// point to a commit that is tagged, it will use the `branch git-sha` as
|
||||||
|
/// the version.
|
||||||
|
///
|
||||||
|
/// This is often not used directly, instead it is used with one of the plugins
|
||||||
|
/// that is supplied with this library. The use case is to set the version of a command line
|
||||||
|
/// tool based on the current git tag.
|
||||||
|
///
|
||||||
|
@DependencyClient
|
||||||
|
public struct GitClient: Sendable {
|
||||||
|
|
||||||
|
/// The closure to run that returns the current version from a given
|
||||||
|
/// git directory.
|
||||||
|
@available(*, deprecated, message: "Use version.")
|
||||||
|
public var currentVersion: @Sendable (String?, Bool) async throws -> String
|
||||||
|
|
||||||
|
/// Get the current version from the `git tag` in the given directory.
|
||||||
|
/// If a directory is not passed in, then we will use the current working directory.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - gitDirectory: The directory to run the command in.
|
||||||
|
@available(*, deprecated, message: "Use version.")
|
||||||
|
public func currentVersion(in gitDirectory: String? = nil, exactMatch: Bool = true) async throws -> String {
|
||||||
|
try await currentVersion(gitDirectory, exactMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var version: @Sendable (CurrentVersionOption) async throws -> Version
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension GitClient {
|
||||||
|
struct CurrentVersionOption: Sendable {
|
||||||
|
let gitDirectory: String?
|
||||||
|
let style: Style
|
||||||
|
|
||||||
|
public init(
|
||||||
|
gitDirectory: String? = nil,
|
||||||
|
style: Style
|
||||||
|
) {
|
||||||
|
self.gitDirectory = gitDirectory
|
||||||
|
self.style = style
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Style: Sendable {
|
||||||
|
case tag(exactMatch: Bool = false)
|
||||||
|
case branch(commitSha: Bool = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Version: Sendable, CustomStringConvertible {
|
||||||
|
case branch(String)
|
||||||
|
case tag(String)
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
switch self {
|
||||||
|
case let .branch(string): return string
|
||||||
|
case let .tag(string): return string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GitClient: TestDependencyKey {
|
||||||
|
|
||||||
|
/// The ``GitVersionClient`` used in test / debug builds.
|
||||||
|
public static let testValue = GitClient()
|
||||||
|
|
||||||
|
/// The ``GitVersionClient`` used in release builds.
|
||||||
|
public static var liveValue: GitClient {
|
||||||
|
.init(
|
||||||
|
currentVersion: { gitDirectory, exactMatch in
|
||||||
|
try await GitVersion(workingDirectory: gitDirectory).currentVersion(exactMatch)
|
||||||
|
},
|
||||||
|
version: { try await $0.run() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a mock git client, that always returns the given value.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - value: The value to return.
|
||||||
|
public static func mock(_ value: Version) -> Self {
|
||||||
|
.init(
|
||||||
|
currentVersion: { _, _ in value.description },
|
||||||
|
version: { _ in value }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private extension GitClient.CurrentVersionOption {
|
||||||
|
|
||||||
|
func run() async throws -> GitClient.Version {
|
||||||
|
switch style {
|
||||||
|
case let .tag(exactMatch: exactMatch):
|
||||||
|
return try await .tag(runCommand(.describeTag(exactMatch: exactMatch)))
|
||||||
|
case let .branch(commitSha: withCommit):
|
||||||
|
async let branch = try await runCommand(.branch)
|
||||||
|
|
||||||
|
if withCommit {
|
||||||
|
let commit = try await runCommand(.commit)
|
||||||
|
return try await .branch("\(branch)-\(commit)")
|
||||||
|
}
|
||||||
|
return try await .branch(branch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommand(_ versionArgs: VersionArgs) async throws -> String {
|
||||||
|
@Dependency(\.asyncShellClient) var shell
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
|
||||||
|
var gitDirectory: String! = self.gitDirectory
|
||||||
|
if gitDirectory == nil {
|
||||||
|
gitDirectory = try await fileClient.currentDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await shell.background(
|
||||||
|
.init(
|
||||||
|
shell: .env,
|
||||||
|
environment: ProcessInfo.processInfo.environment,
|
||||||
|
in: gitDirectory,
|
||||||
|
versionArgs().map(\.rawValue)
|
||||||
|
),
|
||||||
|
trimmingCharactersIn: .whitespacesAndNewlines
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VersionArgs {
|
||||||
|
case branch
|
||||||
|
case commit
|
||||||
|
case describeTag(exactMatch: Bool)
|
||||||
|
|
||||||
|
func callAsFunction() -> [Args] {
|
||||||
|
switch self {
|
||||||
|
case .branch:
|
||||||
|
return [.git, .symbolicRef, .quiet, .short, .head]
|
||||||
|
case .commit:
|
||||||
|
return [.git, .revParse, .short, .head]
|
||||||
|
case let .describeTag(exactMatch):
|
||||||
|
var args = [Args.git, .describe, .tags]
|
||||||
|
if exactMatch {
|
||||||
|
args.append(.exactMatch)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Args: String, CustomStringConvertible {
|
||||||
|
case git
|
||||||
|
case describe
|
||||||
|
case tags = "--tags"
|
||||||
|
case exactMatch = "--exact-match"
|
||||||
|
case quiet = "--quiet"
|
||||||
|
case symbolicRef = "symbolic-ref"
|
||||||
|
case revParse = "rev-parse"
|
||||||
|
case short = "--short"
|
||||||
|
case head = "HEAD"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct GitVersion {
|
||||||
|
@Dependency(\.logger) var logger: Logger
|
||||||
|
@Dependency(\.asyncShellClient) var shell
|
||||||
|
|
||||||
|
let workingDirectory: String?
|
||||||
|
|
||||||
|
func currentVersion(_ exactMatch: Bool) async throws -> String {
|
||||||
|
logger.debug("\("Fetching current version".bold)")
|
||||||
|
do {
|
||||||
|
logger.debug("Checking for tag.")
|
||||||
|
return try await run(command: command(for: .describe(exactMatch: exactMatch)))
|
||||||
|
} catch {
|
||||||
|
logger.debug("\("No tag found, deferring to branch & git sha".red)")
|
||||||
|
let branch = try await run(command: command(for: .branch))
|
||||||
|
let commit = try await run(command: command(for: .commit))
|
||||||
|
return "\(branch) \(commit)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func command(for argument: VersionArgs) -> ShellCommand {
|
||||||
|
.init(
|
||||||
|
shell: .env,
|
||||||
|
environment: nil,
|
||||||
|
in: workingDirectory ?? FileManager.default.currentDirectoryPath,
|
||||||
|
argument.arguments.map(\.rawValue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(command: ShellCommand) async throws -> String {
|
||||||
|
try await shell.background(command, trimmingCharactersIn: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VersionArgs {
|
||||||
|
case branch
|
||||||
|
case commit
|
||||||
|
case describe(exactMatch: Bool)
|
||||||
|
|
||||||
|
var arguments: [Args] {
|
||||||
|
switch self {
|
||||||
|
case .branch:
|
||||||
|
return [.git, .symbolicRef, .quiet, .short, .head]
|
||||||
|
case .commit:
|
||||||
|
return [.git, .revParse, .short, .head]
|
||||||
|
case let .describe(exactMatch):
|
||||||
|
var args = [Args.git, .describe, .tags]
|
||||||
|
if exactMatch {
|
||||||
|
args.append(.exactMatch)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Args: String, CustomStringConvertible {
|
||||||
|
case git
|
||||||
|
case describe
|
||||||
|
case tags = "--tags"
|
||||||
|
case exactMatch = "--exact-match"
|
||||||
|
case quiet = "--quiet"
|
||||||
|
case symbolicRef = "symbolic-ref"
|
||||||
|
case revParse = "rev-parse"
|
||||||
|
case short = "--short"
|
||||||
|
case head = "HEAD"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RawRepresentable where RawValue == String, Self: CustomStringConvertible {
|
||||||
|
var description: String { rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension GitClient.Version {
|
||||||
|
static var mocks: [Self] {
|
||||||
|
[
|
||||||
|
.tag("1.0.0"),
|
||||||
|
.tag("1.0.0-4-g59bc977"),
|
||||||
|
.branch("dev-g59bc977")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
156
Sources/LoggingExtensions/Formatter.swift
Normal file
156
Sources/LoggingExtensions/Formatter.swift
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import LoggingFormatAndPipe
|
||||||
|
import Rainbow
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
// MARK: Custom colors.
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
var orange: Self {
|
||||||
|
bit24(255, 165, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var magenta: Self {
|
||||||
|
bit24(238, 130, 238)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Logger.Level {
|
||||||
|
|
||||||
|
var coloredString: String {
|
||||||
|
switch self {
|
||||||
|
case .info:
|
||||||
|
return "\(self)".cyan
|
||||||
|
case .warning:
|
||||||
|
return "\(self)".orange.bold
|
||||||
|
case .debug:
|
||||||
|
return "\(self)".green
|
||||||
|
case .trace:
|
||||||
|
return "\(self)".yellow
|
||||||
|
case .error:
|
||||||
|
return "\(self)".red.bold
|
||||||
|
default:
|
||||||
|
return "\(self)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct LevelFormatter: LoggingFormatAndPipe.Formatter {
|
||||||
|
|
||||||
|
let basic: BasicFormatter
|
||||||
|
|
||||||
|
var timestampFormatter: DateFormatter { basic.timestampFormatter }
|
||||||
|
|
||||||
|
// swiftlint:disable function_parameter_count
|
||||||
|
func processLog(
|
||||||
|
level: Logger.Level,
|
||||||
|
message: Logger.Message,
|
||||||
|
prettyMetadata: String?,
|
||||||
|
file: String,
|
||||||
|
function: String,
|
||||||
|
line: UInt
|
||||||
|
) -> String {
|
||||||
|
let now = Date()
|
||||||
|
|
||||||
|
return basic.format.map { component -> String in
|
||||||
|
return processComponent(
|
||||||
|
component,
|
||||||
|
now: now,
|
||||||
|
level: level,
|
||||||
|
message: message,
|
||||||
|
prettyMetadata: prettyMetadata,
|
||||||
|
file: file,
|
||||||
|
function: function,
|
||||||
|
line: line
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.filter { $0.count > 0 }
|
||||||
|
.joined(separator: basic.separator ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func processComponent(
|
||||||
|
_ component: LogComponent,
|
||||||
|
now: Date,
|
||||||
|
level: Logger.Level,
|
||||||
|
message: Logger.Message,
|
||||||
|
prettyMetadata: String?,
|
||||||
|
file: String,
|
||||||
|
function: String,
|
||||||
|
line: UInt
|
||||||
|
) -> String {
|
||||||
|
switch component {
|
||||||
|
case .level:
|
||||||
|
let maxLen = "\(Logger.Level.warning)".count
|
||||||
|
let paddingCount = (maxLen - "\(level)".count) / 2
|
||||||
|
var padding = ""
|
||||||
|
for _ in 0 ... paddingCount {
|
||||||
|
padding += " "
|
||||||
|
}
|
||||||
|
return "\(padding)\(level.coloredString)\(padding)"
|
||||||
|
case let .group(components):
|
||||||
|
return components.map { component -> String in
|
||||||
|
self.processComponent(
|
||||||
|
component,
|
||||||
|
now: now,
|
||||||
|
level: level,
|
||||||
|
message: message,
|
||||||
|
prettyMetadata: prettyMetadata,
|
||||||
|
file: file,
|
||||||
|
function: function,
|
||||||
|
line: line
|
||||||
|
)
|
||||||
|
}.joined()
|
||||||
|
case .message:
|
||||||
|
return basic.processComponent(
|
||||||
|
component,
|
||||||
|
now: now,
|
||||||
|
level: level,
|
||||||
|
message: message,
|
||||||
|
prettyMetadata: prettyMetadata,
|
||||||
|
file: file,
|
||||||
|
function: function,
|
||||||
|
line: line
|
||||||
|
).italic
|
||||||
|
default:
|
||||||
|
return basic.processComponent(
|
||||||
|
component,
|
||||||
|
now: now,
|
||||||
|
level: level,
|
||||||
|
message: message,
|
||||||
|
prettyMetadata: prettyMetadata,
|
||||||
|
file: file,
|
||||||
|
function: function,
|
||||||
|
line: line
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// swiftlint:enable function_parameter_count
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LoggingOptions {
|
||||||
|
|
||||||
|
func makeLogger() -> Logger {
|
||||||
|
let formatters: [LogComponent] = [
|
||||||
|
.text(executableName.magenta),
|
||||||
|
.text(command.blue),
|
||||||
|
.level,
|
||||||
|
.group([
|
||||||
|
.text("-> "),
|
||||||
|
.message
|
||||||
|
])
|
||||||
|
]
|
||||||
|
return Logger(label: executableName) { _ in
|
||||||
|
LoggingFormatAndPipe.Handler(
|
||||||
|
formatter: LevelFormatter(basic: BasicFormatter(
|
||||||
|
formatters,
|
||||||
|
separator: " | "
|
||||||
|
)),
|
||||||
|
pipe: LoggerTextOutputStreamPipe.standardOutput
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
Sources/LoggingExtensions/LogLevel+verbose.swift
Normal file
13
Sources/LoggingExtensions/LogLevel+verbose.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Logging
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Logger.Level {
|
||||||
|
|
||||||
|
init(verbose: Int) {
|
||||||
|
switch verbose {
|
||||||
|
case 1: self = .debug
|
||||||
|
case 2...: self = .trace
|
||||||
|
default: self = .info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Sources/LoggingExtensions/LoggingOptions.swift
Normal file
42
Sources/LoggingExtensions/LoggingOptions.swift
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import CustomDump
|
||||||
|
import Dependencies
|
||||||
|
import Logging
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
public struct LoggingOptions: Equatable, Sendable {
|
||||||
|
|
||||||
|
let command: String
|
||||||
|
let executableName: String
|
||||||
|
let verbose: Int
|
||||||
|
|
||||||
|
public init(
|
||||||
|
executableName: String = "bump-version",
|
||||||
|
command: String,
|
||||||
|
verbose: Int
|
||||||
|
) {
|
||||||
|
self.executableName = executableName
|
||||||
|
self.command = command
|
||||||
|
self.verbose = verbose
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withLogger<T>(_ operation: () async throws -> T) async rethrows -> T {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.logger = makeLogger()
|
||||||
|
$0.logger.logLevel = .init(verbose: verbose)
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Logger {
|
||||||
|
func dump<T>(
|
||||||
|
_ type: T,
|
||||||
|
level: Level = .trace,
|
||||||
|
buildMessage: @escaping (String) -> String = { $0 }
|
||||||
|
) {
|
||||||
|
var message = ""
|
||||||
|
customDump(type, to: &message)
|
||||||
|
log(level: level, "\(buildMessage(message))")
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Sources/TestSupport/TestSupport.swift
Normal file
23
Sources/TestSupport/TestSupport.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// swiftlint:disable force_try
|
||||||
|
|
||||||
|
/// Helper to create a temporary directory for running tests in.
|
||||||
|
///
|
||||||
|
/// The temporary directory will be removed after the operation has ran.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - operation: The operation to run with the temporary directory.
|
||||||
|
public func withTemporaryDirectory(
|
||||||
|
_ operation: (URL) async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
let tempUrl = FileManager.default
|
||||||
|
.temporaryDirectory
|
||||||
|
.appendingPathComponent(UUID().uuidString)
|
||||||
|
|
||||||
|
try! FileManager.default.createDirectory(at: tempUrl, withIntermediateDirectories: false)
|
||||||
|
try await operation(tempUrl)
|
||||||
|
try! FileManager.default.removeItem(at: tempUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:enable force_try
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import ArgumentParser
|
|
||||||
import Foundation
|
|
||||||
import CliVersion
|
|
||||||
import ShellClient
|
|
||||||
|
|
||||||
extension CliVersionCommand {
|
|
||||||
struct Build: ParsableCommand {
|
|
||||||
static var configuration: CommandConfiguration = .init(
|
|
||||||
abstract: "Used for the build with version plugin.",
|
|
||||||
discussion: "This should generally not be interacted with directly, outside of the build plugin."
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptionGroup var shared: SharedOptions
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
name: .customLong("git-directory"),
|
|
||||||
help: "The git directory for the version."
|
|
||||||
)
|
|
||||||
var gitDirectory: String
|
|
||||||
|
|
||||||
func run() throws {
|
|
||||||
try withDependencies {
|
|
||||||
$0.logger.logLevel = .debug
|
|
||||||
$0.fileClient = .liveValue
|
|
||||||
$0.gitVersionClient = .liveValue
|
|
||||||
} operation: {
|
|
||||||
@Dependency(\.gitVersionClient) var gitVersion
|
|
||||||
@Dependency(\.fileClient) var fileClient
|
|
||||||
@Dependency(\.logger) var logger: Logger
|
|
||||||
|
|
||||||
logger.info("Building with git-directory: \(gitDirectory)")
|
|
||||||
|
|
||||||
let fileUrl = URL(fileURLWithPath: shared.target)
|
|
||||||
.appendingPathComponent(shared.fileName)
|
|
||||||
|
|
||||||
let fileString = fileUrl.fileString()
|
|
||||||
logger.info("File Url: \(fileString)")
|
|
||||||
|
|
||||||
let currentVersion = try gitVersion.currentVersion(in: gitDirectory)
|
|
||||||
|
|
||||||
let fileContents = buildTemplate
|
|
||||||
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
|
|
||||||
|
|
||||||
try fileClient.write(string: fileContents, to: fileUrl)
|
|
||||||
logger.info("Updated version file: \(fileString)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import ArgumentParser
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct CliVersionCommand: ParsableCommand {
|
|
||||||
|
|
||||||
static var configuration: CommandConfiguration = .init(
|
|
||||||
commandName: "cli-version",
|
|
||||||
version: VERSION ?? "0.0.0",
|
|
||||||
subcommands: [
|
|
||||||
Build.self,
|
|
||||||
Generate.self,
|
|
||||||
Update.self
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import ArgumentParser
|
|
||||||
import Dependencies
|
|
||||||
import Foundation
|
|
||||||
import CliVersion
|
|
||||||
import ShellClient
|
|
||||||
|
|
||||||
extension CliVersionCommand {
|
|
||||||
|
|
||||||
struct Generate: ParsableCommand {
|
|
||||||
static var configuration: CommandConfiguration = .init(
|
|
||||||
abstract: "Generates a version file in a command line tool that can be set via the git tag or git sha.",
|
|
||||||
discussion: "This command can be interacted with directly, outside of the plugin usage context.",
|
|
||||||
version: VERSION ?? "0.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptionGroup var shared: SharedOptions
|
|
||||||
|
|
||||||
func run() throws {
|
|
||||||
@Dependency(\.logger) var logger: Logger
|
|
||||||
@Dependency(\.fileClient) var fileClient
|
|
||||||
|
|
||||||
let targetUrl = parseTarget(shared.target)
|
|
||||||
let fileUrl = targetUrl.appendingPathComponent(shared.fileName)
|
|
||||||
|
|
||||||
let fileString = fileUrl.fileString()
|
|
||||||
|
|
||||||
guard !FileManager.default.fileExists(atPath: fileUrl.absoluteString) else {
|
|
||||||
logger.info("File already exists at path.")
|
|
||||||
throw GenerationError.fileExists(path: fileString)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !shared.dryRun {
|
|
||||||
try fileClient.write(string: optionalTemplate, to: fileUrl)
|
|
||||||
logger.info("Generated file at: \(fileString)")
|
|
||||||
} else {
|
|
||||||
logger.info("Would generate file at: \(fileString)")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate enum GenerationError: Error {
|
|
||||||
case fileExists(path: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import ArgumentParser
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
func parseTarget(_ target: String) -> URL {
|
|
||||||
let url = URL(fileURLWithPath: target)
|
|
||||||
let urlTest = url
|
|
||||||
.deletingLastPathComponent()
|
|
||||||
|
|
||||||
guard urlTest.lastPathComponent == "Sources" else {
|
|
||||||
return URL(fileURLWithPath: "Sources")
|
|
||||||
.appendingPathComponent(target)
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
extension URL {
|
|
||||||
func fileString() -> String {
|
|
||||||
self.absoluteString
|
|
||||||
.replacingOccurrences(of: "file://", with: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let optionalTemplate = """
|
|
||||||
// Do not set this variable, it is set during the build process.
|
|
||||||
let VERSION: String? = nil
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
let buildTemplate = """
|
|
||||||
// Do not set this variable, it is set during the build process.
|
|
||||||
let VERSION: String = nil
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
struct SharedOptions: ParsableArguments {
|
|
||||||
|
|
||||||
@Argument(help: "The target for the version file.")
|
|
||||||
var target: String
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
name: .customLong("filename"),
|
|
||||||
help: "Specify the file name for the version file."
|
|
||||||
)
|
|
||||||
var fileName: String = "Version.swift"
|
|
||||||
|
|
||||||
@Flag(name: .customLong("dry-run"))
|
|
||||||
var dryRun: Bool = false
|
|
||||||
|
|
||||||
@Flag(name: .long, help: "Increase logging level.")
|
|
||||||
var verbose: Bool = false
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import ArgumentParser
|
|
||||||
import Foundation
|
|
||||||
import CliVersion
|
|
||||||
import ShellClient
|
|
||||||
|
|
||||||
extension CliVersionCommand {
|
|
||||||
|
|
||||||
struct Update: ParsableCommand {
|
|
||||||
static var configuration: CommandConfiguration = .init(
|
|
||||||
abstract: "Updates a version string to the git tag or git sha.",
|
|
||||||
discussion: "This command can be interacted with directly outside of the plugin context."
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptionGroup var shared: SharedOptions
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
name: .customLong("git-directory"),
|
|
||||||
help: "The git directory for the version."
|
|
||||||
)
|
|
||||||
var gitDirectory: String? = nil
|
|
||||||
|
|
||||||
func run() throws {
|
|
||||||
try withDependencies {
|
|
||||||
$0.logger.logLevel = shared.verbose ? .debug : .info
|
|
||||||
$0.fileClient = .liveValue
|
|
||||||
$0.gitVersionClient = .liveValue
|
|
||||||
$0.shellClient = .liveValue
|
|
||||||
} operation: {
|
|
||||||
@Dependency(\.gitVersionClient) var gitVersion
|
|
||||||
@Dependency(\.fileClient) var fileClient
|
|
||||||
@Dependency(\.logger) var logger
|
|
||||||
@Dependency(\.shellClient) var shell
|
|
||||||
|
|
||||||
let targetUrl = parseTarget(shared.target)
|
|
||||||
let fileUrl = targetUrl
|
|
||||||
.appendingPathComponent(shared.fileName)
|
|
||||||
|
|
||||||
let fileString = fileUrl.fileString()
|
|
||||||
|
|
||||||
let currentVersion = try gitVersion.currentVersion(in: gitDirectory)
|
|
||||||
|
|
||||||
let fileContents = optionalTemplate
|
|
||||||
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
|
|
||||||
|
|
||||||
if !shared.dryRun {
|
|
||||||
try fileClient.write(string: fileContents, to: fileUrl)
|
|
||||||
logger.info("Updated version file: \(fileString)")
|
|
||||||
} else {
|
|
||||||
logger.info("Would update file contents to:")
|
|
||||||
logger.info("\(fileContents)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate enum UpdateError: Error {
|
|
||||||
case versionFileDoesNotExist(path: String)
|
|
||||||
}
|
|
||||||
152
Tests/CliVersionTests/CliClientTests.swift
Normal file
152
Tests/CliVersionTests/CliClientTests.swift
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
@_spi(Internal) import CliClient
|
||||||
|
import ConfigurationClient
|
||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import GitClient
|
||||||
|
import Logging
|
||||||
|
import Testing
|
||||||
|
import TestSupport
|
||||||
|
|
||||||
|
@Suite("CliClientTests")
|
||||||
|
struct CliClientTests {
|
||||||
|
|
||||||
|
@Test(
|
||||||
|
arguments: TestArguments.testCases
|
||||||
|
)
|
||||||
|
func testBuild(target: String) async throws {
|
||||||
|
let template = Template.build("1.0.0")
|
||||||
|
try await run {
|
||||||
|
$0.fileClient.fileExists = { _ in true }
|
||||||
|
$0.fileClient.read = { @Sendable _ in template }
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.cliClient) var client
|
||||||
|
|
||||||
|
let output = try await client.build(.testOptions(
|
||||||
|
target: target,
|
||||||
|
versionStrategy: .semvar(requireExistingFile: false)
|
||||||
|
))
|
||||||
|
#expect(output == "/baz/Sources/bar/Version.swift")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(
|
||||||
|
arguments: TestArguments.bumpCases
|
||||||
|
)
|
||||||
|
func bump(type: CliClient.BumpOption, optional: Bool) async throws {
|
||||||
|
let template = optional ? Template.optional("1.0.0-4-g59bc977") : Template.build("1.0.0")
|
||||||
|
try await run {
|
||||||
|
$0.fileClient.fileExists = { _ in true }
|
||||||
|
$0.fileClient.read = { @Sendable _ in template }
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.cliClient) var client
|
||||||
|
let output = try await client.bump(type, .testOptions())
|
||||||
|
#expect(output == "/baz/Sources/bar/Version.swift")
|
||||||
|
} assert: { string, _ in
|
||||||
|
|
||||||
|
if type != .preRelease {
|
||||||
|
#expect(string != nil)
|
||||||
|
}
|
||||||
|
let typeString = optional ? "String?" : "String"
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .major:
|
||||||
|
#expect(string!.contains("let VERSION: \(typeString) = \"2.0.0\""))
|
||||||
|
case .minor:
|
||||||
|
#expect(string!.contains("let VERSION: \(typeString) = \"1.1.0\""))
|
||||||
|
case .patch:
|
||||||
|
#expect(string!.contains("let VERSION: \(typeString) = \"1.0.1\""))
|
||||||
|
case .preRelease:
|
||||||
|
// do something
|
||||||
|
#expect(Bool(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(
|
||||||
|
arguments: TestArguments.testCases
|
||||||
|
)
|
||||||
|
func generate(target: String) async throws {
|
||||||
|
try await run {
|
||||||
|
@Dependency(\.cliClient) var client
|
||||||
|
let output = try await client.build(.testOptions(
|
||||||
|
target: target,
|
||||||
|
versionStrategy: .semvar(requireExistingFile: false)
|
||||||
|
))
|
||||||
|
#expect(output == "/baz/Sources/bar/Version.swift")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: GitClient.Version.mocks)
|
||||||
|
func gitVersionToSemVar(version: GitClient.Version) {
|
||||||
|
let semVar = version.semVar
|
||||||
|
if semVar != nil {
|
||||||
|
#expect(semVar!.versionString(withPreReleaseTag: false) == "1.0.0")
|
||||||
|
#expect(semVar!.versionString(withPreReleaseTag: true) == version.description)
|
||||||
|
} else {
|
||||||
|
let semVar = SemVar(preRelease: version.description)
|
||||||
|
#expect(semVar.versionString(withPreReleaseTag: true) == "0.0.0-\(version.description)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(
|
||||||
|
setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
|
||||||
|
operation: @Sendable @escaping () async throws -> Void,
|
||||||
|
assert: @escaping (String?, URL?) -> Void = { _, _ in }
|
||||||
|
) async throws {
|
||||||
|
let captured = CapturingWrite()
|
||||||
|
|
||||||
|
try await withDependencies {
|
||||||
|
$0.logger.logLevel = .debug
|
||||||
|
$0.fileClient = .capturing(captured)
|
||||||
|
$0.fileClient.fileExists = { _ in false }
|
||||||
|
$0.gitClient = .mock(.tag("1.0.0"))
|
||||||
|
$0.cliClient = .liveValue
|
||||||
|
$0.configurationClient = .liveValue
|
||||||
|
$0.configurationClient.find = { _ in URL(filePath: "/") }
|
||||||
|
setupDependencies(&$0)
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
let data = await captured.data
|
||||||
|
let url = await captured.url
|
||||||
|
var string: String?
|
||||||
|
|
||||||
|
if let data {
|
||||||
|
string = String(bytes: data, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(string, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestArguments {
|
||||||
|
static let testCases = ["bar", "Sources/bar", "./Sources/bar"]
|
||||||
|
static let bumpCases = CliClient.BumpOption.allCases.reduce(into: [(CliClient.BumpOption, Bool)]()) {
|
||||||
|
$0.append(($1, true))
|
||||||
|
$0.append(($1, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
static let updateCases = testCases.map { ($0, Bool.random()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestError: Error {}
|
||||||
|
|
||||||
|
extension CliClient.SharedOptions {
|
||||||
|
static func testOptions(
|
||||||
|
gitDirectory: String? = "/baz",
|
||||||
|
dryRun: Bool = false,
|
||||||
|
target: String = "bar",
|
||||||
|
versionStrategy: Configuration.VersionStrategy = .semvar(.init())
|
||||||
|
) -> Self {
|
||||||
|
return .init(
|
||||||
|
dryRun: dryRun,
|
||||||
|
projectDirectory: gitDirectory,
|
||||||
|
loggingOptions: .init(command: "test", verbose: 2),
|
||||||
|
configurationToMerge: .init(
|
||||||
|
target: .init(module: .init(target)),
|
||||||
|
strategy: versionStrategy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import XCTest
|
|
||||||
import CliVersion
|
|
||||||
import ShellClient
|
|
||||||
|
|
||||||
final class GitVersionTests: XCTestCase {
|
|
||||||
|
|
||||||
override func invokeTest() {
|
|
||||||
withDependencies({
|
|
||||||
$0.logger.logLevel = .debug
|
|
||||||
$0.logger = .liveValue
|
|
||||||
$0.shellClient = .liveValue
|
|
||||||
$0.gitVersionClient = .liveValue
|
|
||||||
$0.fileClient = .liveValue
|
|
||||||
}, operation: {
|
|
||||||
super.invokeTest()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var gitDir: String {
|
|
||||||
URL(fileURLWithPath: #file)
|
|
||||||
.deletingLastPathComponent()
|
|
||||||
.deletingLastPathComponent()
|
|
||||||
.deletingLastPathComponent()
|
|
||||||
.absoluteString
|
|
||||||
.replacingOccurrences(of: "file://", with: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func test_overrides_work() throws {
|
|
||||||
try withDependencies {
|
|
||||||
$0.gitVersionClient.override(with: "blob")
|
|
||||||
} operation: {
|
|
||||||
@Dependency(\.gitVersionClient) var versionClient
|
|
||||||
|
|
||||||
let version = try versionClient.currentVersion()
|
|
||||||
XCTAssertEqual(version, "blob")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func test_live() throws {
|
|
||||||
@Dependency(\.gitVersionClient) var versionClient: GitVersionClient
|
|
||||||
|
|
||||||
let version = try versionClient.currentVersion(in: gitDir)
|
|
||||||
print("VERSION: \(version)")
|
|
||||||
// can't really have a predictable result for the live client.
|
|
||||||
XCTAssertNotEqual(version, "blob")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func test_commands() throws {
|
|
||||||
@Dependency(\.shellClient) var shellClient: ShellClient
|
|
||||||
|
|
||||||
XCTAssertNoThrow(
|
|
||||||
try shellClient.background(
|
|
||||||
.gitCurrentBranch(gitDirectory: gitDir),
|
|
||||||
trimmingCharactersIn: .whitespacesAndNewlines
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
XCTAssertNoThrow(
|
|
||||||
try shellClient.background(
|
|
||||||
.gitCurrentSha(gitDirectory: gitDir),
|
|
||||||
trimmingCharactersIn: .whitespacesAndNewlines
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func test_file_client() throws {
|
|
||||||
@Dependency(\.fileClient) var fileClient
|
|
||||||
|
|
||||||
let tmpDir = FileManager.default.temporaryDirectory
|
|
||||||
.appendingPathComponent("file-client-test")
|
|
||||||
|
|
||||||
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true)
|
|
||||||
defer { try! FileManager.default.removeItem(at: tmpDir) }
|
|
||||||
|
|
||||||
let filePath = tmpDir.appendingPathComponent("blob.txt")
|
|
||||||
try fileClient.write(string: "Blob", to: filePath)
|
|
||||||
|
|
||||||
let contents = try String(contentsOf: filePath)
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
XCTAssertEqual(contents, "Blob")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func test_file_client_with_string_path() throws {
|
|
||||||
@Dependency(\.fileClient) var fileClient
|
|
||||||
|
|
||||||
let tmpDir = FileManager.default.temporaryDirectory
|
|
||||||
.appendingPathComponent("file-client-string-test")
|
|
||||||
|
|
||||||
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true)
|
|
||||||
defer { try! FileManager.default.removeItem(at: tmpDir) }
|
|
||||||
|
|
||||||
let filePath = tmpDir.appendingPathComponent("blob.txt")
|
|
||||||
let fileString = filePath.absoluteString.replacingOccurrences(of: "file://", with: "")
|
|
||||||
try fileClient.write(string: "Blob", to: fileString)
|
|
||||||
|
|
||||||
let contents = try String(contentsOf: filePath)
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
XCTAssertEqual(contents, "Blob")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
149
Tests/ConfigurationClientTests/ConfigurationClientTests.swift
Normal file
149
Tests/ConfigurationClientTests/ConfigurationClientTests.swift
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
@_spi(Internal) import ConfigurationClient
|
||||||
|
import CustomDump
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
import TestSupport
|
||||||
|
|
||||||
|
@Suite("ConfigurationClientTests")
|
||||||
|
struct ConfigurationClientTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func codable() async throws {
|
||||||
|
try await run {
|
||||||
|
@Dependency(\.configurationClient) var configurationClient
|
||||||
|
@Dependency(\.coders) var coders
|
||||||
|
|
||||||
|
let configuration = Configuration.customPreRelease
|
||||||
|
let encoded = try coders.jsonEncoder().encode(configuration)
|
||||||
|
let decoded = try coders.jsonDecoder().decode(Configuration.self, from: encoded)
|
||||||
|
|
||||||
|
#expect(decoded == configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func writeAndLoad() async throws {
|
||||||
|
try await withTemporaryDirectory { url in
|
||||||
|
try await run {
|
||||||
|
@Dependency(\.configurationClient) var configurationClient
|
||||||
|
|
||||||
|
for ext in ["json"] {
|
||||||
|
let fileUrl = url.appending(path: "test.\(ext)")
|
||||||
|
let configuration = Configuration.mock()
|
||||||
|
|
||||||
|
try await configurationClient.write(configuration, fileUrl)
|
||||||
|
let loaded = try await configurationClient.load(fileUrl)
|
||||||
|
#expect(loaded == configuration)
|
||||||
|
|
||||||
|
let findAndLoaded = try await configurationClient.findAndLoad(fileUrl)
|
||||||
|
#expect(findAndLoaded == configuration)
|
||||||
|
|
||||||
|
try FileManager.default.removeItem(at: fileUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func findAndLoad() async throws {
|
||||||
|
try await withTemporaryDirectory { url in
|
||||||
|
try await run {
|
||||||
|
@Dependency(\.configurationClient) var configurationClient
|
||||||
|
|
||||||
|
let shouldBeNil = try await configurationClient.find(url)
|
||||||
|
#expect(shouldBeNil == nil)
|
||||||
|
|
||||||
|
do {
|
||||||
|
_ = try await configurationClient.findAndLoad(url)
|
||||||
|
#expect(Bool(false))
|
||||||
|
} catch {
|
||||||
|
#expect(Bool(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
for ext in ["json"] {
|
||||||
|
let fileUrl = url.appending(path: ".bump-version.\(ext)")
|
||||||
|
let configuration = Configuration.mock()
|
||||||
|
|
||||||
|
try await configurationClient.write(configuration, fileUrl)
|
||||||
|
let loaded = try await configurationClient.findAndLoad(fileUrl)
|
||||||
|
#expect(loaded == configuration)
|
||||||
|
|
||||||
|
try FileManager.default.removeItem(at: fileUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func mergingBranch() {
|
||||||
|
let branch = Configuration.Branch(includeCommitSha: false)
|
||||||
|
let branch2 = Configuration.Branch(includeCommitSha: true)
|
||||||
|
let merged = branch.merging(branch2)
|
||||||
|
#expect(merged == branch2)
|
||||||
|
|
||||||
|
let merged2 = branch.merging(nil)
|
||||||
|
#expect(merged2 == branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func mergingSemvar() {
|
||||||
|
let strategy1 = Configuration.VersionStrategy.semvar(.init())
|
||||||
|
let other = Configuration.VersionStrategy.semvar(.init(
|
||||||
|
allowPreRelease: true,
|
||||||
|
preRelease: .init(prefix: "foo", strategy: .gitTag),
|
||||||
|
requireExistingFile: false,
|
||||||
|
requireExistingSemVar: false,
|
||||||
|
strategy: .gitTag()
|
||||||
|
))
|
||||||
|
let merged = strategy1.merging(other)
|
||||||
|
#expect(merged == other)
|
||||||
|
|
||||||
|
let otherMerged = other.merging(strategy1)
|
||||||
|
#expect(otherMerged == other)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func mergingTarget() {
|
||||||
|
let config1 = Configuration(target: .init(path: "foo"))
|
||||||
|
let config2 = Configuration(target: .init(module: .init("bar")))
|
||||||
|
|
||||||
|
let merged = config1.merging(config2)
|
||||||
|
#expect(merged.target! == .init(module: .init("bar")))
|
||||||
|
|
||||||
|
let merged2 = merged.merging(config1)
|
||||||
|
#expect(merged2.target! == .init(path: "foo"))
|
||||||
|
|
||||||
|
let merged3 = merged2.merging(nil)
|
||||||
|
#expect(merged3 == merged2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func mergingVersionStrategy() {
|
||||||
|
let version = Configuration.VersionStrategy.semvar(.init())
|
||||||
|
let version2 = Configuration.VersionStrategy.branch(.init())
|
||||||
|
|
||||||
|
let merged = version.merging(version2)
|
||||||
|
#expect(merged == version2)
|
||||||
|
|
||||||
|
let merged2 = merged.merging(.branch(includeCommitSha: false))
|
||||||
|
#expect(merged2.branch!.includeCommitSha == false)
|
||||||
|
|
||||||
|
let merged3 = version2.merging(version)
|
||||||
|
#expect(merged3 == version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(
|
||||||
|
setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
|
||||||
|
operation: () async throws -> Void
|
||||||
|
) async throws {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.coders = .liveValue
|
||||||
|
$0.fileClient = .liveValue
|
||||||
|
$0.configurationClient = .liveValue
|
||||||
|
setupDependencies(&$0)
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
Tests/GitClientTests/GitClientTests.swift
Normal file
59
Tests/GitClientTests/GitClientTests.swift
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import Dependencies
|
||||||
|
import GitClient
|
||||||
|
import ShellClient
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@Suite("GitClientTests")
|
||||||
|
struct GitClientTests {
|
||||||
|
|
||||||
|
@Test(arguments: GitClientVersionTestArgument.testCases)
|
||||||
|
func testGitClient(input: GitClientVersionTestArgument) async throws {
|
||||||
|
let arguments = try await run {
|
||||||
|
@Dependency(\.gitClient) var gitClient
|
||||||
|
_ = try await gitClient.version(.init(style: input.style))
|
||||||
|
}
|
||||||
|
#expect(arguments == input.expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(
|
||||||
|
_ operation: () async throws -> Void
|
||||||
|
) async throws -> [[String]] {
|
||||||
|
let captured = CapturedCommand()
|
||||||
|
|
||||||
|
try await withDependencies {
|
||||||
|
$0.asyncShellClient = .capturing(captured)
|
||||||
|
$0.fileClient = .noop
|
||||||
|
$0.gitClient = .liveValue
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
return await captured.commands.map(\.arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GitClientVersionTestArgument {
|
||||||
|
let style: GitClient.CurrentVersionOption.Style
|
||||||
|
let expected: [[String]]
|
||||||
|
|
||||||
|
static let testCases: [Self] = [
|
||||||
|
.init(
|
||||||
|
style: .tag(exactMatch: true),
|
||||||
|
expected: [["git", "describe", "--tags", "--exact-match"]]
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
style: .tag(exactMatch: false),
|
||||||
|
expected: [["git", "describe", "--tags"]]
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
style: .branch(commitSha: false),
|
||||||
|
expected: [["git", "symbolic-ref", "--quiet", "--short", "HEAD"]]
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
style: .branch(commitSha: true),
|
||||||
|
expected: [
|
||||||
|
["git", "rev-parse", "--short", "HEAD"],
|
||||||
|
["git", "symbolic-ref", "--quiet", "--short", "HEAD"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
92
justfile
Normal file
92
justfile
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
product := "bump-version"
|
||||||
|
docker_image := "bump-version"
|
||||||
|
docker_tag := "test"
|
||||||
|
|
||||||
|
tap_url := "https://git.housh.dev/michael/homebrew-formula"
|
||||||
|
tap := "michael/formula"
|
||||||
|
formula := "bump-version"
|
||||||
|
release_base_url := "https://git.housh.dev/michael/swift-bump-version/archive"
|
||||||
|
version := "$(git describe --tags --exact-match)"
|
||||||
|
|
||||||
|
[private]
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# Build and bottle homebrew formula.
|
||||||
|
bottle:
|
||||||
|
@brew uninstall {{formula}} || true
|
||||||
|
@brew tap {{tap}} {{tap_url}}
|
||||||
|
@brew install --build-bottle {{tap}}/{{formula}}
|
||||||
|
@brew bottle {{formula}}
|
||||||
|
bottle="$(ls *.gz)" && mv "${bottle}" "${bottle/--/-}"
|
||||||
|
|
||||||
|
# Build locally.
|
||||||
|
build configuration="debug":
|
||||||
|
@swift build \
|
||||||
|
--disable-sandbox \
|
||||||
|
--configuration {{configuration}} \
|
||||||
|
--product {{product}}
|
||||||
|
|
||||||
|
# Bump our version of the command-line tool.
|
||||||
|
bump-version *ARGS: (run "bump" ARGS)
|
||||||
|
|
||||||
|
# Build a docker image.
|
||||||
|
build-docker configuration="debug":
|
||||||
|
@docker build -t {{docker_image}}:{{docker_tag}} .
|
||||||
|
|
||||||
|
# Run the command-line tool.
|
||||||
|
run *ARGS:
|
||||||
|
@swift run {{product}} {{ARGS}}
|
||||||
|
|
||||||
|
# Clean the build folder.
|
||||||
|
clean:
|
||||||
|
rm -rf .build
|
||||||
|
|
||||||
|
# Clean and build.
|
||||||
|
clean-build configuration="debug": clean (build configuration)
|
||||||
|
|
||||||
|
# Remove bottles
|
||||||
|
remove-bottles:
|
||||||
|
rm -rf *.gz
|
||||||
|
|
||||||
|
# Test locally.
|
||||||
|
test *ARGS:
|
||||||
|
@swift test {{ARGS}}
|
||||||
|
|
||||||
|
# Build docker test container and run tests.
|
||||||
|
test-docker: build-docker
|
||||||
|
@docker run --rm {{docker_image}}:{{docker_tag}}
|
||||||
|
|
||||||
|
# Run tests in docker without building a new image.
|
||||||
|
test-docker-without-building:
|
||||||
|
@docker run --rm {{docker_image}}:{{docker_tag}} swift test
|
||||||
|
|
||||||
|
# Show the current git-tag version.
|
||||||
|
echo-version:
|
||||||
|
@echo "VERSION: {{version}}"
|
||||||
|
|
||||||
|
# Get the sha256 sum of the release and copy to clipboard.
|
||||||
|
get-release-sha prefix="": (build "release")
|
||||||
|
url="{{release_base_url}}/{{prefix}}${version}.tar.gz" && \
|
||||||
|
sha=$(curl "$url" | shasum -a 256) && \
|
||||||
|
stripped="${sha% *}" && \
|
||||||
|
echo "$stripped" | pbcopy && \
|
||||||
|
echo "Copied sha to clipboard: $stripped"
|
||||||
|
|
||||||
|
# Preview the documentation locally.
|
||||||
|
preview-documentation target="BumpVersion":
|
||||||
|
swift package \
|
||||||
|
--disable-sandbox \
|
||||||
|
preview-documentation \
|
||||||
|
--target {{target}}
|
||||||
|
|
||||||
|
# Preview the documentation locally.
|
||||||
|
build-documentation dir="./docs" target="BumpVersion" basePath="bump-version":
|
||||||
|
swift package \
|
||||||
|
--allow-writing-to-directory {{dir}} \
|
||||||
|
generate-documentation \
|
||||||
|
--target {{target}} \
|
||||||
|
--disable-indexing \
|
||||||
|
--transform-for-static-hosting \
|
||||||
|
--hosting-base-path {{basePath}} \
|
||||||
|
--output-path {{dir}}
|
||||||
Reference in New Issue
Block a user