├── .github
└── CODEOWNERS
├── .gitignore
├── .gitmodules
├── Assets
├── issue_comment.png
└── pr_comment.png
├── Changelog.md
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── GitBuddy
│ └── main.swift
└── GitBuddyCore
│ ├── Changelog
│ ├── ChangelogBuilder.swift
│ ├── ChangelogItemsFactory.swift
│ └── ChangelogProducer.swift
│ ├── Commands
│ ├── ChangelogCommand.swift
│ ├── GitBuddy.swift
│ ├── ReleaseCommand.swift
│ └── TagDeletionsCommand.swift
│ ├── GitHub
│ ├── Commenter.swift
│ ├── GitHubReleaseNotesGenerator.swift
│ ├── IssuesFetcher.swift
│ ├── IssuesResolver.swift
│ ├── OctoKit+Authentication.swift
│ └── PullRequestFetcher.swift
│ ├── Helpers
│ ├── DateFormatters.swift
│ ├── DependencyInjectors.swift
│ ├── Log.swift
│ ├── OctoKitError.swift
│ └── Shell.swift
│ ├── Models
│ ├── Changelog.swift
│ ├── ChangelogItem.swift
│ ├── GITProject.swift
│ ├── Release.swift
│ ├── Tag.swift
│ └── Token.swift
│ ├── Release
│ └── ReleaseProducer.swift
│ └── Tag Deletions
│ └── TagsDeleter.swift
├── Tests
├── GitBuddyTests
│ ├── Changelog
│ │ ├── ChangelogCommandTests.swift
│ │ └── ChangelogItemsFactoryTests.swift
│ ├── GitBuddyCommandTests.swift
│ ├── GitHub
│ │ ├── CommenterTests.swift
│ │ ├── IssueResolverTests.swift
│ │ └── PullRequestFetcherTests.swift
│ ├── Models
│ │ ├── ChangelogItemTests.swift
│ │ └── SingleSectionChangelogTests.swift
│ ├── Release
│ │ └── ReleaseProducerTests.swift
│ ├── Tag Deletions
│ │ └── TagsDeleterTests.swift
│ └── TestHelpers
│ │ ├── CommentJSON.swift
│ │ ├── IssueJSON.swift
│ │ ├── IssuesJSON.swift
│ │ ├── ListReleasesJSON.swift
│ │ ├── Mocks.swift
│ │ ├── PullRequestsJSON.swift
│ │ ├── ReleaseJSON.swift
│ │ ├── ReleaseNotesJSON.swift
│ │ └── XCTestExtensions.swift
└── LinuxMain.swift
└── fastlane
├── .gitignore
└── Fastfile
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/articles/about-code-owners
2 | # These owners will be the default owners for everything in
3 | # the repo. Unless a later match takes precedence, they
4 | # will be requested for review when someone opens a PR.
5 | * @wetransfer/ios-collect
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | Packages/
41 | Package.pins
42 | *.xcodeproj
43 | #
44 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
45 | # hence it is not needed unless you have added a package configuration file to your project
46 | .swiftpm
47 |
48 | .build/
49 | .spm-build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "Submodules/WeTransfer-iOS-CI"]
2 | path = Submodules/WeTransfer-iOS-CI
3 | url=https://github.com/WeTransfer/WeTransfer-iOS-CI.git
4 | branch = master
5 |
--------------------------------------------------------------------------------
/Assets/issue_comment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/GitBuddy/244578860ca4f82e9d186dda0b52a867661faa84/Assets/issue_comment.png
--------------------------------------------------------------------------------
/Assets/pr_comment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/GitBuddy/244578860ca4f82e9d186dda0b52a867661faa84/Assets/pr_comment.png
--------------------------------------------------------------------------------
/Changelog.md:
--------------------------------------------------------------------------------
1 | ### 4.3.0
2 | - Make sure GH release notes are used correctly with previous tag ([#101](https://github.com/WeTransfer/GitBuddy/pull/101)) via [@AvdLee](https://github.com/AvdLee)
3 | - Merge release 4.2.0 into master ([#100](https://github.com/WeTransfer/GitBuddy/pull/100)) via [@wetransferplatform](https://github.com/wetransferplatform)
4 | - Update CI ([#99](https://github.com/WeTransfer/GitBuddy/pull/99)) via [@AvdLee](https://github.com/AvdLee)
5 |
6 | ### 4.2.0
7 | - Add the possibility to let GitHub generate release notes ([#98](https://github.com/WeTransfer/GitBuddy/pull/98)) via [@AvdLee](https://github.com/AvdLee)
8 | - Merge release 4.1.5 into master ([#97](https://github.com/WeTransfer/GitBuddy/pull/97)) via [@wetransferplatform](https://github.com/wetransferplatform)
9 |
10 | ### 4.1.5
11 | - Increase PR Limit to fetch 100 latest ([#96](https://github.com/WeTransfer/GitBuddy/pull/96)) via [@AvdLee](https://github.com/AvdLee)
12 | - Update README.md ([#92](https://github.com/WeTransfer/GitBuddy/pull/92)) via [@iSapozhnik](https://github.com/iSapozhnik)
13 | - Merge release 4.1.4 into master ([#94](https://github.com/WeTransfer/GitBuddy/pull/94)) via [@wetransferplatform](https://github.com/wetransferplatform)
14 |
15 | ### 4.1.4
16 | - Improve error logs for GitBuddy GitHub API failures ([#93](https://github.com/WeTransfer/GitBuddy/pull/93)) via [@AvdLee](https://github.com/AvdLee)
17 | - Update `Development` section in `README` file ([#91](https://github.com/WeTransfer/GitBuddy/pull/91)) via [@peagasilva](https://github.com/peagasilva)
18 | - Update `octokit.swift` dependency ([#90](https://github.com/WeTransfer/GitBuddy/pull/90)) via [@peagasilva](https://github.com/peagasilva)
19 | - Merge release 4.1.3 into master ([#84](https://github.com/WeTransfer/GitBuddy/pull/84)) via [@wetransferplatform](https://github.com/wetransferplatform)
20 |
21 | ### 4.1.3
22 | - Filter emojis out of changelog ([#82](https://github.com/WeTransfer/GitBuddy/pull/82)) via [@AvdLee](https://github.com/AvdLee)
23 | - Merge release 4.1.2 into master ([#81](https://github.com/WeTransfer/GitBuddy/pull/81)) via [@wetransferplatform](https://github.com/wetransferplatform)
24 |
25 | ### 4.1.2
26 | - Update gems ([#80](https://github.com/WeTransfer/GitBuddy/pull/80)) via [@kairadiagne](https://github.com/kairadiagne)
27 |
28 | ### 4.1.0
29 | - Add a new `tagDeletion` command ([#74](https://github.com/WeTransfer/GitBuddy/pull/74)) via [@AvdLee](https://github.com/AvdLee)
30 | - Make use of the target commitish date ([#75](https://github.com/WeTransfer/GitBuddy/pull/75)) via [@AvdLee](https://github.com/AvdLee)
31 | - Merge release 4.0.0 into master ([#73](https://github.com/WeTransfer/GitBuddy/pull/73)) via [@wetransferplatform](https://github.com/wetransferplatform)
32 |
33 | ### 4.0.0
34 | - Merge release 3.1.1 into master ([#67](https://github.com/WeTransfer/GitBuddy/pull/67)) via [@wetransferplatform](https://github.com/wetransferplatform)
35 | - Add a new `--json` parameter ([#71](https://github.com/WeTransfer/GitBuddy/pull/71)) via [@AvdLee](https://github.com/AvdLee)
36 |
37 | ### 3.1.1
38 | - Fix compilation error that happened due to naming changes in OctoKit. ([#66](https://github.com/WeTransfer/GitBuddy/pull/66)) via [@kairadiagne](https://github.com/kairadiagne)
39 | - Merge release 3.1.0 into master ([#61](https://github.com/WeTransfer/GitBuddy/pull/61)) via [@ghost](https://github.com/ghost)
40 |
41 | ### 3.1.0
42 | - Enable --version flag ([#58](https://github.com/WeTransfer/GitBuddy/pull/58)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
43 | - Avoid referring to the `master` branch in ShellCommand.rawValue ([#59](https://github.com/WeTransfer/GitBuddy/pull/59)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
44 | - Fix CI by updating dependencies ([#60](https://github.com/WeTransfer/GitBuddy/pull/60)) via [@AvdLee](https://github.com/AvdLee)
45 | - Use uppercase first for changelog items ([#56](https://github.com/WeTransfer/GitBuddy/issues/56)) via [@mosamer](https://github.com/mosamer)
46 | - Merge release 3.0.1 into master ([#54](https://github.com/WeTransfer/GitBuddy/pull/54)) via [@WeTransferBot](https://github.com/WeTransferBot)
47 |
48 | ### 3.0.1
49 | - Fix fallback date for new tag releases ([#53](https://github.com/WeTransfer/GitBuddy/pull/53)) via [@AvdLee](https://github.com/AvdLee)
50 | - Merge release 3.0.0 into master ([#51](https://github.com/WeTransfer/GitBuddy/pull/51)) via [@WeTransferBot](https://github.com/WeTransferBot)
51 |
52 | ### 3.0.0
53 | - Use the new ArgumentsParser framework ([#31](https://github.com/WeTransfer/GitBuddy/issues/31)) via [@AvdLee](https://github.com/AvdLee)
54 | - Merge release 2.4.1 into master ([#43](https://github.com/WeTransfer/GitBuddy/pull/43)) via [@WeTransferBot](https://github.com/WeTransferBot)
55 | - Add a watermark to promote GitBuddy ([#28](https://github.com/WeTransfer/GitBuddy/issues/28)) via [@AvdLee](https://github.com/AvdLee)
56 | - Changelog includes changes that are not in the build ([#33](https://github.com/WeTransfer/GitBuddy/issues/33)) via [@AvdLee](https://github.com/AvdLee)
57 |
58 | ### 2.4.1
59 | - Fix OctoKit version in Package.swift ([#42](https://github.com/WeTransfer/GitBuddy/pull/42)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
60 | - Merge release 2.4.0 into master ([#41](https://github.com/WeTransfer/GitBuddy/pull/41)) via [@WeTransferBot](https://github.com/WeTransferBot)
61 |
62 | ### 2.4.0
63 | - Allow changelog to be split into separate sections ([#40](https://github.com/WeTransfer/GitBuddy/pull/40)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
64 | - Log pull request number for verbose changelog ([#39](https://github.com/WeTransfer/GitBuddy/pull/39)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
65 | - Merge release 2.3.0 into master ([#38](https://github.com/WeTransfer/GitBuddy/pull/38)) via [@WeTransferBot](https://github.com/WeTransferBot)
66 |
67 | ### 2.3.0
68 | - Link to user's profile in a generated changelog ([#37](https://github.com/WeTransfer/GitBuddy/pull/37)) via [@MaxDesiatov](https://github.com/MaxDesiatov)
69 | - Merge release 2.2.0 into master ([#35](https://github.com/WeTransfer/GitBuddy/pull/35)) via [@WeTransferBot](https://github.com/WeTransferBot)
70 |
71 | ### 2.2.0
72 | - Username is not included in Changelog ([#30](https://github.com/WeTransfer/GitBuddy/issues/30)) via @AvdLee
73 | - Merge release 2.1.1 into master ([#27](https://github.com/WeTransfer/GitBuddy/pull/27)) via @WeTransferBot
74 |
75 | ### 2.1.1
76 | - Reference OctoKit master ([#26](https://github.com/WeTransfer/GitBuddy/pull/26)) via @AvdLee
77 | - Merge release 2.1.0 into master ([#25](https://github.com/WeTransfer/GitBuddy/pull/25)) via @AvdLee
78 |
79 | ### 2.1.0
80 | - Improved support for release and changelog generation ([#22](https://github.com/WeTransfer/GitBuddy/pull/22)) via @AvdLee
81 | - Add extra arguments for creating releases ([#23](https://github.com/WeTransfer/GitBuddy/pull/23)) via @AvdLee
82 | - Merge release 2.0.0 into master ([#21](https://github.com/WeTransfer/GitBuddy/pull/21))
83 |
84 | ### 2.0.0
85 | - Add an option to release as a prerelease ([#20](https://github.com/WeTransfer/GitBuddy/pull/20)) via @AvdLee
86 | - Add a --version argument ([#17](https://github.com/WeTransfer/GitBuddy/issues/17)) via @AvdLee
87 | - Convert camelCase to kebeb-case ([#18](https://github.com/WeTransfer/GitBuddy/issues/18)) via @AvdLee
88 | - Fix support for running tests on Linux ([#11](https://github.com/WeTransfer/GitBuddy/pull/11)) via @AvdLee
89 | - [base branch] Renaming entries to GitBuddy ([#9](https://github.com/WeTransfer/GitBuddy/pull/9)) via @AvdLee
90 | - Merge release 1.0.0 into master ([#8](https://github.com/WeTransfer/GitBuddy/pull/8))
91 |
92 | ### 1.0.0
93 |
94 | - Implement CI ([#5](https://github.com/WeTransfer/GitBuddy/issues/5)) via @AvdLee
95 | - Base setup ([#3](https://github.com/WeTransfer/GitBuddy/pull/3)) via @AvdLee
96 |
97 | ### 0.1.0
98 |
99 | - Initial setup
100 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ci_gems_path = File.join(File.dirname(__FILE__), "Submodules/WeTransfer-iOS-CI/Gemfile")
4 | eval_gemfile(ci_gems_path) if File.exist?(ci_gems_path)
5 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | dotenv (2.7.5)
5 |
6 | PLATFORMS
7 | ruby
8 |
9 | DEPENDENCIES
10 | dotenv
11 |
12 | BUNDLED WITH
13 | 2.1.4
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | HL License
2 |
3 | Copyright (c) 2020 WeTransfer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | * No Harm: The software may not be used by anyone for systems or activities that actively and knowingly endanger, harm, or otherwise threaten the physical, mental, economic, or general well-being of other individuals or groups, in violation of the United Nations Universal Declaration of Human Rights (https://www.un.org/en/universal-declaration-human-rights/).
10 |
11 | * Services: If the Software is used to provide a service to others, the licensee shall, as a condition of use, require those others not to use the service in any way that violates the No Harm clause above.
12 |
13 | * Enforceability: If any portion or provision of this License shall to any extent be declared illegal or unenforceable by a court of competent jurisdiction, then the remainder of this License, or the application of such portion or provision in circumstances other than those as to which it is so declared illegal or unenforceable, shall not be affected thereby, and each portion and provision of this Agreement shall be valid and enforceable to the fullest extent permitted by law.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 | This Hippocratic License is an Ethical Source license (https://ethicalsource.dev) derived from the MIT License, amended to limit the impact of the unethical use of open source software.
18 |
19 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Mocker",
6 | "repositoryURL": "https://github.com/WeTransfer/Mocker.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "077c52a31f5ebb649ebe546cc77d8647760b2279",
10 | "version": "2.5.5"
11 | }
12 | },
13 | {
14 | "package": "OctoKit",
15 | "repositoryURL": "https://github.com/WeTransfer/octokit.swift",
16 | "state": {
17 | "branch": "main",
18 | "revision": "d3706890a06d2f9afc1de4665191167058438153",
19 | "version": null
20 | }
21 | },
22 | {
23 | "package": "RequestKit",
24 | "repositoryURL": "https://github.com/nerdishbynature/RequestKit.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "8b0258ea2a4345cbcac90509b764faacea12efb0",
28 | "version": "3.2.1"
29 | }
30 | },
31 | {
32 | "package": "swift-argument-parser",
33 | "repositoryURL": "https://github.com/apple/swift-argument-parser",
34 | "state": {
35 | "branch": null,
36 | "revision": "e1465042f195f374b94f915ba8ca49de24300a0d",
37 | "version": "1.0.2"
38 | }
39 | }
40 | ]
41 | },
42 | "version": 1
43 | }
44 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "GitBuddy",
7 | platforms: [
8 | .macOS(.v10_15)
9 | ],
10 | products: [
11 | .executable(name: "GitBuddy", targets: ["GitBuddy"])
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "2.1.0")),
15 | /// Temporarily pointing to the WeTransfer fork of `octokit.swift` because the current version (`0.11.0`) doesn't include
16 | /// the changes around Releases. Once a new version is released, we should go back to pointing to the original repo:
17 | /// `.package(url: "https://github.com/nerdishbynature/octokit.swift", .upToNextMajor(from: "0.11.0"))`
18 | .package(url: "https://github.com/WeTransfer/octokit.swift", .branch("main")),
19 | .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "1.0.0"))
20 | ],
21 | targets: [
22 | .executableTarget(name: "GitBuddy", dependencies: ["GitBuddyCore"]),
23 | .target(name: "GitBuddyCore", dependencies: [
24 | .product(name: "OctoKit", package: "octokit.swift"),
25 | .product(name: "ArgumentParser", package: "swift-argument-parser")
26 | ]),
27 | .testTarget(name: "GitBuddyTests", dependencies: ["GitBuddy", "Mocker"])
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitBuddy
2 | Your buddy in managing and maintaining GitHub repositories.
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | GitBuddy helps you with:
11 |
12 | - [x] Generating a changelog
13 | - [x] Converting a tag into a GitHub release with a changelog attached
14 | - [x] Commenting on issues and PRs when a releases contained the related code changes
15 |
16 | ### Generating a changelog
17 | ```
18 | $ gitbuddy changelog --help
19 | OVERVIEW: Create a changelog for GitHub repositories
20 |
21 | USAGE: gitbuddy changelog [--since-tag ] [--base-branch ] [--sections] [--verbose]
22 |
23 | OPTIONS:
24 | -s, --since-tag
25 | The tag to use as a base. Defaults to the latest tag.
26 | -b, --base-branch
27 | The base branch to compare with. Defaults to master.
28 | --sections Whether the changelog should be split into sections. Defaults to false.
29 | --verbose Show extra logging for debugging purposes
30 | -h, --help Show help information.
31 | ```
32 |
33 | This command generates a changelog based on merged PRs and fixed issues. Once a PR contains a reference like `"Fixed #30"`, the title of issue 30 will be included in the changelog. Otherwise, the Pull Request title will be used.
34 |
35 | Pull requests that are merged into the `baseBranch` will be used as input for the changelog. Only pull requests that are merged after the creation date of the `sinceTag` are taken as input.
36 |
37 | #### A Changelog example
38 | This is an example taken from [Mocker](https://github.com/WeTransfer/Mocker/releases/tag/2.0.1)
39 |
40 | ----
41 |
42 | - Switch over to Danger-Swift & Bitrise ([#34](https://github.com/WeTransfer/Mocker/pull/34)) via @AvdLee
43 | - Fix important mismatch for getting the right mock ([#31](https://github.com/WeTransfer/Mocker/pull/31)) via @AvdLee
44 |
45 | ----
46 |
47 | If you'd like a changelog to link to issues closed before a release was tagged, pass the `--sections` argument, then it's going to look like this:
48 |
49 | ----
50 |
51 | **Closed issues:**
52 |
53 | - Can SPM support be merged branch add-spm-support be merged to master? ([#33](https://github.com/WeTransfer/Mocker/pull/33))
54 | - migrate 2.0.0 changes to spm compatible branch `feature/add-spm-support`? ([#32](https://github.com/WeTransfer/Mocker/pull/32))
55 |
56 | **Merged pull requests:**
57 |
58 | - Switch over to Danger-Swift & Bitrise ([#34](https://github.com/WeTransfer/Mocker/pull/34)) via @AvdLee
59 | - Fix important mismatch for getting the right mock ([#31](https://github.com/WeTransfer/Mocker/pull/31)) via @AvdLee
60 |
61 | ----
62 |
63 | ### Generating a release
64 | ```
65 | $ gitbuddy release --help
66 | OVERVIEW: Create a new release including a changelog and publish comments on related issues.
67 |
68 | USAGE: gitbuddy release [--changelog-path ] [--skip-comments] [--use-pre-release] [--target-commitish ] [--tag-name ] [--release-title ] [--last-release-tag ] [--base-branch ] [--sections] [--verbose]
69 |
70 | OPTIONS:
71 | -c, --changelog-path
72 | The path to the Changelog to update it with the latest changes.
73 | -s, --skip-comments Disable commenting on issues and PRs about the new release.
74 | -p, --use-pre-release Create the release as a pre-release.
75 | -t, --target-commitish
76 | Specifies the commitish value that determines where the Git tag is created
77 | from. Can be any branch or commit SHA. Unused if the Git tag already exists.
78 | Default: the repository's default branch (usually master).
79 | -n, --tag-name
80 | The name of the tag. Default: takes the last created tag to publish as a GitHub
81 | release.
82 | -r, --release-title
83 | The title of the release. Default: uses the tag name.
84 | -l, --last-release-tag
85 | The last release tag to use as a base for the changelog creation. Default:
86 | previous tag.
87 | -b, --base-branch
88 | The base branch to compare with for generating the changelog. Defaults to
89 | master.
90 | --sections Whether the changelog should be split into sections. Defaults to false.
91 | --verbose Show extra logging for debugging purposes
92 | -h, --help Show help information.
93 | ```
94 |
95 | The `release` command can be used to transform the latest tag into a GitHub release including the changelog as a body.
96 | The changelog is generated from all the changes between the latest and previous tag.
97 |
98 | #### Updating the changelog file
99 | The changelog is appended to the beginning of the changelog file if a `changelogPath` is passed. It's most commonly set to `Changelog.md`.
100 | It's best to use this on a release branch so you can commit the changes and open a PR.
101 |
102 | #### Post comments
103 | A great feature of this release command is the automation of posting comments to issues and PRs that are released with this release.
104 |
105 | **A comment posted on an issue**
106 | 
107 |
108 | **A comment posted on a pull request**
109 | 
110 |
111 | ### Installation using [Mint](https://github.com/yonaskolb/mint)
112 | You can install GitBuddy using Mint as follows:
113 |
114 | ```
115 | $ mint install WeTransfer/GitBuddy
116 | ```
117 |
118 | [Setup a personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) with the scope set to `repo` only. Add this token as an environment variable `GITBUDDY_ACCESS_TOKEN` by combining your GitHub username with the token:
119 |
120 | ```ruby
121 | export GITBUDDY_ACCESS_TOKEN=":"
122 | ```
123 |
124 | The token is used with the GitHub API and uses Basic HTTP authentication.
125 |
126 | After that you can directly use it by executing it from within the repo you would like to work with:
127 |
128 | ```
129 | $ gitbuddy --help
130 | OVERVIEW: Manage your GitHub repositories with ease
131 |
132 | USAGE: gitbuddy [--version]
133 |
134 | OPTIONS:
135 | --version Prints the current GitBuddy version
136 | -h, --help Show help information.
137 |
138 | SUBCOMMANDS:
139 | changelog Create a changelog for GitHub repositories
140 | release Create a new release including a changelog and publish comments on related issues.
141 | ```
142 |
143 | ### Development
144 | - `cd` into the repository
145 | - open the `Package.swift` file
146 | - Run the following command from the project you're using it for:
147 |
148 | ```bash
149 | swift run --package-path ../GitBuddy/ GitBuddy --help
150 | ```
151 |
--------------------------------------------------------------------------------
/Sources/GitBuddy/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Main.swift
3 | // GitBuddy
4 | //
5 | // Created by Antoine van der Lee on 16/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import GitBuddyCore
10 |
11 | GitBuddy.main()
12 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Changelog/ChangelogBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangelogBuilder.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 16/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | struct ChangelogBuilder {
13 | let items: [ChangelogItem]
14 |
15 | func build() -> String {
16 | return items
17 | .compactMap { $0.title }
18 | .filter { !$0.lowercased().contains("#trivial") }
19 | .map { "- \($0)" }
20 | .joined(separator: "\n")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Changelog/ChangelogItemsFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangelogItemsFactory.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 16/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | struct ChangelogItemsFactory {
13 | let octoKit: Octokit
14 | let pullRequests: [PullRequest]
15 | let project: GITProject
16 |
17 | func items(using session: URLSession = URLSession.shared) -> [ChangelogItem] {
18 | return pullRequests.flatMap { pullRequest -> [ChangelogItem] in
19 | let issuesResolver = IssuesResolver(octoKit: octoKit, project: project, input: pullRequest)
20 | guard let resolvedIssues = issuesResolver.resolve(using: session), !resolvedIssues.isEmpty else {
21 | return [ChangelogItem(input: pullRequest, closedBy: pullRequest)]
22 | }
23 | return resolvedIssues.map { issue -> ChangelogItem in
24 | ChangelogItem(input: issue, closedBy: pullRequest)
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Changelog/ChangelogProducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GitBuddy.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | /// Capable of producing a changelog based on input parameters.
13 | final class ChangelogProducer: URLSessionInjectable {
14 | enum Since {
15 | case date(date: Date)
16 | case tag(tag: String)
17 | case latestTag
18 |
19 | /// Gets the date for the current Since property.
20 | /// In the case of a tag, we add 60 seconds to make sure that the Changelog does not include the commit that
21 | /// is used for creating the tag.
22 | /// This is needed as the tag creation date equals the commit creation date.
23 | func get() throws -> Date {
24 | switch self {
25 | case .date(let date):
26 | return date
27 | case .tag(let tag):
28 | return try Tag(name: tag).created.addingTimeInterval(60)
29 | case .latestTag:
30 | return try Tag.latest().created.addingTimeInterval(60)
31 | }
32 | }
33 | }
34 |
35 | private lazy var octoKit: Octokit = .init()
36 |
37 | /// The base branch to compare with. Defaults to master.
38 | let baseBranch: Branch
39 |
40 | /// "The tag to use as a base. Defaults to the latest tag.
41 | let since: Since
42 |
43 | let from: Date
44 | let to: Date
45 |
46 | /// The GIT Project to create a changelog for.
47 | let project: GITProject
48 |
49 | /// If `true`, release notes will be generated by GitHub.
50 | /// Defaults to false, which will lead to a changelog based on PR and issue titles matching the tag changes.
51 | let useGitHubReleaseNotes: Bool
52 |
53 | /// The name of the tag to use as base for the changelog comparison.
54 | let tagName: String?
55 |
56 | /// Specifies the commitish value that will be the target for the release's tag.
57 | /// Required if the supplied tagName does not reference an existing tag. Ignored if the tagName already exists.
58 | /// While we're talking about creating tags, the changelog producer will only use these values for GitHub release
59 | /// rotes generation.
60 | let targetCommitish: String?
61 |
62 | /// The previous tag to compare against. Will only be used for GitHub release notes generation.
63 | let previousTagName: String?
64 |
65 | init(
66 | since: Since = .latestTag,
67 | to: Date = Date(),
68 | baseBranch: Branch?,
69 | useGitHubReleaseNotes: Bool = false,
70 | tagName: String? = nil,
71 | targetCommitish: String? = nil,
72 | previousTagName: String? = nil
73 | ) throws {
74 | try Octokit.authenticate()
75 |
76 | self.to = to
77 | self.since = since
78 |
79 | let from = try since.get()
80 |
81 | if from != self.to {
82 | self.from = from
83 | } else {
84 | Log.debug("From date could not be determined. Using today minus 30 days as the base date")
85 | self.from = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
86 | }
87 |
88 | Log.debug("Getting all changes between \(self.from) and \(self.to)")
89 | self.baseBranch = baseBranch ?? "master"
90 | project = GITProject.current()
91 | self.useGitHubReleaseNotes = useGitHubReleaseNotes
92 | self.tagName = tagName
93 | self.targetCommitish = targetCommitish
94 | self.previousTagName = previousTagName
95 | }
96 |
97 | @discardableResult public func run(isSectioned: Bool) throws -> Changelog {
98 | if useGitHubReleaseNotes, let tagName, let targetCommitish, let previousTagName {
99 | return try GitHubReleaseNotesGenerator(
100 | octoKit: octoKit,
101 | project: project,
102 | tagName: tagName,
103 | targetCommitish: targetCommitish,
104 | previousTagName: previousTagName
105 | ).generate(using: urlSession)
106 | } else {
107 | return try generateChangelogUsingPRsAndIssues(isSectioned: isSectioned)
108 | }
109 | }
110 |
111 | private func generateChangelogUsingPRsAndIssues(isSectioned: Bool) throws -> Changelog {
112 | let pullRequestsFetcher = PullRequestFetcher(octoKit: octoKit, baseBranch: baseBranch, project: project)
113 | let pullRequests = try pullRequestsFetcher.fetchAllBetween(from, and: to, using: urlSession)
114 |
115 | if Log.isVerbose {
116 | Log.debug("\nChangelog will use the following pull requests as input:")
117 | pullRequests.forEach { pullRequest in
118 | guard let title = pullRequest.title, let mergedAt = pullRequest.mergedAt else { return }
119 | Log.debug("- #\(pullRequest.number): \(title), merged at: \(mergedAt)\n")
120 | }
121 | }
122 |
123 | if isSectioned {
124 | let issuesFetcher = IssuesFetcher(octoKit: octoKit, project: project)
125 | let issues = try issuesFetcher.fetchAllBetween(from, and: to, using: urlSession)
126 |
127 | if Log.isVerbose {
128 | Log.debug("\nChangelog will use the following issues as input:")
129 | issues.forEach { issue in
130 | guard let title = issue.title, let closedAt = issue.closedAt else { return }
131 | Log.debug("- #\(issue.number): \(title), closed at: \(closedAt)\n")
132 | }
133 | }
134 |
135 | return SectionedChangelog(issues: issues, pullRequests: pullRequests)
136 | } else {
137 | let items = ChangelogItemsFactory(
138 | octoKit: octoKit,
139 | pullRequests: pullRequests,
140 | project: project
141 | ).items(using: urlSession)
142 |
143 | Log.debug("Result of creating the changelog:")
144 | return SingleSectionChangelog(items: items)
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Commands/ChangelogCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangelogCommand.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 09/04/2020.
6 | //
7 |
8 | import ArgumentParser
9 | import Foundation
10 |
11 | struct ChangelogCommand: ParsableCommand {
12 | public static let configuration = CommandConfiguration(commandName: "changelog", abstract: "Create a changelog for GitHub repositories")
13 |
14 | @Option(name: .shortAndLong, help: "The tag to use as a base. Defaults to the latest tag.")
15 | private var sinceTag: String?
16 |
17 | @Option(name: .shortAndLong, help: "The base branch to compare with. Defaults to master.")
18 | private var baseBranch: String?
19 |
20 | @Flag(name: .customLong("sections"), help: "Whether the changelog should be split into sections. Defaults to false.")
21 | private var isSectioned: Bool = false
22 |
23 | @Flag(name: .long, help: "Show extra logging for debugging purposes")
24 | private var verbose: Bool = false
25 |
26 | func run() throws {
27 | Log.isVerbose = verbose
28 |
29 | let sinceTag = self.sinceTag.map { ChangelogProducer.Since.tag(tag: $0) }
30 | let changelogProducer = try ChangelogProducer(since: sinceTag ?? .latestTag,
31 | baseBranch: baseBranch)
32 | let changelog = try changelogProducer.run(isSectioned: isSectioned)
33 | Log.message(changelog.description)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Commands/GitBuddy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GitBuddy.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 03/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import ArgumentParser
10 | import Foundation
11 |
12 | /// Entry class of GitBuddy that registers commands and handles execution.
13 | public struct GitBuddy: ParsableCommand {
14 | public static let version = "4.3.0"
15 |
16 | public static let configuration = CommandConfiguration(
17 | commandName: "gitbuddy",
18 | abstract: "Manage your GitHub repositories with ease",
19 | version: Self.version,
20 | subcommands: [ChangelogCommand.self, ReleaseCommand.self, TagDeletionsCommand.self]
21 | )
22 |
23 | public init() {}
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Commands/ReleaseCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReleaseCommand.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 09/04/2020.
6 | //
7 |
8 | import ArgumentParser
9 | import Foundation
10 |
11 | struct ReleaseCommand: ParsableCommand {
12 | public static let configuration = CommandConfiguration(
13 | commandName: "release",
14 | abstract: "Create a new release including a changelog and publish comments on related issues."
15 | )
16 |
17 | @Option(name: .shortAndLong, help: "The path to the Changelog to update it with the latest changes.")
18 | var changelogPath: String?
19 |
20 | @Flag(name: .shortAndLong, help: "Disable commenting on issues and PRs about the new release.")
21 | var skipComments: Bool = false
22 |
23 | @Flag(name: [.customLong("use-pre-release"), .customShort("p")], help: "Create the release as a pre-release.")
24 | var isPrerelease: Bool = false
25 |
26 | // Main
27 | @Option(
28 | name: .shortAndLong,
29 | help: """
30 | Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA.
31 | Unused if the Git tag already exists.
32 |
33 | Default: the repository's default branch (usually main).
34 | """
35 | )
36 | var targetCommitish: String?
37 |
38 | // 1.3.1b1072
39 | @Option(
40 | name: [.long, .customShort("n")],
41 | help: """
42 | The name of the tag. If set, `changelogToTag` is required too.
43 |
44 | Default: takes the last created tag to publish as a GitHub release.
45 | """
46 | )
47 | var tagName: String?
48 |
49 | // // 1.3.1b1072 - App Store Release
50 | @Option(name: .shortAndLong, help: "The title of the release. Default: uses the tag name.")
51 | var releaseTitle: String?
52 |
53 | // 1.3.0b1039
54 | @Option(name: .shortAndLong, help: "The last release tag to use as a base for the changelog creation. Default: previous tag.")
55 | var lastReleaseTag: String?
56 |
57 | @Option(
58 | name: .customLong("changelogToTag"),
59 | help: """
60 | If set, the date of this tag will be used as the limit for the changelog creation.
61 | This variable should be passed when `tagName` is set.
62 |
63 | Default: latest tag.
64 | """
65 | )
66 | var changelogToTag: String?
67 |
68 | // Develop
69 | @Option(name: .shortAndLong, help: "The base branch to compare with for generating the changelog. Defaults to master.")
70 | var baseBranch: String?
71 |
72 | @Flag(name: .customLong("sections"), help: "Whether the changelog should be split into sections. Defaults to false.")
73 | private var isSectioned: Bool = false
74 |
75 | @Flag(name: .customLong("use-github-release-notes"), help: "If `true`, release notes will be generated by GitHub. Defaults to false, which will lead to a changelog based on PR and issue titles matching the tag changes.")
76 | private var useGitHubReleaseNotes: Bool = false
77 |
78 | @Flag(name: .customLong("json"), help: "Whether the release output should be in JSON, containing more details. Defaults to false.")
79 | private var shouldUseJSONOutput: Bool = false
80 |
81 | @Flag(name: .long, help: "Show extra logging for debugging purposes")
82 | private var verbose: Bool = false
83 |
84 | func run() throws {
85 | Log.isVerbose = verbose
86 |
87 | let releaseProducer = try ReleaseProducer(changelogPath: changelogPath,
88 | skipComments: skipComments,
89 | isPrerelease: isPrerelease,
90 | targetCommitish: targetCommitish,
91 | tagName: tagName,
92 | releaseTitle: releaseTitle,
93 | lastReleaseTag: lastReleaseTag,
94 | baseBranch: baseBranch,
95 | changelogToTag: changelogToTag,
96 | useGitHubReleaseNotes: useGitHubReleaseNotes)
97 | let release = try releaseProducer.run(isSectioned: isSectioned)
98 |
99 | if shouldUseJSONOutput {
100 | let jsonData = try JSONEncoder().encode(release)
101 | let jsonString = String(decoding: jsonData, as: UTF8.self)
102 | Log.message(jsonString)
103 | } else {
104 | Log.message(release.url.absoluteString)
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Commands/TagDeletionsCommand.swift:
--------------------------------------------------------------------------------
1 | import ArgumentParser
2 | import Foundation
3 |
4 | struct TagDeletionsCommand: ParsableCommand {
5 | public static let configuration = CommandConfiguration(
6 | commandName: "tagDeletion",
7 | abstract: "Delete a batch of tags based on given predicates."
8 | )
9 |
10 | @Option(name: .shortAndLong, help: "The date of this tag will be used as a limit. Defaults to the latest tag.")
11 | private var upUntilTag: String?
12 |
13 | @Option(name: .shortAndLong, help: "The limit of tags to delete in this batch. Defaults to 50")
14 | private var limit: Int = 50
15 |
16 | @Flag(name: .long, help: "Delete pre releases only")
17 | private var prereleaseOnly: Bool = false
18 |
19 | @Flag(name: .long, help: "Does not actually delete but just logs which tags would be deleted")
20 | private var dryRun: Bool = false
21 |
22 | @Flag(name: .long, help: "Show extra logging for debugging purposes")
23 | var verbose: Bool = false
24 |
25 | func run() throws {
26 | Log.isVerbose = verbose
27 |
28 | let tagsDeleter = try TagsDeleter(upUntilTagName: upUntilTag, limit: limit, prereleaseOnly: prereleaseOnly, dryRun: dryRun)
29 | let deletedTags = try tagsDeleter.run()
30 |
31 | guard !deletedTags.isEmpty else {
32 | Log.message("There were no tags found to be deleted.")
33 | return
34 | }
35 | Log.message("Deleted tags: \(deletedTags)")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/GitHub/Commenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Commenter.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 06/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | enum Comment {
13 | case releasedPR(release: Release)
14 | case releasedIssue(release: Release, pullRequestID: Int)
15 |
16 | var body: String {
17 | switch self {
18 | case .releasedPR(let release):
19 | return "Congratulations! :tada: This was released as part of [Release \(release.title)](\(release.url)) :rocket:"
20 | case .releasedIssue(let release, let pullRequestID):
21 | var body = "The pull request #\(pullRequestID) that closed this issue was merged and released "
22 | body += "as part of [Release \(release.title)](\(release.url)) :rocket:\n"
23 | body += "Please let us know if the functionality works as expected as a reply here. "
24 | body += "If it does not, please open a new issue. Thanks!"
25 | return body
26 | }
27 | }
28 | }
29 |
30 | /// Responsible for posting comments on issues at GitHub.
31 | enum Commenter {
32 | static var urlSession: URLSession { URLSessionInjector.urlSession }
33 |
34 | /// Adds a right aligned watermark to each comment to grow our audience.
35 | static var watermark: String {
36 | return "Generated by GitBuddy
"
37 | }
38 |
39 | /// Posts a given comment on the issue from the given project.
40 | /// - Parameters:
41 | /// - comment: The comment to post.
42 | /// - issueID: The issue ID on which the comment has to be posted.
43 | /// - project: The project on which the issue exists. E.g. WeTransfer/Diagnostics.
44 | /// - completion: The completion callback which will be called once the comment is placed.
45 | static func post(_ comment: Comment, on issueID: Int, at project: GITProject, completion: @escaping () -> Void) {
46 | var body = comment.body
47 | body += watermark
48 | Octokit()
49 | .commentIssue(urlSession, owner: project.organisation, repository: project.repository, number: issueID,
50 | body: body)
51 | { response in
52 | switch response {
53 | case .success(let comment):
54 | Log.debug("Successfully posted comment at: \(comment.htmlURL)")
55 | case .failure(let error):
56 | Log.debug("Posting comment for issue #\(issueID) failed: \(error)")
57 | }
58 | completion()
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/GitHub/GitHubReleaseNotesGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GitHubReleaseNotesGenerator.swift
3 | //
4 | //
5 | // Created by Antoine van der Lee on 13/06/2023.
6 | // Copyright © 2023 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | struct GitHubReleaseNotesGenerator {
13 | let octoKit: Octokit
14 | let project: GITProject
15 | let tagName: String
16 | let targetCommitish: String
17 | let previousTagName: String
18 |
19 | func generate(using session: URLSession = URLSession.shared) throws -> ReleaseNotes {
20 | let group = DispatchGroup()
21 | group.enter()
22 |
23 | var result: Result!
24 |
25 | octoKit.generateReleaseNotes(
26 | session,
27 | owner: project.organisation,
28 | repository: project.repository,
29 | tagName: tagName,
30 | targetCommitish: targetCommitish,
31 | previousTagName: previousTagName) { response in
32 | switch response {
33 | case .success(let releaseNotes):
34 | result = .success(releaseNotes)
35 | case .failure(let error):
36 | result = .failure(OctoKitError(error: error))
37 | }
38 | group.leave()
39 | }
40 |
41 | group.wait()
42 |
43 | return try result.get()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/GitHub/IssuesFetcher.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssuesFetcher.swift
3 | //
4 | //
5 | // Created by Max Desiatov on 26/03/2020.
6 | //
7 |
8 | import Foundation
9 | import OctoKit
10 |
11 | struct IssuesFetcher {
12 | let octoKit: Octokit
13 | let project: GITProject
14 |
15 | /// Fetches issues and filters them by a given `fromDate` and `toDate`.
16 | func fetchAllBetween(_ fromDate: Date, and toDate: Date, using session: URLSession = URLSession.shared) throws -> [Issue] {
17 | let group = DispatchGroup()
18 | group.enter()
19 |
20 | var result: Result<[Issue], Swift.Error>!
21 |
22 | octoKit.issues(session, owner: project.organisation, repository: project.repository, state: .closed) { response in
23 | switch response {
24 | case .success(let issues):
25 | result = .success(issues)
26 | case .failure(let error):
27 | result = .failure(OctoKitError(error: error))
28 | }
29 | group.leave()
30 | }
31 | group.wait()
32 |
33 | return try result.get().filter { issue -> Bool in
34 | guard
35 | // It looks like OctoKit.swift doesn't support `pull_request` property that helps in
36 | // distinguishing between issues and pull requests, as the issues endpoint returns both.
37 | // See https://developer.github.com/v3/issues/ for more details.
38 | issue.htmlURL?.pathComponents.contains("issues") ?? false,
39 | let closedAt = issue.closedAt
40 | else { return false }
41 | return closedAt > fromDate && closedAt < toDate
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/GitHub/IssuesResolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssuesResolver.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | struct IssuesResolver {
13 | let octoKit: Octokit
14 | let project: GITProject
15 | let input: ChangelogInput
16 |
17 | func resolve(using session: URLSession = URLSession.shared) -> [Issue]? {
18 | guard let resolvedIssueNumbers = input.body?.resolvingIssues(), !resolvedIssueNumbers.isEmpty else {
19 | return nil
20 | }
21 | return issues(for: resolvedIssueNumbers, using: session)
22 | }
23 |
24 | private func issues(for issueNumbers: [Int], using session: URLSession) -> [Issue] {
25 | var issues: [Issue] = []
26 | let dispatchGroup = DispatchGroup()
27 | for issueNumber in issueNumbers {
28 | dispatchGroup.enter()
29 | octoKit.issue(session, owner: project.organisation, repository: project.repository, number: issueNumber) { response in
30 | switch response {
31 | case .success(let issue):
32 | issues.append(issue)
33 | case .failure(let error):
34 | print("Fetching issue \(issueNumber) failed with \(error)")
35 | }
36 | dispatchGroup.leave()
37 | }
38 | }
39 | dispatchGroup.wait()
40 | return issues
41 | }
42 | }
43 |
44 | extension String {
45 | /// Extracts the resolved issues from a Pull Request body.
46 | func resolvingIssues() -> [Int] {
47 | var resolvedIssues = Set()
48 |
49 | let splits = split(separator: "#")
50 |
51 | let issueClosingKeywords = [
52 | "close ",
53 | "closes ",
54 | "closed ",
55 | "fix ",
56 | "fixes ",
57 | "fixed ",
58 | "resolve ",
59 | "resolves ",
60 | "resolved "
61 | ]
62 |
63 | for (index, split) in splits.enumerated() {
64 | let lowerCaseSplit = split.lowercased()
65 |
66 | for keyword in issueClosingKeywords {
67 | if lowerCaseSplit.hasSuffix(keyword) {
68 | guard index + 1 <= splits.count - 1 else { break }
69 | let nextSplit = splits[index + 1]
70 |
71 | let numberPrefixString = nextSplit.prefix { character -> Bool in
72 | character.isNumber
73 | }
74 |
75 | if !numberPrefixString.isEmpty, let numberPrefix = Int(numberPrefixString.description) {
76 | resolvedIssues.insert(numberPrefix)
77 | break
78 | } else {
79 | continue
80 | }
81 | }
82 | }
83 | }
84 |
85 | return Array(resolvedIssues)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/GitHub/OctoKit+Authentication.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLSessionConfiguration+Authentication.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 09/04/2020.
6 | //
7 |
8 | import Foundation
9 | import OctoKit
10 |
11 | extension Octokit {
12 | static var protocolClasses: [AnyClass]?
13 | static var environment: [String: String] = ProcessInfo.processInfo.environment
14 |
15 | /// Sets up the URLSession to be authenticated for the GitHub API.
16 | static func authenticate() throws {
17 | let token = try Token(environment: environment)
18 | Log.debug("Token is \(token)")
19 | let configuration = URLSessionConfiguration.default
20 | configuration.httpAdditionalHeaders = ["Authorization": "Basic \(token.base64Encoded)"]
21 | configuration.protocolClasses = protocolClasses ?? configuration.protocolClasses
22 | URLSessionInjector.urlSession = URLSession(configuration: configuration, delegate: nil, delegateQueue: nil)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/GitHub/PullRequestFetcher.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PullRequestFetcher.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | typealias Branch = String
13 |
14 | struct PullRequestFetcher {
15 | let octoKit: Octokit
16 | let baseBranch: Branch
17 | let project: GITProject
18 |
19 | func fetchAllBetween(_ fromDate: Date, and toDate: Date, using session: URLSession = URLSession.shared) throws -> [PullRequest] {
20 | let group = DispatchGroup()
21 | group.enter()
22 |
23 | var result: Result<[PullRequest], Swift.Error>!
24 |
25 | octoKit.pullRequests(
26 | session,
27 | owner: project.organisation,
28 | repository: project.repository,
29 | base: baseBranch,
30 | state: .closed,
31 | sort: .updated,
32 | direction: .desc,
33 | perPage: 100
34 | ) { response in
35 | switch response {
36 | case .success(let pullRequests):
37 | result = .success(pullRequests)
38 | case .failure(let error):
39 | result = .failure(OctoKitError(error: error))
40 | }
41 | group.leave()
42 | }
43 | group.wait()
44 |
45 | return try result.get().filter { pullRequest -> Bool in
46 | guard let mergedAt = pullRequest.mergedAt else { return false }
47 | return mergedAt > fromDate && mergedAt < toDate
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Helpers/DateFormatters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateFormatters.swift
3 | //
4 | //
5 | // Created by Antoine van der Lee on 25/01/2022.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum Formatter {
12 | static let gitDateFormatter: DateFormatter = {
13 | let dateFormatter = DateFormatter()
14 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
15 | dateFormatter.locale = Locale(identifier: "en_US_POSIX")
16 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
17 | return dateFormatter
18 | }()
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Helpers/DependencyInjectors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DependencyInjectors.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 03/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Adds a `urlSession` property which defaults to `URLSession.shared`.
12 | protocol URLSessionInjectable {}
13 |
14 | extension URLSessionInjectable {
15 | var urlSession: URLSession { return URLSessionInjector.urlSession }
16 | }
17 |
18 | enum URLSessionInjector {
19 | /// Will be setup using the configuration inside the GitBuddy.run method
20 | static var urlSession: URLSession!
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Helpers/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Log.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum Log {
12 | static var isVerbose: Bool = false
13 |
14 | /// Can be used to catch output for testing purposes.
15 | static var pipe: ((_ message: String) -> Void)?
16 |
17 | static func debug(_ message: String) {
18 | guard isVerbose else { return }
19 | print(message)
20 | }
21 |
22 | static func message(_ message: String) {
23 | print(message)
24 | }
25 |
26 | private static func print(_ message: String) {
27 | pipe?(message)
28 | Swift.print(message)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Helpers/OctoKitError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Antoine van der Lee on 20/10/2022.
3 | // Copyright © 2022 WeTransfer. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | /// Ensures rich details become available when GitBuddy fails due to a failing GitHub API request.
9 | struct OctoKitError: LocalizedError {
10 |
11 | let statusCode: Int
12 | let underlyingError: Error
13 | let errorDetails: String
14 |
15 | var errorDescription: String? {
16 | """
17 | GitHub API Request failed (StatusCode: \(statusCode)): \(errorDetails)
18 | Underlying error:
19 | \(underlyingError)
20 | """
21 | }
22 |
23 | init(error: Error) {
24 | underlyingError = error
25 | statusCode = error._code
26 | errorDetails = (error as NSError).userInfo.debugDescription
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Helpers/Shell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Shell.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum ShellCommand {
12 | case fetchTags
13 | case latestTag
14 | case previousTag
15 | case repositoryName
16 | case tagCreationDate(tag: String)
17 | case commitDate(commitish: String)
18 |
19 | var rawValue: String {
20 | switch self {
21 | case .fetchTags:
22 | return "git fetch --tags origin --no-recurse-submodules -q"
23 | case .latestTag:
24 | return "git describe --abbrev=0 --tags `git rev-list --tags --max-count=1 --no-walk`"
25 | case .previousTag:
26 | return "git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1 --no-walk`"
27 | case .repositoryName:
28 | return "git remote show origin -n | ruby -ne 'puts /^\\s*Fetch.*(:|\\/){1}([^\\/]+\\/[^\\/]+).git/.match($_)[2] rescue nil'"
29 | case .tagCreationDate(let tag):
30 | return "git log -1 --format=%ai \(tag)"
31 | case .commitDate(let commitish):
32 | return "git show -s --format=%ai \(commitish)"
33 | }
34 | }
35 | }
36 |
37 | extension Process {
38 | func shell(_ command: ShellCommand) -> String {
39 | launchPath = "/bin/bash"
40 | arguments = ["-c", command.rawValue]
41 |
42 | let outputPipe = Pipe()
43 | standardOutput = outputPipe
44 | launch()
45 |
46 | let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
47 | guard let outputData = String(data: data, encoding: String.Encoding.utf8) else { return "" }
48 |
49 | return outputData
50 | .reduce("") { result, value in
51 | result + String(value)
52 | }
53 | .trimmingCharacters(in: .whitespacesAndNewlines)
54 | }
55 | }
56 |
57 | protocol ShellExecuting {
58 | @discardableResult static func execute(_ command: ShellCommand) -> String
59 | }
60 |
61 | private enum Shell: ShellExecuting {
62 | @discardableResult static func execute(_ command: ShellCommand) -> String {
63 | return Process().shell(command)
64 | }
65 | }
66 |
67 | /// Adds a `shell` property which defaults to `Shell.self`.
68 | protocol ShellInjectable {}
69 |
70 | extension ShellInjectable {
71 | static var shell: ShellExecuting.Type { ShellInjector.shell }
72 | }
73 |
74 | enum ShellInjector {
75 | static var shell: ShellExecuting.Type = Shell.self
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Models/Changelog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Changelog.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 05/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | typealias PullRequestID = Int
13 | typealias IssueID = Int
14 |
15 | /// Generalizes different types of changelogs with either single or multiple sections.
16 | protocol Changelog: CustomStringConvertible {
17 | /// The pull requests ID and related issues IDs that are merged with the related release of this
18 | /// changelog. It is used to post update comments on corresponding PRs and issues when a release
19 | /// is published.
20 | var itemIdentifiers: [PullRequestID: [IssueID]] { get }
21 | }
22 |
23 | extension ReleaseNotes: Changelog {
24 | var itemIdentifiers: [PullRequestID : [IssueID]] {
25 | [:]
26 | }
27 |
28 | public var description: String { body }
29 | }
30 |
31 | /// Represents a changelog with a single section of changelog items.
32 | struct SingleSectionChangelog: Changelog {
33 | let description: String
34 |
35 | let itemIdentifiers: [PullRequestID: [IssueID]]
36 |
37 | init(items: [ChangelogItem]) {
38 | description = ChangelogBuilder(items: items).build()
39 | itemIdentifiers = items.reduce(into: [:]) { result, item in
40 | let pullRequestID: PullRequestID = item.closedBy.number
41 |
42 | if var pullRequestIssues = result[pullRequestID] {
43 | guard let issue = item.input as? ChangelogIssue else { return }
44 | pullRequestIssues.append(issue.number)
45 | result[pullRequestID] = pullRequestIssues
46 | } else if let issue = item.input as? ChangelogIssue {
47 | result[pullRequestID] = [issue.number]
48 | } else {
49 | result[pullRequestID] = []
50 | }
51 | }
52 | }
53 | }
54 |
55 | /// Represents a changelog with at least two sections, one for closed issues, the other for
56 | /// merged pull requests.
57 | struct SectionedChangelog: Changelog {
58 | let description: String
59 |
60 | let itemIdentifiers: [PullRequestID: [IssueID]]
61 |
62 | init(issues: [Issue], pullRequests: [PullRequest]) {
63 | description =
64 | """
65 | **Closed issues:**
66 |
67 | \(ChangelogBuilder(items: issues.map { ChangelogItem(input: $0, closedBy: $0) }).build())
68 |
69 | **Merged pull requests:**
70 |
71 | \(ChangelogBuilder(items: pullRequests.map { ChangelogItem(input: $0, closedBy: $0) }).build())
72 | """
73 |
74 | itemIdentifiers = pullRequests.reduce(into: [:]) { result, item in
75 | result[item.number] = item.body?.resolvingIssues()
76 | }
77 | }
78 | }
79 |
80 | extension PullRequest: Hashable {
81 | public func hash(into hasher: inout Hasher) {
82 | hasher.combine(id)
83 | }
84 |
85 | public static func == (lhs: PullRequest, rhs: PullRequest) -> Bool {
86 | return lhs.id == rhs.id
87 | }
88 | }
89 |
90 | extension Issue: Hashable {
91 | public func hash(into hasher: inout Hasher) {
92 | hasher.combine(id)
93 | }
94 |
95 | public static func == (lhs: Issue, rhs: Issue) -> Bool {
96 | return lhs.id == rhs.id
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Models/ChangelogItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangelogItem.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | protocol ChangelogInput {
13 | var number: Int { get }
14 | var title: String? { get }
15 | var body: String? { get }
16 | var htmlURL: Foundation.URL? { get }
17 | var username: String? { get }
18 | }
19 |
20 | protocol ChangelogIssue: ChangelogInput {}
21 | protocol ChangelogPullRequest: ChangelogInput {}
22 |
23 | extension PullRequest: ChangelogPullRequest {
24 | var username: String? { user?.login ?? assignee?.login }
25 | }
26 |
27 | extension Issue: ChangelogIssue {
28 | var username: String? { assignee?.login }
29 | }
30 |
31 | struct ChangelogItem {
32 | let input: ChangelogInput
33 | let closedBy: ChangelogInput
34 |
35 | var title: String? {
36 | guard var title = input.title else { return nil }
37 | title = title.prefix(1).uppercased() + title.dropFirst()
38 | title = title.removingEmojis().trimmingCharacters(in: .whitespaces)
39 |
40 | if let htmlURL = input.htmlURL {
41 | title += " ([#\(input.number)](\(htmlURL)))"
42 | }
43 | if closedBy is ChangelogPullRequest, let username = closedBy.username {
44 | title += " via [@\(username)](https://github.com/\(username))"
45 | }
46 | return title
47 | }
48 | }
49 |
50 | private extension String {
51 | func removingEmojis() -> String {
52 | String(unicodeScalars.filter {
53 | !$0.properties.isEmojiPresentation
54 | })
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Models/GITProject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GITProject.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct GITProject: ShellInjectable {
12 | let organisation: String
13 | let repository: String
14 |
15 | static func current() -> GITProject {
16 | /// E.g. WeTransfer/Coyote
17 | let projectInfo = shell.execute(.repositoryName)
18 | .split(separator: "/")
19 | .map { String($0) }
20 |
21 | return GITProject(organisation: String(projectInfo[0]), repository: String(projectInfo[1]))
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Models/Release.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Release.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 05/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// A release that exists on GitHub.
12 | struct Release: Encodable {
13 | let tagName: String
14 | let url: URL
15 | let title: String
16 | let changelog: String
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Models/Tag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tag.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Tag: ShellInjectable, Encodable {
12 | enum Error: Swift.Error, CustomStringConvertible {
13 | case missingTagCreationDate
14 | case noTagsAvailable
15 |
16 | var description: String {
17 | switch self {
18 | case .missingTagCreationDate:
19 | return "Tag creation date could not be determined"
20 | case .noTagsAvailable:
21 | return "There's no tags available"
22 | }
23 | }
24 | }
25 |
26 | let name: String
27 | let created: Date
28 |
29 | /// Creates a new Tag instance.
30 | /// - Parameters:
31 | /// - name: The name to use for the tag.
32 | /// - created: The creation date to use. If `nil`, the date is fetched using the `git` terminal command. See `fallbackDate`
33 | /// for setting a date if this operation fails due to a missing tag.
34 | /// - Throws: An error if the creation date could not be found.
35 | init(name: String, created: Date? = nil) throws {
36 | self.name = name
37 |
38 | if let created = created {
39 | self.created = created
40 | } else {
41 | let tagCreationDate = Self.shell.execute(.tagCreationDate(tag: name))
42 | if tagCreationDate.isEmpty {
43 | Log.debug("Tag creation date could not be found for \(name)")
44 | throw Error.missingTagCreationDate
45 | }
46 |
47 | Log.debug("Tag \(name) is created at \(tagCreationDate)")
48 |
49 | guard let date = Formatter.gitDateFormatter.date(from: tagCreationDate) else {
50 | throw Error.missingTagCreationDate
51 | }
52 | self.created = date
53 | }
54 | }
55 |
56 | static func latest() throws -> Self {
57 | Log.debug("Fetching tags")
58 | shell.execute(.fetchTags)
59 | let latestTag = shell.execute(.latestTag)
60 |
61 | guard !latestTag.isEmpty else { throw Error.noTagsAvailable }
62 |
63 | Log.debug("Latest tag is \(latestTag)")
64 |
65 | return try Tag(name: latestTag)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Models/Token.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Token.swift
3 | // GitBuddyCore
4 | //
5 | // Created by Antoine van der Lee on 10/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Token: CustomStringConvertible {
12 | enum Error: Swift.Error, LocalizedError {
13 | case missingAccessToken
14 | case invalidAccessToken
15 |
16 | var errorDescription: String? { debugDescription }
17 | var debugDescription: String {
18 | switch self {
19 | case .missingAccessToken:
20 | return "GitHub Access Token is missing. Add an environment variable: GITBUDDY_ACCESS_TOKEN='username:access_token'"
21 | case .invalidAccessToken:
22 | return "Access token is found but invalid. Correct format: :"
23 | }
24 | }
25 | }
26 |
27 | let username: String
28 | let accessToken: String
29 |
30 | var base64Encoded: String {
31 | "\(username):\(accessToken)".data(using: .utf8)!.base64EncodedString()
32 | }
33 |
34 | var description: String {
35 | "\(username):\(accessToken.prefix(5))..."
36 | }
37 |
38 | init(environment: [String: String]) throws {
39 | guard let gitHubAccessToken = environment["GITBUDDY_ACCESS_TOKEN"] else {
40 | throw Error.missingAccessToken
41 | }
42 | let tokenParts = gitHubAccessToken.split(separator: ":")
43 | guard tokenParts.count == 2, let username = tokenParts.first, let accessToken = tokenParts.last else {
44 | throw Error.invalidAccessToken
45 | }
46 |
47 | self.username = String(username)
48 | self.accessToken = String(accessToken)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Release/ReleaseProducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReleaseProducer.swift
3 | // GitBuddy
4 | //
5 | // Created by Antoine van der Lee on 04/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OctoKit
11 |
12 | /// Capable of producing a release, adjusting a Changelog file, and posting comments to released issues/PRs.
13 | final class ReleaseProducer: URLSessionInjectable, ShellInjectable {
14 | enum Error: Swift.Error, CustomStringConvertible {
15 | case changelogTargetDateMissing
16 |
17 | var description: String {
18 | switch self {
19 | case .changelogTargetDateMissing:
20 | return "Tag name is set, but `changelogToTag` is missing"
21 | }
22 | }
23 | }
24 |
25 | private lazy var octoKit: Octokit = .init()
26 |
27 | /// The path to the Changelog to update it with the latest changes.
28 | let changelogURL: Foundation.URL?
29 |
30 | /// Disable commenting on issues and PRs about the new release.
31 | let skipComments: Bool
32 |
33 | /// Create the release as a pre-release.
34 | let isPrerelease: Bool
35 |
36 | /*
37 | Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA.
38 | Unused if the Git tag already exists.
39 |
40 | Default: the repository's default branch (usually main).
41 | */
42 | let targetCommitish: String?
43 |
44 | /*
45 | The name of the tag. If set, `changelogToTag` is required too.
46 |
47 | Default: takes the last created tag to publish as a GitHub release.
48 | */
49 | let tagName: String?
50 |
51 | /// The title of the release. Default: uses the tag name.
52 | let releaseTitle: String?
53 |
54 | /// The last release tag to use as a base for the changelog creation. Default: previous tag.
55 | let lastReleaseTag: String?
56 |
57 | /*
58 | If set, the date of this tag will be used as the limit for the changelog creation.
59 | This variable should be passed when `tagName` is set.
60 |
61 | Default: latest tag.
62 | */
63 | let changelogToTag: String?
64 |
65 | /// The base branch to compare with for generating the changelog. Defaults to master.
66 | let baseBranch: String
67 |
68 | /// If `true`, release notes will be generated by GitHub.
69 | /// Defaults to false, which will lead to a changelog based on PR and issue titles matching the tag changes.
70 | let useGitHubReleaseNotes: Bool
71 |
72 | init(
73 | changelogPath: String?,
74 | skipComments: Bool,
75 | isPrerelease: Bool,
76 | targetCommitish: String? = nil,
77 | tagName: String? = nil,
78 | releaseTitle: String? = nil,
79 | lastReleaseTag: String? = nil,
80 | baseBranch: String? = nil,
81 | changelogToTag: String? = nil,
82 | useGitHubReleaseNotes: Bool
83 | ) throws {
84 | try Octokit.authenticate()
85 |
86 | if let changelogPath = changelogPath {
87 | changelogURL = URL(string: changelogPath)
88 | } else {
89 | changelogURL = nil
90 | }
91 | self.skipComments = skipComments
92 | self.isPrerelease = isPrerelease
93 | self.targetCommitish = targetCommitish
94 | self.tagName = tagName
95 | self.releaseTitle = releaseTitle
96 | self.lastReleaseTag = lastReleaseTag
97 | self.changelogToTag = changelogToTag
98 | self.baseBranch = baseBranch ?? "master"
99 | self.useGitHubReleaseNotes = useGitHubReleaseNotes
100 | }
101 |
102 | @discardableResult public func run(isSectioned: Bool) throws -> Release {
103 | let changelogToDate = try fetchChangelogToDate()
104 |
105 | /// We're adding 60 seconds to make sure the tag commit itself is included in the changelog as well.
106 | let adjustedChangelogToDate = changelogToDate.addingTimeInterval(60)
107 | let tagName = try tagName ?? Tag.latest().name
108 |
109 | let changelogSinceTag = lastReleaseTag ?? Self.shell.execute(.previousTag)
110 | let changelogProducer = try ChangelogProducer(
111 | since: .tag(tag: changelogSinceTag),
112 | to: adjustedChangelogToDate,
113 | baseBranch: baseBranch,
114 | useGitHubReleaseNotes: useGitHubReleaseNotes,
115 | tagName: tagName,
116 | targetCommitish: targetCommitish,
117 | previousTagName: changelogSinceTag
118 | )
119 | let changelog = try changelogProducer.run(isSectioned: isSectioned)
120 | Log.debug("\(changelog)\n")
121 |
122 | try updateChangelogFile(adding: changelog.description, for: tagName)
123 |
124 | let repositoryName = Self.shell.execute(.repositoryName)
125 | let project = GITProject.current()
126 | Log.debug("Creating a release for tag \(tagName) at repository \(repositoryName)")
127 | let release = try createRelease(
128 | using: project,
129 | tagName: tagName,
130 | targetCommitish: targetCommitish,
131 | title: releaseTitle,
132 | body: changelog.description
133 | )
134 | postComments(for: changelog, project: project, release: release)
135 |
136 | Log.debug("Result of creating the release:\n")
137 | return release
138 | }
139 |
140 | private func fetchChangelogToDate() throws -> Date {
141 | if tagName != nil {
142 | /// If a tagname exists, it means we're creating a new tag.
143 | /// In this case, we need another way to fetch the `to` date for the changelog.
144 | ///
145 | /// One option is using the `changelogToTag`:
146 | if let changelogToTag = changelogToTag {
147 | return try Tag(name: changelogToTag).created
148 | } else if let targetCommitishDate = targetCommitishDate() {
149 | /// We fallback to the target commit date, covering cases in which we create a release
150 | /// from a certain branch
151 | return targetCommitishDate
152 | } else {
153 | /// Since we were unable to fetch the date
154 | throw Error.changelogTargetDateMissing
155 | }
156 | } else {
157 | /// This is the scenario of creating a release for an already created tag.
158 | return try Tag.latest().created
159 | }
160 | }
161 |
162 | private func targetCommitishDate() -> Date? {
163 | guard let targetCommitish = targetCommitish else {
164 | return nil
165 | }
166 | let commitishDate = Self.shell.execute(.commitDate(commitish: targetCommitish))
167 | return Formatter.gitDateFormatter.date(from: commitishDate)
168 | }
169 |
170 | private func postComments(for changelog: Changelog, project: GITProject, release: Release) {
171 | guard !skipComments else {
172 | Log.debug("Skipping comments")
173 | return
174 | }
175 | let dispatchGroup = DispatchGroup()
176 | for (pullRequestID, issueIDs) in changelog.itemIdentifiers {
177 | Log.debug("Marking PR #\(pullRequestID) as having been released in version #\(release.tagName)")
178 | dispatchGroup.enter()
179 | Commenter.post(.releasedPR(release: release), on: pullRequestID, at: project) {
180 | dispatchGroup.leave()
181 | }
182 |
183 | issueIDs.forEach { issueID in
184 | Log.debug("Adding a comment to issue #\(issueID) that pull request #\(pullRequestID) has been released")
185 | dispatchGroup.enter()
186 | Commenter.post(.releasedIssue(release: release, pullRequestID: pullRequestID), on: issueID, at: project) {
187 | dispatchGroup.leave()
188 | }
189 | }
190 | }
191 | dispatchGroup.wait()
192 | }
193 |
194 | /// Appends the changelog to the changelog file if the argument is set.
195 | /// - Parameters:
196 | /// - changelog: The changelog to append to the changelog file.
197 | /// - tagName: The name of the tag that is used as the title for the newly added section.
198 | private func updateChangelogFile(adding changelog: String, for tagName: String) throws {
199 | guard let changelogURL = changelogURL else {
200 | Log.debug("Skipping changelog file updating")
201 | return
202 | }
203 |
204 | let currentContent = try String(contentsOfFile: changelogURL.path)
205 | let newContent = """
206 | ### \(tagName)
207 | \(changelog)\n
208 | \(currentContent)
209 | """
210 |
211 | let handle = try FileHandle(forWritingTo: changelogURL)
212 | handle.write(Data(newContent.utf8))
213 | handle.closeFile()
214 | }
215 |
216 | private func createRelease(using project: GITProject, tagName: String, targetCommitish: String?, title: String?,
217 | body: String) throws -> Release
218 | {
219 | let group = DispatchGroup()
220 | group.enter()
221 |
222 | let releaseTitle = title ?? tagName
223 | Log.debug("""
224 | \nCreating a new release:
225 | owner: \(project.organisation)
226 | repo: \(project.repository)
227 | tagName: \(tagName)
228 | targetCommitish: \(targetCommitish ?? "default")
229 | prerelease: \(isPrerelease)
230 | draft: false
231 | title: \(releaseTitle)
232 | useGitHubReleaseNotes: \(useGitHubReleaseNotes)
233 | body:
234 | \(body)\n
235 | """)
236 |
237 | var result: Result!
238 | octoKit.postRelease(
239 | urlSession,
240 | owner: project.organisation,
241 | repository: project.repository,
242 | tagName: tagName,
243 | targetCommitish: targetCommitish,
244 | name: releaseTitle,
245 | body: body,
246 | prerelease: isPrerelease,
247 | draft: false,
248 | /// Since GitHub's API does not support setting `previous_tag_name`, we manually call the API to generate automated GH changelogs.
249 | generateReleaseNotes: false
250 | ) { response in
251 | switch response {
252 | case .success(let release):
253 | result = .success(release.htmlURL)
254 | case .failure(let error):
255 | result = .failure(OctoKitError(error: error))
256 | }
257 | group.leave()
258 | }
259 | group.wait()
260 | let releaseURL = try result.get()
261 | return Release(tagName: tagName, url: releaseURL, title: releaseTitle, changelog: body)
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/Sources/GitBuddyCore/Tag Deletions/TagsDeleter.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import OctoKit
3 |
4 | final class TagsDeleter: URLSessionInjectable, ShellInjectable {
5 | private lazy var octoKit: Octokit = .init()
6 | let upUntilTagName: String?
7 | let limit: Int
8 | let prereleaseOnly: Bool
9 | let dryRun: Bool
10 |
11 | init(upUntilTagName: String? = nil, limit: Int, prereleaseOnly: Bool, dryRun: Bool) throws {
12 | try Octokit.authenticate()
13 |
14 | self.upUntilTagName = upUntilTagName
15 | self.limit = limit
16 | self.prereleaseOnly = prereleaseOnly
17 | self.dryRun = dryRun
18 | }
19 |
20 | @discardableResult public func run() throws -> [String] {
21 | let upUntilTag = try upUntilTagName.map { try Tag(name: $0) } ?? Tag.latest()
22 | Log.debug("Deleting up to \(limit) tags before \(upUntilTag.name) (Dry run: \(dryRun.description))")
23 |
24 | let currentProject = GITProject.current()
25 | let releases = try fetchReleases(project: currentProject, upUntil: upUntilTag.created)
26 |
27 | guard !releases.isEmpty else {
28 | return []
29 | }
30 | try deleteReleases(releases, project: currentProject)
31 | try deleteTags(releases, project: currentProject)
32 |
33 | return releases.map { $0.tagName }
34 | }
35 |
36 | private func fetchReleases(project: GITProject, upUntil: Date) throws -> [OctoKit.Release] {
37 | let group = DispatchGroup()
38 | group.enter()
39 |
40 | var result: Result<[OctoKit.Release], Swift.Error>!
41 | octoKit.listReleases(urlSession, owner: project.organisation, repository: project.repository, perPage: 100) { response in
42 | result = response
43 | group.leave()
44 | }
45 | group.wait()
46 |
47 | let releases = try result.get()
48 | Log.debug("Fetched releases: \(releases.map { $0.tagName }.joined(separator: ", "))")
49 |
50 | return releases
51 | .filter { release in
52 | guard !prereleaseOnly || release.prerelease else {
53 | return false
54 | }
55 | return release.createdAt < upUntil
56 | }
57 | .suffix(limit)
58 | }
59 |
60 | private func deleteReleases(_ releases: [OctoKit.Release], project: GITProject) throws {
61 | let releasesToDelete = releases.map { $0.tagName }
62 | Log.debug("Deleting tags: \(releasesToDelete.joined(separator: ", "))")
63 |
64 | let group = DispatchGroup()
65 | var lastError: Error?
66 | for release in releases {
67 | group.enter()
68 | Log.debug("Deleting release \(release.tagName) with id \(release.id) url: \(release.htmlURL)")
69 | guard !dryRun else {
70 | group.leave()
71 | return
72 | }
73 |
74 | octoKit.deleteRelease(urlSession, owner: project.organisation, repository: project.repository, releaseId: release.id) { error in
75 | defer { group.leave() }
76 | guard let error = error else {
77 | Log.debug("Successfully deleted release \(release.tagName)")
78 | return
79 | }
80 | Log.debug("Deletion of release \(release.tagName) failed: \(error)")
81 | lastError = error
82 | }
83 | }
84 | group.wait()
85 |
86 | if let lastError = lastError {
87 | throw lastError
88 | }
89 | }
90 |
91 | private func deleteTags(_ releases: [OctoKit.Release], project: GITProject) throws {
92 | let tagsToDelete = releases.map { $0.tagName }
93 | Log.debug("Deleting tags: \(tagsToDelete.joined(separator: ", "))")
94 |
95 | let group = DispatchGroup()
96 | var lastError: Error?
97 | for release in releases {
98 | group.enter()
99 | Log.debug("Deleting tag \(release.tagName) with id \(release.id) url: \(release.htmlURL)")
100 | guard !dryRun else {
101 | group.leave()
102 | return
103 | }
104 |
105 | octoKit.deleteReference(
106 | urlSession,
107 | owner: project.organisation,
108 | repository: project.repository,
109 | ref: "tags/\(release.tagName)"
110 | ) { error in
111 | defer { group.leave() }
112 | guard let error = error else {
113 | Log.debug("Successfully deleted tag \(release.tagName)")
114 | return
115 | }
116 | Log.debug("Deletion of tag \(release.tagName) failed: \(error)")
117 | lastError = error
118 | }
119 | }
120 | group.wait()
121 | if let lastError = lastError {
122 | throw lastError
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/Changelog/ChangelogCommandTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangelogCommandTests.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 09/04/2020.
6 | //
7 |
8 | @testable import GitBuddyCore
9 | import Mocker
10 | import OctoKit
11 | import XCTest
12 |
13 | final class ChangelogCommandTests: XCTestCase {
14 | override func setUp() {
15 | super.setUp()
16 | Octokit.protocolClasses = [MockingURLProtocol.self]
17 | mockGITAuthentication()
18 | ShellInjector.shell = MockedShell.self
19 | MockedShell.mockRelease(tag: "1.0.0")
20 | MockedShell.mockGITProject()
21 | }
22 |
23 | override func tearDown() {
24 | super.tearDown()
25 | MockedShell.commandMocks.removeAll()
26 | Mocker.removeAll()
27 | }
28 |
29 | /// It should use the GitHub Access Token for setting up URLSession.
30 | func testAccessTokenConfiguration() throws {
31 | Mocker.mockPullRequests()
32 | Mocker.mockForIssueNumber(39)
33 | MockedShell.mockGITProject(organisation: "WeTransfer", repository: "Diagnostics")
34 |
35 | let token = "username:79B02BE4-38D1-4E3D-9B41-4E0739761512"
36 | mockGITAuthentication(token)
37 | try executeCommand("gitbuddy changelog")
38 | XCTAssertEqual(
39 | URLSessionInjector.urlSession.configuration.httpAdditionalHeaders?["Authorization"] as? String,
40 | "Basic dXNlcm5hbWU6NzlCMDJCRTQtMzhEMS00RTNELTlCNDEtNEUwNzM5NzYxNTEy"
41 | )
42 | }
43 |
44 | /// It should correctly output the changelog.
45 | func testChangelogOutput() throws {
46 | Mocker.mockPullRequests()
47 | Mocker.mockForIssueNumber(39)
48 | MockedShell.mockGITProject(organisation: "WeTransfer", repository: "Diagnostics")
49 |
50 | let expectedChangelog = """
51 | - Add charset utf-8 to html head \
52 | ([#50](https://github.com/WeTransfer/Diagnostics/pull/50)) via [@AvdLee](https://github.com/AvdLee)
53 | - Get warning for file \'style.css\' after building \
54 | ([#39](https://github.com/WeTransfer/Diagnostics/issues/39)) via [@AvdLee](https://github.com/AvdLee)
55 | """
56 |
57 | try AssertExecuteCommand("gitbuddy changelog", expected: expectedChangelog)
58 | }
59 |
60 | /// It should correctly output the changelog.
61 | func testSectionedChangelogOutput() throws {
62 | Mocker.mockPullRequests()
63 | Mocker.mockIssues()
64 | Mocker.mockForIssueNumber(39)
65 | MockedShell.mockGITProject(organisation: "WeTransfer", repository: "Diagnostics")
66 |
67 | let expectedChangelog = """
68 | **Closed issues:**
69 |
70 | - Include device product names ([#60](https://github.com/WeTransfer/Diagnostics/issues/60))
71 | - Change the order of reported sessions ([#54](https://github.com/WeTransfer/Diagnostics/issues/54))
72 | - Encode Logging for HTML so object descriptions are visible ([#51](https://github.com/WeTransfer/Diagnostics/issues/51))
73 | - Chinese characters display incorrectly in HTML output in Safari ([#48](https://github.com/WeTransfer/Diagnostics/issues/48))
74 | - Get warning for file 'style.css' after building ([#39](https://github.com/WeTransfer/Diagnostics/issues/39))
75 | - Crash happening when there is no space left on the device ([#37](https://github.com/WeTransfer/Diagnostics/issues/37))
76 | - Add support for users without the Apple Mail app ([#36](https://github.com/WeTransfer/Diagnostics/issues/36))
77 | - Support for Apple Watch App Logs ([#33](https://github.com/WeTransfer/Diagnostics/issues/33))
78 | - Support different platforms/APIs ([#30](https://github.com/WeTransfer/Diagnostics/issues/30))
79 | - Strongly typed HTML would be nice ([#6](https://github.com/WeTransfer/Diagnostics/issues/6))
80 |
81 | **Merged pull requests:**
82 |
83 | - Add charset utf-8 to html head ([#50](https://github.com/WeTransfer/Diagnostics/pull/50)) via [@AvdLee](https://github.com/AvdLee)
84 | """
85 |
86 | try AssertExecuteCommand("gitbuddy changelog --sections", expected: expectedChangelog)
87 | }
88 |
89 | /// It should default to master branch.
90 | func testDefaultBranch() throws {
91 | let producer = try ChangelogProducer(baseBranch: nil)
92 | XCTAssertEqual(producer.baseBranch, "master")
93 | }
94 |
95 | /// It should accept a different branch as base argument.
96 | func testBaseBranchArgument() throws {
97 | let producer = try ChangelogProducer(baseBranch: "develop")
98 | XCTAssertEqual(producer.baseBranch, "develop")
99 | }
100 |
101 | /// It should use the latest tag by default for the latest release adding 60 seconds to its creation date.
102 | func testLatestReleaseUsingLatestTag() throws {
103 | let tag = "2.1.3"
104 | let date = Date().addingTimeInterval(TimeInterval.random(in: 0 ..< 100))
105 | MockedShell.mockRelease(tag: tag, date: date)
106 |
107 | let producer = try ChangelogProducer(baseBranch: nil)
108 | XCTAssertEqual(Int(producer.from.timeIntervalSince1970), Int(date.addingTimeInterval(60).timeIntervalSince1970))
109 | }
110 |
111 | /// It should use a tag passed as argument over the latest tag adding 60 seconds to its creation date..
112 | func testReleaseUsingTagArgument() throws {
113 | let expectedTag = "3.0.2"
114 | let date = Date().addingTimeInterval(TimeInterval.random(in: 0 ..< 100))
115 | MockedShell.mockRelease(tag: expectedTag, date: date)
116 |
117 | let producer = try ChangelogProducer(since: .tag(tag: expectedTag), baseBranch: nil)
118 | guard case ChangelogProducer.Since.tag(let tag) = producer.since else {
119 | XCTFail("Wrong since used")
120 | return
121 | }
122 | XCTAssertEqual(tag, expectedTag)
123 | XCTAssertEqual(Int(producer.from.timeIntervalSince1970), Int(date.addingTimeInterval(60).timeIntervalSince1970))
124 | }
125 |
126 | /// It should parse the current GIT project correctly.
127 | func testGITProjectParsing() throws {
128 | let organisation = "WeTransfer"
129 | let repository = "GitBuddy"
130 | MockedShell.mockGITProject(organisation: organisation, repository: repository)
131 |
132 | let producer = try ChangelogProducer(baseBranch: nil)
133 | XCTAssertEqual(producer.project.organisation, organisation)
134 | XCTAssertEqual(producer.project.repository, repository)
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/Changelog/ChangelogItemsFactoryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangelogItemsFactoryTests.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import GitBuddyCore
10 | import Mocker
11 | @testable import OctoKit
12 | import XCTest
13 |
14 | final class ChangelogItemsFactoryTests: XCTestCase {
15 | private let octoKit: Octokit = .init()
16 | private var urlSession: URLSession!
17 | private let project = GITProject(organisation: "WeTransfer", repository: "Diagnostics")
18 |
19 | override func setUp() {
20 | super.setUp()
21 | let configuration = URLSessionConfiguration.default
22 | configuration.protocolClasses = [MockingURLProtocol.self]
23 | urlSession = URLSession(configuration: configuration)
24 | ShellInjector.shell = MockedShell.self
25 | }
26 |
27 | override func tearDown() {
28 | super.tearDown()
29 | urlSession = nil
30 | }
31 |
32 | /// It should return the pull request only if no referencing issues are found.
33 | func testCreatingItems() {
34 | let pullRequest = Data(PullRequestsJSON.utf8).mapJSON(to: [PullRequest].self).first!
35 | let factory = ChangelogItemsFactory(octoKit: octoKit, pullRequests: [pullRequest], project: project)
36 | let items = factory.items(using: urlSession)
37 | XCTAssertEqual(items.count, 1)
38 | XCTAssertEqual(items.first?.input.title, pullRequest.title)
39 | XCTAssertEqual(items.first?.closedBy.title, pullRequest.title)
40 | }
41 |
42 | /// It should return the referencing issue with the pull request.
43 | func testReferencingIssue() {
44 | let pullRequest = Data(PullRequestsJSON.utf8).mapJSON(to: [PullRequest].self).last!
45 | let issue = Data(IssueJSON.utf8).mapJSON(to: Issue.self)
46 | let factory = ChangelogItemsFactory(octoKit: octoKit, pullRequests: [pullRequest], project: project)
47 | Mocker.mockForIssueNumber(39)
48 | let items = factory.items(using: urlSession)
49 | XCTAssertEqual(items.count, 1)
50 | XCTAssertEqual(items.first?.input.title, issue.title)
51 | XCTAssertEqual(items.first?.closedBy.title, pullRequest.title)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/GitBuddyCommandTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GitBuddyCommandTests.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 04/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import GitBuddyCore
10 | import XCTest
11 |
12 | final class GitBuddyCommandTests: XCTestCase {
13 | /// It should throw an error if the GitHub access token was not set.
14 | func testMissingAccessToken() {
15 | do {
16 | try executeCommand("gitbuddy changelog")
17 | } catch {
18 | XCTAssertEqual(error as? Token.Error, .missingAccessToken)
19 | }
20 | }
21 |
22 | /// It should throw an error if the GitHub access token was invalid.
23 | func testInvalidAccessToken() {
24 | do {
25 | mockGITAuthentication(UUID().uuidString)
26 | try executeCommand("gitbuddy changelog")
27 | } catch {
28 | XCTAssertEqual(error as? Token.Error, .invalidAccessToken)
29 | }
30 | }
31 |
32 | /// It should only print partly the access token.
33 | func testDebugPrintAccessToken() throws {
34 | let token = try Token(environment: ["GITBUDDY_ACCESS_TOKEN": "username:79B02BE4-38D1-4E3D-9B41-4E0739761512"])
35 | XCTAssertEqual(token.description, "username:79B02...")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/GitHub/CommenterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommenterTests.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 09/04/2020.
6 | //
7 |
8 | @testable import GitBuddyCore
9 | import Mocker
10 | import XCTest
11 |
12 | final class CommenterTests: XCTestCase {
13 | override func setUp() {
14 | super.setUp()
15 | let configuration = URLSessionConfiguration.default
16 | configuration.protocolClasses = [MockingURLProtocol.self]
17 | URLSessionInjector.urlSession = URLSession(configuration: configuration)
18 | ShellInjector.shell = MockedShell.self
19 | }
20 |
21 | override func tearDown() {
22 | super.tearDown()
23 | URLSessionInjector.urlSession = URLSession.shared
24 | }
25 |
26 | /// It should add a watermark to the comments.
27 | func testWatermark() throws {
28 | Mocker.mockPullRequests()
29 | let latestTag = try Tag.latest()
30 | let release = Release(
31 | tagName: latestTag.name,
32 | url: URL(string: "https://www.fakegithub.com")!,
33 | title: "Release title",
34 | changelog: ""
35 | )
36 | let project = GITProject(organisation: "WeTransfer", repository: "GitBuddy")
37 |
38 | let mockExpectation = expectation(description: "Mock should be called")
39 | var mock = Mock(
40 | url: URL(string: "https://api.github.com/repos/WeTransfer/GitBuddy/issues/1/comments")!,
41 | dataType: .json,
42 | statusCode: 200,
43 | data: [.post: Data()]
44 | )
45 | mock.onRequest = { _, postBodyArguments in
46 | let body = postBodyArguments?["body"] as? String
47 | XCTAssertTrue(body?.contains(Commenter.watermark) == true)
48 | mockExpectation.fulfill()
49 | }
50 | mock.register()
51 |
52 | let commentExpectation = expectation(description: "Comment should post")
53 | Commenter.post(.releasedPR(release: release), on: 1, at: project) {
54 | commentExpectation.fulfill()
55 | }
56 | wait(for: [mockExpectation, commentExpectation], timeout: 10.0, enforceOrder: true)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/GitHub/IssueResolverTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueResolverTests.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import GitBuddyCore
10 | import Mocker
11 | import OctoKit
12 | import XCTest
13 |
14 | final class IssueResolverTests: XCTestCase {
15 | private let octoKit: Octokit = .init()
16 | private var urlSession: URLSession!
17 |
18 | override func setUp() {
19 | super.setUp()
20 | let configuration = URLSessionConfiguration.default
21 | configuration.protocolClasses = [MockingURLProtocol.self]
22 | urlSession = URLSession(configuration: configuration)
23 | ShellInjector.shell = MockedShell.self
24 | }
25 |
26 | override func tearDown() {
27 | super.tearDown()
28 | urlSession = nil
29 | }
30 |
31 | /// It should return the issue fetched from GitHub.
32 | func testFetchingIssue() {
33 | let project = GITProject(organisation: "WeTransfer", repository: "Diagnostics")
34 | let input = MockChangelogInput(body: "Fixes #39")
35 | let resolver = IssuesResolver(octoKit: octoKit, project: project, input: input)
36 | Mocker.mockForIssueNumber(39)
37 | let issues = resolver.resolve(using: urlSession)
38 |
39 | XCTAssertEqual(issues?.count, 1)
40 | XCTAssertEqual(issues?[0].title, "Get warning for file 'style.css' after building")
41 | }
42 |
43 | /// It should return no issues if there's no issue referenced.
44 | func testNoReferencedIssue() {
45 | let project = GITProject(organisation: "WeTransfer", repository: "Diagnostics")
46 | let input = MockChangelogInput(body: "Pull Request description text")
47 | let resolver = IssuesResolver(octoKit: octoKit, project: project, input: input)
48 | XCTAssertNil(resolver.resolve(using: urlSession))
49 | }
50 |
51 | /// It should extract the fixed issue from the Pull Request body.
52 | func testResolvingReferencedIssue() {
53 | let issueClosingKeywords = [
54 | "close",
55 | "Closes",
56 | "closed",
57 | "fix",
58 | "fixes",
59 | "Fixed",
60 | "resolve",
61 | "Resolves",
62 | "resolved"
63 | ]
64 |
65 | let issueNumber = 4343
66 |
67 | issueClosingKeywords.forEach { closingKeyword in
68 | let description = examplePullRequestDescriptionUsing(closingKeyword: closingKeyword, issueNumber: issueNumber)
69 | XCTAssertEqual(description.resolvingIssues(), [issueNumber])
70 | }
71 | }
72 |
73 | /// It should not extract anything if no issue number is found.
74 | func testResolvingNoReferencedIssue() {
75 | let description = examplePullRequestDescriptionUsing(closingKeyword: "fixes", issueNumber: nil)
76 | XCTAssertTrue(description.resolvingIssues().isEmpty)
77 | }
78 |
79 | /// It should not extract anything if no closing keyword is found.
80 | func testResolvingNoClosingKeyword() {
81 | let issueNumber = 4343
82 |
83 | let description = examplePullRequestDescriptionUsing(closingKeyword: "", issueNumber: issueNumber)
84 | XCTAssertTrue(description.resolvingIssues().isEmpty)
85 | }
86 |
87 | /// It should extract mulitple issues.
88 | func testResolvingMultipleIssues() {
89 | let description = "This is a beautiful PR that close #123 for real. It also fixes #1 and fixes #2"
90 | let resolvedIssues = description.resolvingIssues()
91 | XCTAssertEqual(resolvedIssues.count, 3)
92 | XCTAssertEqual(Set(description.resolvingIssues()), Set([123, 1, 2]))
93 | }
94 |
95 | /// It should deduplicate if the same issue is closed multiple times.
96 | func testResolvingMultipleIssuesDedup() {
97 | let description = "This is a beautiful PR that close #123 for real. It also fixes #123"
98 | XCTAssertEqual(description.resolvingIssues(), [123])
99 | }
100 |
101 | /// It should not extract anything if there is no number after the #.
102 | func testResolvingNoNumber() {
103 | let description = "This is a beautiful PR that close # for real."
104 | XCTAssertTrue(description.resolvingIssues().isEmpty)
105 | }
106 |
107 | /// It should not extract anything if there is no number after the #, and it's at the end.
108 | func testResolvingNoNumberLast() {
109 | let description = "This is a beautiful PR that close #"
110 | XCTAssertTrue(description.resolvingIssues().isEmpty)
111 | }
112 |
113 | /// It should extract the issue if it's first.
114 | func testResolvingIssueFirst() {
115 | let description = "Resolves #123. Yay!"
116 | XCTAssertEqual(description.resolvingIssues(), [123])
117 | }
118 |
119 | /// It should extract the issue if it's the only thing present.
120 | func testResolvingIssueOnly() {
121 | let description = "Resolved #123"
122 | XCTAssertEqual(description.resolvingIssues(), [123])
123 | }
124 | }
125 |
126 | extension IssueResolverTests {
127 | func examplePullRequestDescriptionUsing(closingKeyword: String, issueNumber: Int?) -> String {
128 | let issueNumberString = issueNumber?.description ?? ""
129 |
130 | return """
131 | This PR does a lot of awesome stuff.
132 | It even closes some issues!
133 | Not #3737 though. This one is too hard.
134 |
135 | \(closingKeyword) #\(issueNumberString)
136 | """
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/GitHub/PullRequestFetcherTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PullRequestFetcherTests.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import GitBuddyCore
10 | import Mocker
11 | import OctoKit
12 | import XCTest
13 |
14 | final class PullRequestFetcherTests: XCTestCase {
15 | private let octoKit: Octokit = .init()
16 | private var urlSession: URLSession!
17 |
18 | override func setUp() {
19 | super.setUp()
20 | let configuration = URLSessionConfiguration.default
21 | configuration.protocolClasses = [MockingURLProtocol.self]
22 | urlSession = URLSession(configuration: configuration)
23 | ShellInjector.shell = MockedShell.self
24 | }
25 |
26 | override func tearDown() {
27 | super.tearDown()
28 | urlSession = nil
29 | }
30 |
31 | func testFetchingPullRequest() throws {
32 | Mocker.mockPullRequests()
33 | let release = try Tag.latest()
34 | let project = GITProject(organisation: "WeTransfer", repository: "Diagnostics")
35 | let fetcher = PullRequestFetcher(octoKit: octoKit, baseBranch: "master", project: project)
36 | let pullRequests = try fetcher.fetchAllBetween(release.created, and: Date(), using: urlSession)
37 | XCTAssertEqual(pullRequests.count, 2)
38 | XCTAssertEqual(pullRequests[0].title, "Add charset utf-8 to html head 😃")
39 | XCTAssertEqual(pullRequests[1].title, "Fix warning occurring in pod library because of importing style.css #trivial")
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/Models/ChangelogItemTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChangelogItemTests.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import GitBuddyCore
10 | import OctoKit
11 | import XCTest
12 |
13 | final class ChangelogItemTests: XCTestCase {
14 | /// It should return `nil` if there's no title.
15 | func testNilTitle() {
16 | let item = ChangelogItem(input: MockChangelogInput(), closedBy: MockedPullRequest())
17 | XCTAssertNil(item.title)
18 | }
19 |
20 | /// It should return `title` as is.
21 | func testFirstUpperCaseTitle() {
22 | let item = ChangelogItem(input: MockChangelogInput(title: "Update README.md"), closedBy: MockedPullRequest())
23 | XCTAssertEqual(item.title, "Update README.md")
24 | }
25 |
26 | /// It should capitalize first word in `title`
27 | func testFirstLowerCaseTitle() {
28 | let item = ChangelogItem(input: MockChangelogInput(title: "ignoring query example"), closedBy: MockedPullRequest())
29 | XCTAssertEqual(item.title, "Ignoring query example")
30 | }
31 |
32 | /// It should correctly display the number and URL.
33 | func testNumberURL() {
34 | let input = MockChangelogInput(number: 1, title: UUID().uuidString, htmlURL: URL(string: "https://www.fakeurl.com")!)
35 | let item = ChangelogItem(input: input, closedBy: MockedPullRequest())
36 | XCTAssertEqual(item.title, "\(input.title!) ([#1](https://www.fakeurl.com))")
37 | }
38 |
39 | /// It should show the user if possible.
40 | func testUser() {
41 | let input = Data(PullRequestsJSON.utf8).mapJSON(to: [PullRequest].self).first!
42 | input.htmlURL = nil
43 | let item = ChangelogItem(input: input, closedBy: input)
44 | XCTAssertEqual(item.title, "Add charset utf-8 to html head via [@AvdLee](https://github.com/AvdLee)")
45 | }
46 |
47 | /// It should fallback to the assignee if the user is nil for Pull Requests.
48 | func testAssigneeFallback() {
49 | let input = Data(PullRequestsJSON.utf8).mapJSON(to: [PullRequest].self).first!
50 | input.user = nil
51 | input.htmlURL = nil
52 | let item = ChangelogItem(input: input, closedBy: input)
53 | XCTAssertEqual(
54 | item.title,
55 | "Add charset utf-8 to html head via [@kairadiagne](https://github.com/kairadiagne)"
56 | )
57 | }
58 |
59 | /// It should combine the title, number and user.
60 | func testTitleNumberUser() {
61 | let input = MockChangelogInput(number: 1, title: UUID().uuidString, htmlURL: URL(string: "https://www.fakeurl.com")!)
62 | let closedBy = MockedPullRequest(username: "Henk")
63 | let item = ChangelogItem(input: input, closedBy: closedBy)
64 | XCTAssertEqual(
65 | item.title,
66 | "\(input.title!) ([#1](https://www.fakeurl.com)) via [@Henk](https://github.com/Henk)"
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/Models/SingleSectionChangelogTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SingleSectionChangelogTests.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 05/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | @testable import GitBuddyCore
11 | import XCTest
12 |
13 | final class SingleSectionChangelogTests: XCTestCase {
14 | /// It should report a single issue from a pull request correctly.
15 | func testPullRequestSingleIssue() {
16 | let pullRequest = MockedPullRequest(number: 0)
17 | let issue = MockedIssue(number: 0)
18 | let changelogItem = ChangelogItem(input: issue, closedBy: pullRequest)
19 | let changelog = SingleSectionChangelog(items: [changelogItem])
20 |
21 | XCTAssertEqual(changelog.itemIdentifiers, [0: [0]])
22 | }
23 |
24 | /// It should report multiple issues from a single pull request correctly.
25 | func testPullRequestMultipleIssues() {
26 | let pullRequest = MockedPullRequest(number: 0)
27 | let changelogItems = [
28 | ChangelogItem(input: MockedIssue(number: 0), closedBy: pullRequest),
29 | ChangelogItem(input: MockedIssue(number: 1), closedBy: pullRequest)
30 | ]
31 | let changelog = SingleSectionChangelog(items: changelogItems)
32 |
33 | XCTAssertEqual(changelog.itemIdentifiers, [0: [0, 1]])
34 | }
35 |
36 | /// It should report the pull request even though there's no linked issues.
37 | func testPullRequestNoIssues() {
38 | let pullRequest = MockedPullRequest(number: 0)
39 | let changelogItem = ChangelogItem(input: pullRequest, closedBy: pullRequest)
40 | let changelog = SingleSectionChangelog(items: [changelogItem])
41 |
42 | XCTAssertEqual(changelog.itemIdentifiers, [0: []])
43 | }
44 |
45 | /// It should create a changelog from its inputs.
46 | func testDescription() {
47 | let pullRequest = MockedPullRequest(number: 0)
48 | let issue = MockedIssue(title: "Fixed something")
49 | let changelogItem = ChangelogItem(input: issue, closedBy: pullRequest)
50 | let changelog = SingleSectionChangelog(items: [changelogItem])
51 |
52 | XCTAssertEqual(changelog.description, "- Fixed something")
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/Release/ReleaseProducerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReleaseProducerTests.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 05/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import GitBuddyCore
10 | import Mocker
11 | import OctoKit
12 | import XCTest
13 |
14 | final class ReleaseProducerTests: XCTestCase {
15 | override func setUp() {
16 | super.setUp()
17 | Octokit.protocolClasses = [MockingURLProtocol.self]
18 | mockGITAuthentication()
19 | ShellInjector.shell = MockedShell.self
20 | Mocker.mockPullRequests()
21 | Mocker.mockIssues()
22 | Mocker.mockForIssueNumber(39)
23 | Mocker.mockRelease()
24 | MockedShell.mockRelease(tag: "1.0.1")
25 | MockedShell.mock(.previousTag, value: "1.0.0")
26 | MockedShell.mockGITProject(organisation: "WeTransfer", repository: "Diagnostics")
27 | }
28 |
29 | override func tearDown() {
30 | super.tearDown()
31 | MockedShell.commandMocks.removeAll()
32 | Mocker.removeAll()
33 | }
34 |
35 | /// It should use the GitHub Access Token for setting up URLSession.
36 | func testAccessTokenConfiguration() throws {
37 | let token = "username:79B02BE4-38D1-4E3D-9B41-4E0739761512"
38 | mockGITAuthentication(token)
39 | try executeCommand("gitbuddy release -s")
40 | XCTAssertEqual(
41 | URLSessionInjector.urlSession.configuration.httpAdditionalHeaders?["Authorization"] as? String,
42 | "Basic dXNlcm5hbWU6NzlCMDJCRTQtMzhEMS00RTNELTlCNDEtNEUwNzM5NzYxNTEy"
43 | )
44 | }
45 |
46 | /// It should correctly output the release URL.
47 | func testReleaseOutputURL() throws {
48 | try AssertExecuteCommand("gitbuddy release -s", expected: "https://github.com/WeTransfer/ChangelogProducer/releases/tag/1.0.1")
49 | }
50 |
51 | func testReleaseOutputJSON() throws {
52 | let output = try executeCommand("gitbuddy release -s --json")
53 | // swiftlint:disable:next line_length
54 | XCTAssertTrue(output.contains("{\"title\":\"1.0.1\",\"tagName\":\"1.0.1\",\"url\":\"https:\\/\\/github.com\\/WeTransfer\\/ChangelogProducer\\/releases\\/tag\\/1.0.1\""))
55 | }
56 |
57 | /// It should set the parameters correctly.
58 | func testPostBodyArguments() throws {
59 | let mockExpectation = expectation(description: "Mocks should be called")
60 | var mock = Mocker.mockRelease()
61 | mock.onRequest = { _, parameters in
62 | guard let parameters = try? XCTUnwrap(parameters) else { return }
63 | XCTAssertEqual(parameters["prerelease"] as? Bool, false)
64 | XCTAssertEqual(parameters["draft"] as? Bool, false)
65 | XCTAssertEqual(parameters["tag_name"] as? String, "1.0.1")
66 | XCTAssertEqual(parameters["name"] as? String, "1.0.1")
67 | XCTAssertEqual(parameters["body"] as? String, """
68 | - Add charset utf-8 to html head \
69 | ([#50](https://github.com/WeTransfer/Diagnostics/pull/50)) via [@AvdLee](https://github.com/AvdLee)
70 | - Get warning for file 'style.css' after building \
71 | ([#39](https://github.com/WeTransfer/Diagnostics/issues/39)) via [@AvdLee](https://github.com/AvdLee)
72 | """)
73 | mockExpectation.fulfill()
74 | }
75 | mock.register()
76 |
77 | try executeCommand("gitbuddy release -s")
78 | wait(for: [mockExpectation], timeout: 0.3)
79 | }
80 |
81 | /// It should set the parameters correctly.
82 | func testPostBodyArgumentsGitHubReleaseNotes() throws {
83 | let mockExpectation = expectation(description: "Mocks should be called")
84 | Mocker.mockReleaseNotes()
85 | var mock = Mocker.mockRelease()
86 | mock.onRequest = { _, parameters in
87 | guard let parameters = try? XCTUnwrap(parameters) else { return }
88 | XCTAssertEqual(parameters["prerelease"] as? Bool, false)
89 | XCTAssertEqual(parameters["draft"] as? Bool, false)
90 | XCTAssertEqual(parameters["tag_name"] as? String, "1.0.1")
91 | XCTAssertEqual(parameters["name"] as? String, "1.0.1")
92 | XCTAssertEqual(parameters["body"] as? String, """
93 | ##Changes in Release v1.0.0 ... ##Contributors @monalisa
94 | """)
95 | mockExpectation.fulfill()
96 | }
97 | mock.register()
98 |
99 | try executeCommand("gitbuddy release -s --use-github-release-notes -t develop")
100 | wait(for: [mockExpectation], timeout: 0.3)
101 | }
102 |
103 | /// It should update the changelog file if the argument is set.
104 | func testChangelogUpdating() throws {
105 | let existingChangelog = """
106 | ### 1.0.0
107 | - Initial release
108 | """
109 | let tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("Changelog.md")
110 | XCTAssertTrue(FileManager.default.createFile(atPath: tempFileURL.path, contents: Data(existingChangelog.utf8), attributes: nil))
111 | try executeCommand("gitbuddy release -s -c \(tempFileURL.path)")
112 | let updatedChangelogContents = try String(contentsOfFile: tempFileURL.path)
113 |
114 | XCTAssertEqual(updatedChangelogContents, """
115 | ### 1.0.1
116 | - Add charset utf-8 to html head \
117 | ([#50](https://github.com/WeTransfer/Diagnostics/pull/50)) via [@AvdLee](https://github.com/AvdLee)
118 | - Get warning for file \'style.css\' after building \
119 | ([#39](https://github.com/WeTransfer/Diagnostics/issues/39)) via [@AvdLee](https://github.com/AvdLee)
120 |
121 | \(existingChangelog)
122 | """)
123 | }
124 |
125 | func testSectionedChangelogUpdating() throws {
126 | let existingChangelog = """
127 | ### 1.0.0
128 | - Initial release
129 | """
130 | let tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("Changelog.md")
131 | XCTAssertTrue(FileManager.default.createFile(atPath: tempFileURL.path, contents: Data(existingChangelog.utf8), attributes: nil))
132 | try executeCommand("gitbuddy release --sections -s -c \(tempFileURL.path)")
133 |
134 | let updatedChangelogContents = try String(contentsOfFile: tempFileURL.path)
135 |
136 | XCTAssertEqual(updatedChangelogContents, """
137 | ### 1.0.1
138 | **Closed issues:**
139 |
140 | - Include device product names ([#60](https://github.com/WeTransfer/Diagnostics/issues/60))
141 | - Change the order of reported sessions ([#54](https://github.com/WeTransfer/Diagnostics/issues/54))
142 | - Encode Logging for HTML so object descriptions are visible ([#51](https://github.com/WeTransfer/Diagnostics/issues/51))
143 | - Chinese characters display incorrectly in HTML output in Safari ([#48](https://github.com/WeTransfer/Diagnostics/issues/48))
144 | - Get warning for file 'style.css' after building ([#39](https://github.com/WeTransfer/Diagnostics/issues/39))
145 | - Crash happening when there is no space left on the device ([#37](https://github.com/WeTransfer/Diagnostics/issues/37))
146 | - Add support for users without the Apple Mail app ([#36](https://github.com/WeTransfer/Diagnostics/issues/36))
147 | - Support for Apple Watch App Logs ([#33](https://github.com/WeTransfer/Diagnostics/issues/33))
148 | - Support different platforms/APIs ([#30](https://github.com/WeTransfer/Diagnostics/issues/30))
149 | - Strongly typed HTML would be nice ([#6](https://github.com/WeTransfer/Diagnostics/issues/6))
150 |
151 | **Merged pull requests:**
152 |
153 | - Add charset utf-8 to html head ([#50](https://github.com/WeTransfer/Diagnostics/pull/50)) via [@AvdLee](https://github.com/AvdLee)
154 |
155 | \(existingChangelog)
156 | """)
157 | }
158 |
159 | /// It should comment on related pull requests and issues.
160 | func testPullRequestIssueCommenting() throws {
161 | let mocksExpectations = expectation(description: "Mocks should be called")
162 | mocksExpectations.expectedFulfillmentCount = 3
163 | let mocks = [
164 | Mocker.mockForCommentingOn(issueNumber: 39),
165 | Mocker.mockForCommentingOn(issueNumber: 49),
166 | Mocker.mockForCommentingOn(issueNumber: 50)
167 | ]
168 | mocks.forEach { mock in
169 | var mock = mock
170 | mock.completion = {
171 | mocksExpectations.fulfill()
172 | }
173 | mock.register()
174 | }
175 | try executeCommand("gitbuddy release")
176 | wait(for: [mocksExpectations], timeout: 2.0)
177 | }
178 |
179 | /// It should not post comments if --skipComments is passed as an argument.
180 | func testSkippingComments() throws {
181 | let mockExpectation = expectation(description: "Mock should not be called")
182 | mockExpectation.isInverted = true
183 | var mock = Mocker.mockForCommentingOn(issueNumber: 39)
184 | mock.completion = {
185 | mockExpectation.fulfill()
186 | }
187 | mock.register()
188 |
189 | try executeCommand("gitbuddy release -s")
190 | wait(for: [mockExpectation], timeout: 0.3)
191 | }
192 |
193 | /// It should use the prerelease setting.
194 | func testPrerelease() throws {
195 | let mockExpectation = expectation(description: "Mocks should be called")
196 | var mock = Mocker.mockRelease()
197 | mock.onRequest = { _, parameters in
198 | guard let parameters = try? XCTUnwrap(parameters) else { return }
199 | XCTAssertTrue(parameters["prerelease"] as? Bool == true)
200 | mockExpectation.fulfill()
201 | }
202 | mock.register()
203 |
204 | try executeCommand("gitbuddy release -s -p")
205 | wait(for: [mockExpectation], timeout: 0.3)
206 | }
207 |
208 | /// It should use the target commitish setting.
209 | func testTargetCommitish() throws {
210 | let mockExpectation = expectation(description: "Mocks should be called")
211 | var mock = Mocker.mockRelease()
212 | mock.onRequest = { _, parameters in
213 | guard let parameters = try? XCTUnwrap(parameters) else { return }
214 | XCTAssertEqual(parameters["target_commitish"] as? String, "develop")
215 | mockExpectation.fulfill()
216 | }
217 | mock.register()
218 |
219 | try executeCommand("gitbuddy release -s -t develop")
220 | wait(for: [mockExpectation], timeout: 0.3)
221 | }
222 |
223 | /// It should use the tag name setting.
224 | func testTagName() throws {
225 | let mockExpectation = expectation(description: "Mocks should be called")
226 | let tagName = "3.0.0b1233"
227 | let changelogToTag = "3.0.0b1232"
228 | MockedShell.mockRelease(tag: changelogToTag)
229 | var mock = Mocker.mockRelease()
230 | mock.onRequest = { _, parameters in
231 | guard let parameters = try? XCTUnwrap(parameters) else { return }
232 | XCTAssertEqual(parameters["tag_name"] as? String, tagName)
233 | mockExpectation.fulfill()
234 | }
235 | mock.register()
236 |
237 | try executeCommand("gitbuddy release --changelogToTag \(changelogToTag) -s -n \(tagName)")
238 | wait(for: [mockExpectation], timeout: 0.3)
239 | }
240 |
241 | func testUsingTargetCommitishDate() throws {
242 | let existingChangelog = """
243 | ### 1.0.0
244 | - Initial release
245 | """
246 | let tagName = "2.0.0b1233"
247 | let changelogToTag = "2.0.0b1232"
248 | let commitish = "1e88145e2101dc7203af3095d6e"
249 | let dateFormatter = DateFormatter()
250 | dateFormatter.dateFormat = "yyyy-MM-dd"
251 | let date = dateFormatter.date(from: "2020-01-05")!
252 | MockedShell.mockCommitish(commitish, date: date)
253 | MockedShell.mockRelease(tag: changelogToTag, date: date)
254 |
255 | let tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("Changelog.md")
256 | XCTAssertTrue(FileManager.default.createFile(atPath: tempFileURL.path, contents: Data(existingChangelog.utf8), attributes: nil))
257 |
258 | try executeCommand("gitbuddy release --target-commitish \(commitish) -s -n \(tagName) -c \(tempFileURL.path)")
259 |
260 | let updatedChangelogContents = try String(contentsOfFile: tempFileURL.path)
261 |
262 | // Merged at: 2020-01-06 - Add charset utf-8 to html head
263 | // Closed at: 2020-01-03 - Get warning for file
264 | // Setting the tag date to 2020-01-05 should remove the Add charset
265 |
266 | XCTAssertEqual(updatedChangelogContents, """
267 | ### 2.0.0b1233
268 | - Get warning for file \'style.css\' after building \
269 | ([#39](https://github.com/WeTransfer/Diagnostics/issues/39)) via [@AvdLee](https://github.com/AvdLee)
270 |
271 | \(existingChangelog)
272 | """)
273 | }
274 |
275 | func testThrowsMissingTargetDateError() {
276 | XCTAssertThrowsError(try executeCommand("gitbuddy release -s -n 3.0.0"), "Missing target date error should be thrown") { error in
277 | XCTAssertEqual(error as? ReleaseProducer.Error, .changelogTargetDateMissing)
278 | }
279 | }
280 |
281 | /// It should not contain changes that are merged into the target branch after the creation date of the tag we're using.
282 | func testIncludedChangesForUsedTagName() throws {
283 | let existingChangelog = """
284 | ### 1.0.0
285 | - Initial release
286 | """
287 | let tagName = "2.0.0b1233"
288 | let changelogToTag = "2.0.0b1232"
289 |
290 | let dateFormatter = DateFormatter()
291 | dateFormatter.dateFormat = "yyyy-MM-dd"
292 | let date = dateFormatter.date(from: "2020-01-05")!
293 | MockedShell.mockRelease(tag: changelogToTag, date: date)
294 |
295 | let tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("Changelog.md")
296 | XCTAssertTrue(FileManager.default.createFile(atPath: tempFileURL.path, contents: Data(existingChangelog.utf8), attributes: nil))
297 |
298 | try executeCommand("gitbuddy release --changelogToTag \(changelogToTag) -s -n \(tagName) -c \(tempFileURL.path)")
299 |
300 | let updatedChangelogContents = try String(contentsOfFile: tempFileURL.path)
301 |
302 | // Merged at: 2020-01-06 - Add charset utf-8 to html head
303 | // Closed at: 2020-01-03 - Get warning for file
304 | // Setting the tag date to 2020-01-05 should remove the Add charset
305 |
306 | XCTAssertEqual(updatedChangelogContents, """
307 | ### 2.0.0b1233
308 | - Get warning for file \'style.css\' after building \
309 | ([#39](https://github.com/WeTransfer/Diagnostics/issues/39)) via [@AvdLee](https://github.com/AvdLee)
310 |
311 | \(existingChangelog)
312 | """)
313 | }
314 |
315 | /// It should use the release title setting.
316 | func testReleaseTitle() throws {
317 | let mockExpectation = expectation(description: "Mocks should be called")
318 | let title = UUID().uuidString
319 | var mock = Mocker.mockRelease()
320 | mock.onRequest = { _, parameters in
321 | guard let parameters = try? XCTUnwrap(parameters) else { return }
322 | XCTAssertEqual(parameters["name"] as? String, title)
323 | mockExpectation.fulfill()
324 | }
325 | mock.register()
326 |
327 | try executeCommand("gitbuddy release -s -r \(title)")
328 | wait(for: [mockExpectation], timeout: 0.3)
329 | }
330 |
331 | /// It should use the changelog settings.
332 | func testChangelogSettings() throws {
333 | let lastReleaseTag = "2.0.1"
334 | let dateFormatter = DateFormatter()
335 | dateFormatter.dateFormat = "yyyy-MM-dd"
336 | let date = dateFormatter.date(from: "2020-01-03")!
337 | MockedShell.mockRelease(tag: lastReleaseTag, date: date)
338 |
339 | let baseBranch = UUID().uuidString
340 | Mocker.mockPullRequests(baseBranch: baseBranch)
341 |
342 | try executeCommand("gitbuddy release -s -l \(lastReleaseTag) -b \(baseBranch)")
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/Tag Deletions/TagsDeleterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TagsDeleterTests.swift
3 | //
4 | //
5 | // Created by Antoine van der Lee on 25/01/2022.
6 | //
7 |
8 | @testable import GitBuddyCore
9 | import Mocker
10 | import OctoKit
11 | import XCTest
12 |
13 | final class TagsDeleterTests: XCTestCase {
14 | override func setUp() {
15 | super.setUp()
16 | Octokit.protocolClasses = [MockingURLProtocol.self]
17 | mockGITAuthentication()
18 | ShellInjector.shell = MockedShell.self
19 | Mocker.mockListReleases()
20 | MockedShell.mockGITProject(organisation: "WeTransfer", repository: "Diagnostics")
21 | }
22 |
23 | override func tearDown() {
24 | super.tearDown()
25 | MockedShell.commandMocks.removeAll()
26 | Mocker.removeAll()
27 | }
28 |
29 | func testDeletingUpUntilTagName() throws {
30 | let dateFormatter = DateFormatter()
31 | dateFormatter.dateFormat = "dd-MM-yyyy"
32 | let dateMatchingTagInMockedJSON = dateFormatter.date(from: "26-06-2013")!
33 | MockedShell.mockRelease(tag: "2.0.0", date: dateMatchingTagInMockedJSON)
34 |
35 | let expectedTags = ["v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3"]
36 | for (index, tagName) in expectedTags.enumerated() {
37 | Mocker.mockDeletingRelease(id: index)
38 | Mocker.mockDeletingReference(tagName: tagName)
39 | }
40 |
41 | let output = try executeCommand("gitbuddy tagDeletion -u 2.0.0 -l 100")
42 | XCTAssertEqual(output, "Deleted tags: [\"\(expectedTags.joined(separator: "\", \""))\"]")
43 | }
44 |
45 | func testDeletingLimit() throws {
46 | let dateFormatter = DateFormatter()
47 | dateFormatter.dateFormat = "dd-MM-yyyy"
48 | let dateMatchingTagInMockedJSON = dateFormatter.date(from: "26-06-2013")!
49 | MockedShell.mockRelease(tag: "2.0.0", date: dateMatchingTagInMockedJSON)
50 |
51 | Mocker.mockDeletingRelease(id: 1)
52 | Mocker.mockDeletingReference(tagName: "v1.0.3")
53 |
54 | let output = try executeCommand("gitbuddy tagDeletion -u 2.0.0 -l 1")
55 | XCTAssertEqual(output, "Deleted tags: [\"v1.0.3\"]")
56 | }
57 |
58 | func testDeletingPrereleaseOnly() throws {
59 | MockedShell.mockRelease(tag: "2.0.0", date: Date())
60 |
61 | Mocker.mockDeletingRelease(id: 1)
62 | Mocker.mockDeletingReference(tagName: "v1.0.3")
63 |
64 | let output = try executeCommand("gitbuddy tagDeletion -u 2.0.0 -l 1 --prerelease-only")
65 | XCTAssertEqual(output, "Deleted tags: [\"v1.0.3\"]")
66 | }
67 |
68 | func testDeletingUpUntilLastTag() throws {
69 | MockedShell.mockRelease(tag: "2.0.0", date: Date())
70 |
71 | MockedShell.mock(.previousTag, value: "2.0.0")
72 | Mocker.mockDeletingRelease(id: 1)
73 | Mocker.mockDeletingReference(tagName: "v1.0.3")
74 |
75 | let output = try executeCommand("gitbuddy tagDeletion -l 1 --prerelease-only")
76 | XCTAssertEqual(output, "Deleted tags: [\"v1.0.3\"]")
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/TestHelpers/CommentJSON.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommentJSON.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 05/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | let CommentJSON = """
12 |
13 | {
14 | "id": 1,
15 | "node_id": "MDEyOklzc3VlQ29tbWVudDE=",
16 | "url": "https://api.github.com/repos/octocat/Hello-World/issues/comments/1",
17 | "html_url": "https://github.com/octocat/Hello-World/issues/1347#issuecomment-1",
18 | "body": "Me too",
19 | "user": {
20 | "login": "octocat",
21 | "id": 1,
22 | "node_id": "MDQ6VXNlcjE=",
23 | "avatar_url": "https://github.com/images/error/octocat_happy.gif",
24 | "gravatar_id": "",
25 | "url": "https://api.github.com/users/octocat",
26 | "html_url": "https://github.com/octocat",
27 | "followers_url": "https://api.github.com/users/octocat/followers",
28 | "following_url": "https://api.github.com/users/octocat/following{/other_user}",
29 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
30 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
31 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
32 | "organizations_url": "https://api.github.com/users/octocat/orgs",
33 | "repos_url": "https://api.github.com/users/octocat/repos",
34 | "events_url": "https://api.github.com/users/octocat/events{/privacy}",
35 | "received_events_url": "https://api.github.com/users/octocat/received_events",
36 | "type": "User",
37 | "site_admin": false
38 | },
39 | "created_at": "2011-04-14T16:00:49Z",
40 | "updated_at": "2011-04-14T16:00:49Z"
41 | }
42 |
43 | """
44 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/TestHelpers/IssueJSON.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IssueJSON.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | // swiftlint:disable line_length
10 | let IssueJSON = """
11 |
12 | {
13 | "url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/39",
14 | "repository_url": "https://api.github.com/repos/WeTransfer/Diagnostics",
15 | "labels_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/39/labels{/name}",
16 | "comments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/39/comments",
17 | "events_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/39/events",
18 | "html_url": "https://github.com/WeTransfer/Diagnostics/issues/39",
19 | "id": 541453394,
20 | "node_id": "MDU6SXNzdWU1NDE0NTMzOTQ=",
21 | "number": 39,
22 | "title": "Get warning for file 'style.css' after building",
23 | "user": {
24 | "login": "davidsteppenbeck",
25 | "id": 59126213,
26 | "node_id": "MDQ6VXNlcjU5MTI2MjEz",
27 | "avatar_url": "https://avatars2.githubusercontent.com/u/59126213?v=4",
28 | "gravatar_id": "",
29 | "url": "https://api.github.com/users/davidsteppenbeck",
30 | "html_url": "https://github.com/davidsteppenbeck",
31 | "followers_url": "https://api.github.com/users/davidsteppenbeck/followers",
32 | "following_url": "https://api.github.com/users/davidsteppenbeck/following{/other_user}",
33 | "gists_url": "https://api.github.com/users/davidsteppenbeck/gists{/gist_id}",
34 | "starred_url": "https://api.github.com/users/davidsteppenbeck/starred{/owner}{/repo}",
35 | "subscriptions_url": "https://api.github.com/users/davidsteppenbeck/subscriptions",
36 | "organizations_url": "https://api.github.com/users/davidsteppenbeck/orgs",
37 | "repos_url": "https://api.github.com/users/davidsteppenbeck/repos",
38 | "events_url": "https://api.github.com/users/davidsteppenbeck/events{/privacy}",
39 | "received_events_url": "https://api.github.com/users/davidsteppenbeck/received_events",
40 | "type": "User",
41 | "site_admin": false
42 | },
43 | "labels": [
44 |
45 | ],
46 | "state": "closed",
47 | "locked": false,
48 | "assignee": null,
49 | "assignees": [
50 |
51 | ],
52 | "milestone": null,
53 | "comments": 2,
54 | "created_at": "2019-12-22T13:49:30Z",
55 | "updated_at": "2020-01-06T08:45:18Z",
56 | "closed_at": "2020-01-03T13:02:59Z",
57 | "author_association": "NONE",
58 | "body": "Details are in the image. I resolved it for the time being by removing file 'style.css' from the Xcode project. (Installed Diagnostics 1.2 using Cocoapods 1.8 and Xcode)",
59 | "closed_by": {
60 | "login": "AvdLee",
61 | "id": 4329185,
62 | "node_id": "MDQ6VXNlcjQzMjkxODU=",
63 | "avatar_url": "https://avatars2.githubusercontent.com/u/4329185?v=4",
64 | "gravatar_id": "",
65 | "url": "https://api.github.com/users/AvdLee",
66 | "html_url": "https://github.com/AvdLee",
67 | "followers_url": "https://api.github.com/users/AvdLee/followers",
68 | "following_url": "https://api.github.com/users/AvdLee/following{/other_user}",
69 | "gists_url": "https://api.github.com/users/AvdLee/gists{/gist_id}",
70 | "starred_url": "https://api.github.com/users/AvdLee/starred{/owner}{/repo}",
71 | "subscriptions_url": "https://api.github.com/users/AvdLee/subscriptions",
72 | "organizations_url": "https://api.github.com/users/AvdLee/orgs",
73 | "repos_url": "https://api.github.com/users/AvdLee/repos",
74 | "events_url": "https://api.github.com/users/AvdLee/events{/privacy}",
75 | "received_events_url": "https://api.github.com/users/AvdLee/received_events",
76 | "type": "User",
77 | "site_admin": false
78 | }
79 | }
80 |
81 | """
82 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/TestHelpers/ListReleasesJSON.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListReleasesJSON.swift
3 | //
4 | //
5 | // Created by Antoine van der Lee on 25/01/2022.
6 | //
7 |
8 | import Foundation
9 |
10 | let ListReleasesJSON = """
11 |
12 | [{
13 | "url": "https://api.github.com/repos/octocat/Hello-World/releases/1",
14 | "html_url": "https://github.com/octocat/Hello-World/releases/v1.0.0",
15 | "assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/1/assets",
16 | "upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}",
17 | "tarball_url": null,
18 | "zipball_url": null,
19 | "id": 1,
20 | "node_id": "MDc6UmVsZWFzZTE=",
21 | "tag_name": "v1.0.0",
22 | "target_commitish": "master",
23 | "name": "v1.0.0 Release",
24 | "body": "The changelog of this release",
25 | "draft": true,
26 | "prerelease": false,
27 | "created_at": "2013-02-27T19:35:32Z",
28 | "published_at": null,
29 | "author": {
30 | "login": "octocat",
31 | "id": 1,
32 | "node_id": "MDQ6VXNlcjE=",
33 | "avatar_url": "https://github.com/images/error/octocat_happy.gif",
34 | "gravatar_id": "",
35 | "url": "https://api.github.com/users/octocat",
36 | "html_url": "https://github.com/octocat",
37 | "followers_url": "https://api.github.com/users/octocat/followers",
38 | "following_url": "https://api.github.com/users/octocat/following{/other_user}",
39 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
40 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
41 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
42 | "organizations_url": "https://api.github.com/users/octocat/orgs",
43 | "repos_url": "https://api.github.com/users/octocat/repos",
44 | "events_url": "https://api.github.com/users/octocat/events{/privacy}",
45 | "received_events_url": "https://api.github.com/users/octocat/received_events",
46 | "type": "User",
47 | "site_admin": false
48 | },
49 | "assets": []
50 | },
51 | {
52 | "url": "https://api.github.com/repos/octocat/Hello-World/releases/2",
53 | "html_url": "https://github.com/octocat/Hello-World/releases/v1.0.1",
54 | "assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/2/assets",
55 | "upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/2/assets{?name,label}",
56 | "tarball_url": "https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.1",
57 | "zipball_url": "https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.1",
58 | "id": 1,
59 | "node_id": "MDc6UmVsZWFzZTE=",
60 | "tag_name": "v1.0.1",
61 | "target_commitish": "master",
62 | "name": "v1.0.1 Release",
63 | "body": "The changelog of this release",
64 | "draft": false,
65 | "prerelease": false,
66 | "created_at": "2013-03-27T19:35:32Z",
67 | "published_at": "2013-03-27T19:35:32Z",
68 | "author": {
69 | "login": "octocat",
70 | "id": 1,
71 | "node_id": "MDQ6VXNlcjE=",
72 | "avatar_url": "https://github.com/images/error/octocat_happy.gif",
73 | "gravatar_id": "",
74 | "url": "https://api.github.com/users/octocat",
75 | "html_url": "https://github.com/octocat",
76 | "followers_url": "https://api.github.com/users/octocat/followers",
77 | "following_url": "https://api.github.com/users/octocat/following{/other_user}",
78 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
79 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
80 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
81 | "organizations_url": "https://api.github.com/users/octocat/orgs",
82 | "repos_url": "https://api.github.com/users/octocat/repos",
83 | "events_url": "https://api.github.com/users/octocat/events{/privacy}",
84 | "received_events_url": "https://api.github.com/users/octocat/received_events",
85 | "type": "User",
86 | "site_admin": false
87 | },
88 | "assets": []
89 | },
90 | {
91 | "url": "https://api.github.com/repos/octocat/Hello-World/releases/3",
92 | "html_url": "https://github.com/octocat/Hello-World/releases/v1.0.2",
93 | "assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/3/assets",
94 | "upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/3/assets{?name,label}",
95 | "tarball_url": "https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.2",
96 | "zipball_url": "https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.2",
97 | "id": 1,
98 | "node_id": "MDc6UmVsZWFzZTE=",
99 | "tag_name": "v1.0.2",
100 | "target_commitish": "master",
101 | "name": "v1.0.2 Release",
102 | "body": "The changelog of this release",
103 | "draft": false,
104 | "prerelease": true,
105 | "created_at": "2013-04-27T19:35:32Z",
106 | "published_at": "2013-04-27T19:35:32Z",
107 | "author": {
108 | "login": "octocat",
109 | "id": 1,
110 | "node_id": "MDQ6VXNlcjE=",
111 | "avatar_url": "https://github.com/images/error/octocat_happy.gif",
112 | "gravatar_id": "",
113 | "url": "https://api.github.com/users/octocat",
114 | "html_url": "https://github.com/octocat",
115 | "followers_url": "https://api.github.com/users/octocat/followers",
116 | "following_url": "https://api.github.com/users/octocat/following{/other_user}",
117 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
118 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
119 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
120 | "organizations_url": "https://api.github.com/users/octocat/orgs",
121 | "repos_url": "https://api.github.com/users/octocat/repos",
122 | "events_url": "https://api.github.com/users/octocat/events{/privacy}",
123 | "received_events_url": "https://api.github.com/users/octocat/received_events",
124 | "type": "User",
125 | "site_admin": false
126 | },
127 | "assets": []
128 | },
129 | {
130 | "url": "https://api.github.com/repos/octocat/Hello-World/releases/4",
131 | "html_url": "https://github.com/octocat/Hello-World/releases/v1.0.3",
132 | "assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/4/assets",
133 | "upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/4/assets{?name,label}",
134 | "tarball_url": "https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.3",
135 | "zipball_url": "https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.3",
136 | "id": 1,
137 | "node_id": "MDc6UmVsZWFzZTE=",
138 | "tag_name": "v1.0.3",
139 | "target_commitish": "master",
140 | "name": "v1.0.3 Release",
141 | "body": "The changelog of this release",
142 | "draft": false,
143 | "prerelease": true,
144 | "created_at": "2013-05-27T19:35:32Z",
145 | "published_at": "2013-05-27T19:35:32Z",
146 | "author": {
147 | "login": "octocat",
148 | "id": 1,
149 | "node_id": "MDQ6VXNlcjE=",
150 | "avatar_url": "https://github.com/images/error/octocat_happy.gif",
151 | "gravatar_id": "",
152 | "url": "https://api.github.com/users/octocat",
153 | "html_url": "https://github.com/octocat",
154 | "followers_url": "https://api.github.com/users/octocat/followers",
155 | "following_url": "https://api.github.com/users/octocat/following{/other_user}",
156 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
157 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
158 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
159 | "organizations_url": "https://api.github.com/users/octocat/orgs",
160 | "repos_url": "https://api.github.com/users/octocat/repos",
161 | "events_url": "https://api.github.com/users/octocat/events{/privacy}",
162 | "received_events_url": "https://api.github.com/users/octocat/received_events",
163 | "type": "User",
164 | "site_admin": false
165 | },
166 | "assets": []
167 | },
168 | {
169 | "url": "https://api.github.com/repos/octocat/Hello-World/releases/5",
170 | "html_url": "https://github.com/octocat/Hello-World/releases/v2.0.0",
171 | "assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/5/assets",
172 | "upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/5/assets{?name,label}",
173 | "tarball_url": "https://api.github.com/repos/octocat/Hello-World/tarball/v2.0.0",
174 | "zipball_url": "https://api.github.com/repos/octocat/Hello-World/zipball/v2.0.0",
175 | "id": 1,
176 | "node_id": "MDc6UmVsZWFzZTE=",
177 | "tag_name": "v2.0.0",
178 | "target_commitish": "master",
179 | "name": "v2.0.0 Release",
180 | "body": "The changelog of this release",
181 | "draft": false,
182 | "prerelease": false,
183 | "created_at": "2013-06-27T19:35:32Z",
184 | "published_at": "2013-06-27T19:35:32Z",
185 | "author": {
186 | "login": "octocat",
187 | "id": 1,
188 | "node_id": "MDQ6VXNlcjE=",
189 | "avatar_url": "https://github.com/images/error/octocat_happy.gif",
190 | "gravatar_id": "",
191 | "url": "https://api.github.com/users/octocat",
192 | "html_url": "https://github.com/octocat",
193 | "followers_url": "https://api.github.com/users/octocat/followers",
194 | "following_url": "https://api.github.com/users/octocat/following{/other_user}",
195 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
196 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
197 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
198 | "organizations_url": "https://api.github.com/users/octocat/orgs",
199 | "repos_url": "https://api.github.com/users/octocat/repos",
200 | "events_url": "https://api.github.com/users/octocat/events{/privacy}",
201 | "received_events_url": "https://api.github.com/users/octocat/received_events",
202 | "type": "User",
203 | "site_admin": false
204 | },
205 | "assets": []
206 | }
207 | ]
208 | """
209 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/TestHelpers/Mocks.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Mocks.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 | // swiftlint:disable final_class
9 |
10 | import Foundation
11 | @testable import GitBuddyCore
12 | import Mocker
13 | import OctoKit
14 | import XCTest
15 |
16 | struct MockedShell: ShellExecuting {
17 | static var commandMocks: [String: String] = [:]
18 |
19 | @discardableResult static func execute(_ command: ShellCommand) -> String {
20 | return commandMocks[command.rawValue] ?? ""
21 | }
22 |
23 | static func mock(_ command: ShellCommand, value: String) {
24 | commandMocks[command.rawValue] = value
25 | }
26 |
27 | static func mockCommitish(_ commitish: String, date: Date = Date()) {
28 | let dateFormatter = DateFormatter()
29 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
30 | commandMocks[ShellCommand.commitDate(commitish: commitish).rawValue] = dateFormatter.string(from: date)
31 | }
32 |
33 | static func mockRelease(tag: String, date: Date = Date()) {
34 | let dateFormatter = DateFormatter()
35 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
36 | commandMocks[ShellCommand.latestTag.rawValue] = tag
37 | commandMocks[ShellCommand.tagCreationDate(tag: tag).rawValue] = dateFormatter.string(from: date)
38 | }
39 |
40 | static func mockGITProject(organisation: String = "WeTransfer", repository: String = "GitBuddy") {
41 | commandMocks[ShellCommand.repositoryName.rawValue] = "\(organisation)/\(repository)"
42 | }
43 | }
44 |
45 | class MockChangelogInput: ChangelogInput {
46 | let number: Int
47 | let title: String?
48 | let body: String?
49 | let username: String?
50 | let htmlURL: Foundation.URL?
51 |
52 | init(number: Int = 0, title: String? = nil, body: String? = nil, username: String? = nil, htmlURL: URL? = nil) {
53 | self.number = number
54 | self.title = title
55 | self.body = body
56 | self.username = username
57 | self.htmlURL = htmlURL
58 | }
59 | }
60 |
61 | final class MockedPullRequest: MockChangelogInput, ChangelogPullRequest {}
62 | final class MockedIssue: MockChangelogInput, ChangelogIssue {}
63 |
64 | extension Mocker {
65 | static func mockPullRequests(baseBranch: String = "master") {
66 | let dateFormatter = DateFormatter()
67 | dateFormatter.dateFormat = "yyyy-MM-dd"
68 | let date = dateFormatter.date(from: "2020-01-03")!
69 | MockedShell.mockRelease(tag: "1.0.0", date: date)
70 |
71 | var urlComponents = URLComponents(
72 | url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/pulls")!,
73 | resolvingAgainstBaseURL: false
74 | )!
75 | urlComponents.queryItems = [
76 | URLQueryItem(name: "base", value: baseBranch),
77 | URLQueryItem(name: "direction", value: "desc"),
78 | URLQueryItem(name: "per_page", value: "100"),
79 | URLQueryItem(name: "sort", value: "updated"),
80 | URLQueryItem(name: "state", value: "closed")
81 | ]
82 |
83 | let pullRequestJSONData = Data(PullRequestsJSON.utf8)
84 | let mock = Mock(url: urlComponents.url!, dataType: .json, statusCode: 200, data: [.get: pullRequestJSONData])
85 | mock.register()
86 | }
87 |
88 | static func mockIssues() {
89 | let dateFormatter = DateFormatter()
90 | dateFormatter.dateFormat = "yyyy-MM-dd"
91 | let date = dateFormatter.date(from: "2020-01-03")!
92 | MockedShell.mockRelease(tag: "1.0.0", date: date)
93 |
94 | var urlComponents = URLComponents(
95 | url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/issues")!,
96 | resolvingAgainstBaseURL: false
97 | )!
98 | urlComponents.queryItems = [
99 | URLQueryItem(name: "page", value: "1"),
100 | URLQueryItem(name: "per_page", value: "100"),
101 | URLQueryItem(name: "state", value: "closed")
102 | ]
103 |
104 | let data = Data(IssuesJSON.utf8)
105 | let mock = Mock(url: urlComponents.url!, dataType: .json, statusCode: 200, data: [.get: data])
106 | mock.register()
107 | }
108 |
109 | static func mockForIssueNumber(_ issueNumber: Int) {
110 | let urlComponents = URLComponents(string: "https://api.github.com/repos/WeTransfer/Diagnostics/issues/\(issueNumber)")!
111 | let issueJSONData = Data(IssueJSON.utf8)
112 | Mock(url: urlComponents.url!, dataType: .json, statusCode: 200, data: [.get: issueJSONData]).register()
113 | }
114 |
115 | @discardableResult static func mockRelease() -> Mock {
116 | let releaseJSONData = Data(ReleaseJSON.utf8)
117 | let mock = Mock(
118 | url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/releases")!,
119 | dataType: .json,
120 | statusCode: 201,
121 | data: [.post: releaseJSONData]
122 | )
123 | mock.register()
124 | return mock
125 | }
126 |
127 | @discardableResult static func mockReleaseNotes() -> Mock {
128 | let releaseNotesJSONData = Data(ReleaseNotesJSON.utf8)
129 | let mock = Mock(
130 | url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/releases/generate-notes")!,
131 | dataType: .json,
132 | statusCode: 200,
133 | data: [.post: releaseNotesJSONData]
134 | )
135 | mock.register()
136 | return mock
137 | }
138 |
139 | @discardableResult static func mockListReleases() -> Mock {
140 | let releaseJSONData = Data(ListReleasesJSON.utf8)
141 | let mock = Mock(
142 | url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/releases?per_page=100")!,
143 | dataType: .json,
144 | statusCode: 200,
145 | data: [.get: releaseJSONData]
146 | )
147 | mock.register()
148 | return mock
149 | }
150 |
151 | @discardableResult static func mockDeletingRelease(id: Int) -> Mock {
152 | let mock = Mock(
153 | url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/releases/\(id)")!,
154 | dataType: .json,
155 | statusCode: 204,
156 | data: [.delete: Data()]
157 | )
158 | mock.register()
159 | return mock
160 | }
161 |
162 | @discardableResult static func mockDeletingReference(tagName: String) -> Mock {
163 | let mock = Mock(
164 | url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/git/refs/tags/\(tagName)")!,
165 | dataType: .json,
166 | statusCode: 204,
167 | data: [.delete: Data()]
168 | )
169 | mock.register()
170 | return mock
171 | }
172 |
173 | static func mockForCommentingOn(issueNumber: Int) -> Mock {
174 | let urlComponents = URLComponents(string: "https://api.github.com/repos/WeTransfer/Diagnostics/issues/\(issueNumber)/comments")!
175 | let commentJSONData = Data(CommentJSON.utf8)
176 | return Mock(url: urlComponents.url!, dataType: .json, statusCode: 201, data: [.post: commentJSONData])
177 | }
178 | }
179 |
180 | extension Data {
181 | func mapJSON(to type: T.Type) -> T {
182 | let decoder = JSONDecoder()
183 | decoder.dateDecodingStrategy = .formatted(Time.rfc3339DateFormatter)
184 | return try! decoder.decode(type, from: self)
185 | }
186 | }
187 |
188 | extension XCTestCase {
189 | func mockGITAuthentication(_ token: String = "username:79B02BE4-38D1-4E3D-9B41-4E0739761512") {
190 | Octokit.environment = ["GITBUDDY_ACCESS_TOKEN": token]
191 |
192 | addTeardownBlock {
193 | Octokit.environment = ProcessInfo.processInfo.environment
194 | }
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/TestHelpers/PullRequestsJSON.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PullRequestsJSON.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 10/01/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable line_length
12 | let PullRequestsJSON = """
13 |
14 | [{
15 | "url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/50",
16 | "id": 359005203,
17 | "node_id": "MDExOlB1bGxSZXF1ZXN0MzU5MDA1MjAz",
18 | "html_url": "https://github.com/WeTransfer/Diagnostics/pull/50",
19 | "diff_url": "https://github.com/WeTransfer/Diagnostics/pull/50.diff",
20 | "patch_url": "https://github.com/WeTransfer/Diagnostics/pull/50.patch",
21 | "issue_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/50",
22 | "number": 50,
23 | "state": "closed",
24 | "locked": false,
25 | "title": "Add charset utf-8 to html head 😃",
26 | "user": {
27 | "login": "AvdLee",
28 | "id": 793147,
29 | "node_id": "MDQ6VXNlcjc5MzE0Nw==",
30 | "avatar_url": "https://avatars0.githubusercontent.com/u/793147?v=4",
31 | "gravatar_id": "",
32 | "url": "https://api.github.com/users/AvdLee",
33 | "html_url": "https://github.com/AvdLee",
34 | "followers_url": "https://api.github.com/users/AvdLee/followers",
35 | "following_url": "https://api.github.com/users/AvdLee/following{/other_user}",
36 | "gists_url": "https://api.github.com/users/AvdLee/gists{/gist_id}",
37 | "starred_url": "https://api.github.com/users/AvdLee/starred{/owner}{/repo}",
38 | "subscriptions_url": "https://api.github.com/users/AvdLee/subscriptions",
39 | "organizations_url": "https://api.github.com/users/AvdLee/orgs",
40 | "repos_url": "https://api.github.com/users/AvdLee/repos",
41 | "events_url": "https://api.github.com/users/AvdLee/events{/privacy}",
42 | "received_events_url": "https://api.github.com/users/AvdLee/received_events",
43 | "type": "User",
44 | "site_admin": false
45 | },
46 | "body": "Non-ascii characters in HTML output were not displayed correctly in Safari. Specifying charset to utf-8 in HTML meta tag will fix this issue.",
47 | "created_at": "2020-01-03T14:22:35Z",
48 | "updated_at": "2020-01-06T12:46:36Z",
49 | "closed_at": "2020-01-06T12:46:36Z",
50 | "merged_at": "2020-01-06T12:46:35Z",
51 | "merge_commit_sha": "ada293ab4b12f6423da130edf37217d94ccf8148",
52 | "assignee": {
53 | "login": "kairadiagne",
54 | "id": 4329185,
55 | "node_id": "MDQ6VXNlcjQzMjkxODU=",
56 | "avatar_url": "https://avatars2.githubusercontent.com/u/4329185?v=4",
57 | "gravatar_id": "",
58 | "url": "https://api.github.com/users/kairadiagne",
59 | "html_url": "https://github.com/kairadiagne",
60 | "followers_url": "https://api.github.com/users/kairadiagne/followers",
61 | "following_url": "https://api.github.com/users/kairadiagne/following{/other_user}",
62 | "gists_url": "https://api.github.com/users/kairadiagne/gists{/gist_id}",
63 | "starred_url": "https://api.github.com/users/kairadiagne/starred{/owner}{/repo}",
64 | "subscriptions_url": "https://api.github.com/users/kairadiagne/subscriptions",
65 | "organizations_url": "https://api.github.com/users/kairadiagne/orgs",
66 | "repos_url": "https://api.github.com/users/kairadiagne/repos",
67 | "events_url": "https://api.github.com/users/kairadiagne/events{/privacy}",
68 | "received_events_url": "https://api.github.com/users/kairadiagne/received_events",
69 | "type": "User",
70 | "site_admin": false
71 | },
72 | "assignees": [{
73 | "login": "kairadiagne",
74 | "id": 4329185,
75 | "node_id": "MDQ6VXNlcjQzMjkxODU=",
76 | "avatar_url": "https://avatars2.githubusercontent.com/u/4329185?v=4",
77 | "gravatar_id": "",
78 | "url": "https://api.github.com/users/kairadiagne",
79 | "html_url": "https://github.com/kairadiagne",
80 | "followers_url": "https://api.github.com/users/kairadiagne/followers",
81 | "following_url": "https://api.github.com/users/kairadiagne/following{/other_user}",
82 | "gists_url": "https://api.github.com/users/kairadiagne/gists{/gist_id}",
83 | "starred_url": "https://api.github.com/users/kairadiagne/starred{/owner}{/repo}",
84 | "subscriptions_url": "https://api.github.com/users/kairadiagne/subscriptions",
85 | "organizations_url": "https://api.github.com/users/kairadiagne/orgs",
86 | "repos_url": "https://api.github.com/users/kairadiagne/repos",
87 | "events_url": "https://api.github.com/users/kairadiagne/events{/privacy}",
88 | "received_events_url": "https://api.github.com/users/kairadiagne/received_events",
89 | "type": "User",
90 | "site_admin": false
91 | }],
92 | "requested_reviewers": [
93 |
94 | ],
95 | "requested_teams": [
96 |
97 | ],
98 | "labels": [
99 |
100 | ],
101 | "milestone": null,
102 | "commits_url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/50/commits",
103 | "review_comments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/50/comments",
104 | "review_comment_url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/comments{/number}",
105 | "comments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/50/comments",
106 | "statuses_url": "https://api.github.com/repos/WeTransfer/Diagnostics/statuses/6c695a40f809fdf80e5522e92b4ded1903a42097",
107 | "head": {
108 | "label": "AvdLee:charset-in-html",
109 | "ref": "charset-in-html",
110 | "sha": "6c695a40f809fdf80e5522e92b4ded1903a42097",
111 | "user": {
112 | "login": "AvdLee",
113 | "id": 793147,
114 | "node_id": "MDQ6VXNlcjc5MzE0Nw==",
115 | "avatar_url": "https://avatars0.githubusercontent.com/u/793147?v=4",
116 | "gravatar_id": "",
117 | "url": "https://api.github.com/users/AvdLee",
118 | "html_url": "https://github.com/AvdLee",
119 | "followers_url": "https://api.github.com/users/AvdLee/followers",
120 | "following_url": "https://api.github.com/users/AvdLee/following{/other_user}",
121 | "gists_url": "https://api.github.com/users/AvdLee/gists{/gist_id}",
122 | "starred_url": "https://api.github.com/users/AvdLee/starred{/owner}{/repo}",
123 | "subscriptions_url": "https://api.github.com/users/AvdLee/subscriptions",
124 | "organizations_url": "https://api.github.com/users/AvdLee/orgs",
125 | "repos_url": "https://api.github.com/users/AvdLee/repos",
126 | "events_url": "https://api.github.com/users/AvdLee/events{/privacy}",
127 | "received_events_url": "https://api.github.com/users/AvdLee/received_events",
128 | "type": "User",
129 | "site_admin": false
130 | },
131 | "repo": null
132 | },
133 | "base": {
134 | "label": "WeTransfer:master",
135 | "ref": "master",
136 | "sha": "f67a18ae2aa208fd94178b729e5dced0e56eac77",
137 | "user": {
138 | "login": "WeTransfer",
139 | "id": 14807662,
140 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0ODA3NjYy",
141 | "avatar_url": "https://avatars3.githubusercontent.com/u/14807662?v=4",
142 | "gravatar_id": "",
143 | "url": "https://api.github.com/users/WeTransfer",
144 | "html_url": "https://github.com/WeTransfer",
145 | "followers_url": "https://api.github.com/users/WeTransfer/followers",
146 | "following_url": "https://api.github.com/users/WeTransfer/following{/other_user}",
147 | "gists_url": "https://api.github.com/users/WeTransfer/gists{/gist_id}",
148 | "starred_url": "https://api.github.com/users/WeTransfer/starred{/owner}{/repo}",
149 | "subscriptions_url": "https://api.github.com/users/WeTransfer/subscriptions",
150 | "organizations_url": "https://api.github.com/users/WeTransfer/orgs",
151 | "repos_url": "https://api.github.com/users/WeTransfer/repos",
152 | "events_url": "https://api.github.com/users/WeTransfer/events{/privacy}",
153 | "received_events_url": "https://api.github.com/users/WeTransfer/received_events",
154 | "type": "Organization",
155 | "site_admin": false
156 | },
157 | "repo": {
158 | "id": 225350163,
159 | "node_id": "MDEwOlJlcG9zaXRvcnkyMjUzNTAxNjM=",
160 | "name": "Diagnostics",
161 | "full_name": "WeTransfer/Diagnostics",
162 | "private": false,
163 | "owner": {
164 | "login": "WeTransfer",
165 | "id": 14807662,
166 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0ODA3NjYy",
167 | "avatar_url": "https://avatars3.githubusercontent.com/u/14807662?v=4",
168 | "gravatar_id": "",
169 | "url": "https://api.github.com/users/WeTransfer",
170 | "html_url": "https://github.com/WeTransfer",
171 | "followers_url": "https://api.github.com/users/WeTransfer/followers",
172 | "following_url": "https://api.github.com/users/WeTransfer/following{/other_user}",
173 | "gists_url": "https://api.github.com/users/WeTransfer/gists{/gist_id}",
174 | "starred_url": "https://api.github.com/users/WeTransfer/starred{/owner}{/repo}",
175 | "subscriptions_url": "https://api.github.com/users/WeTransfer/subscriptions",
176 | "organizations_url": "https://api.github.com/users/WeTransfer/orgs",
177 | "repos_url": "https://api.github.com/users/WeTransfer/repos",
178 | "events_url": "https://api.github.com/users/WeTransfer/events{/privacy}",
179 | "received_events_url": "https://api.github.com/users/WeTransfer/received_events",
180 | "type": "Organization",
181 | "site_admin": false
182 | },
183 | "html_url": "https://github.com/WeTransfer/Diagnostics",
184 | "description": "Allow users to easily share Diagnostics with your support team to improve the flow of fixing bugs.",
185 | "fork": false,
186 | "url": "https://api.github.com/repos/WeTransfer/Diagnostics",
187 | "forks_url": "https://api.github.com/repos/WeTransfer/Diagnostics/forks",
188 | "keys_url": "https://api.github.com/repos/WeTransfer/Diagnostics/keys{/key_id}",
189 | "collaborators_url": "https://api.github.com/repos/WeTransfer/Diagnostics/collaborators{/collaborator}",
190 | "teams_url": "https://api.github.com/repos/WeTransfer/Diagnostics/teams",
191 | "hooks_url": "https://api.github.com/repos/WeTransfer/Diagnostics/hooks",
192 | "issue_events_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/events{/number}",
193 | "events_url": "https://api.github.com/repos/WeTransfer/Diagnostics/events",
194 | "assignees_url": "https://api.github.com/repos/WeTransfer/Diagnostics/assignees{/user}",
195 | "branches_url": "https://api.github.com/repos/WeTransfer/Diagnostics/branches{/branch}",
196 | "tags_url": "https://api.github.com/repos/WeTransfer/Diagnostics/tags",
197 | "blobs_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/blobs{/sha}",
198 | "git_tags_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/tags{/sha}",
199 | "git_refs_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/refs{/sha}",
200 | "trees_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/trees{/sha}",
201 | "statuses_url": "https://api.github.com/repos/WeTransfer/Diagnostics/statuses/{sha}",
202 | "languages_url": "https://api.github.com/repos/WeTransfer/Diagnostics/languages",
203 | "stargazers_url": "https://api.github.com/repos/WeTransfer/Diagnostics/stargazers",
204 | "contributors_url": "https://api.github.com/repos/WeTransfer/Diagnostics/contributors",
205 | "subscribers_url": "https://api.github.com/repos/WeTransfer/Diagnostics/subscribers",
206 | "subscription_url": "https://api.github.com/repos/WeTransfer/Diagnostics/subscription",
207 | "commits_url": "https://api.github.com/repos/WeTransfer/Diagnostics/commits{/sha}",
208 | "git_commits_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/commits{/sha}",
209 | "comments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/comments{/number}",
210 | "issue_comment_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/comments{/number}",
211 | "contents_url": "https://api.github.com/repos/WeTransfer/Diagnostics/contents/{+path}",
212 | "compare_url": "https://api.github.com/repos/WeTransfer/Diagnostics/compare/{base}...{head}",
213 | "merges_url": "https://api.github.com/repos/WeTransfer/Diagnostics/merges",
214 | "archive_url": "https://api.github.com/repos/WeTransfer/Diagnostics/{archive_format}{/ref}",
215 | "downloads_url": "https://api.github.com/repos/WeTransfer/Diagnostics/downloads",
216 | "issues_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues{/number}",
217 | "pulls_url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls{/number}",
218 | "milestones_url": "https://api.github.com/repos/WeTransfer/Diagnostics/milestones{/number}",
219 | "notifications_url": "https://api.github.com/repos/WeTransfer/Diagnostics/notifications{?since,all,participating}",
220 | "labels_url": "https://api.github.com/repos/WeTransfer/Diagnostics/labels{/name}",
221 | "releases_url": "https://api.github.com/repos/WeTransfer/Diagnostics/releases{/id}",
222 | "deployments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/deployments",
223 | "created_at": "2019-12-02T10:43:07Z",
224 | "updated_at": "2020-01-16T02:01:31Z",
225 | "pushed_at": "2020-01-06T12:46:36Z",
226 | "git_url": "git://github.com/WeTransfer/Diagnostics.git",
227 | "ssh_url": "git@github.com:WeTransfer/Diagnostics.git",
228 | "clone_url": "https://github.com/WeTransfer/Diagnostics.git",
229 | "svn_url": "https://github.com/WeTransfer/Diagnostics",
230 | "homepage": null,
231 | "size": 2118,
232 | "stargazers_count": 377,
233 | "watchers_count": 377,
234 | "language": "Swift",
235 | "has_issues": true,
236 | "has_projects": true,
237 | "has_downloads": true,
238 | "has_wiki": true,
239 | "has_pages": false,
240 | "forks_count": 20,
241 | "mirror_url": null,
242 | "archived": false,
243 | "disabled": false,
244 | "open_issues_count": 3,
245 | "license": {
246 | "key": "mit",
247 | "name": "MIT License",
248 | "spdx_id": "MIT",
249 | "url": "https://api.github.com/licenses/mit",
250 | "node_id": "MDc6TGljZW5zZTEz"
251 | },
252 | "forks": 20,
253 | "open_issues": 3,
254 | "watchers": 377,
255 | "default_branch": "master"
256 | }
257 | },
258 | "_links": {
259 | "self": {
260 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/50"
261 | },
262 | "html": {
263 | "href": "https://github.com/WeTransfer/Diagnostics/pull/50"
264 | },
265 | "issue": {
266 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/50"
267 | },
268 | "comments": {
269 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/50/comments"
270 | },
271 | "review_comments": {
272 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/50/comments"
273 | },
274 | "review_comment": {
275 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/comments{/number}"
276 | },
277 | "commits": {
278 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/50/commits"
279 | },
280 | "statuses": {
281 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/statuses/6c695a40f809fdf80e5522e92b4ded1903a42097"
282 | }
283 | },
284 | "author_association": "CONTRIBUTOR"
285 | },
286 | {
287 | "url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/49",
288 | "id": 358972101,
289 | "node_id": "MDExOlB1bGxSZXF1ZXN0MzU4OTcyMTAx",
290 | "html_url": "https://github.com/WeTransfer/Diagnostics/pull/49",
291 | "diff_url": "https://github.com/WeTransfer/Diagnostics/pull/49.diff",
292 | "patch_url": "https://github.com/WeTransfer/Diagnostics/pull/49.patch",
293 | "issue_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/49",
294 | "number": 49,
295 | "state": "closed",
296 | "locked": false,
297 | "title": "Fix warning occurring in pod library because of importing style.css #trivial",
298 | "user": {
299 | "login": "AvdLee",
300 | "id": 4329185,
301 | "node_id": "MDQ6VXNlcjQzMjkxODU=",
302 | "avatar_url": "https://avatars2.githubusercontent.com/u/4329185?v=4",
303 | "gravatar_id": "",
304 | "url": "https://api.github.com/users/AvdLee",
305 | "html_url": "https://github.com/AvdLee",
306 | "followers_url": "https://api.github.com/users/AvdLee/followers",
307 | "following_url": "https://api.github.com/users/AvdLee/following{/other_user}",
308 | "gists_url": "https://api.github.com/users/AvdLee/gists{/gist_id}",
309 | "starred_url": "https://api.github.com/users/AvdLee/starred{/owner}{/repo}",
310 | "subscriptions_url": "https://api.github.com/users/AvdLee/subscriptions",
311 | "organizations_url": "https://api.github.com/users/AvdLee/orgs",
312 | "repos_url": "https://api.github.com/users/AvdLee/repos",
313 | "events_url": "https://api.github.com/users/AvdLee/events{/privacy}",
314 | "received_events_url": "https://api.github.com/users/AvdLee/received_events",
315 | "type": "User",
316 | "site_admin": false
317 | },
318 | "body": "Fixes #39",
319 | "created_at": "2020-01-03T12:24:16Z",
320 | "updated_at": "2020-01-03T13:03:00Z",
321 | "closed_at": "2020-01-03T13:02:59Z",
322 | "merged_at": "2020-01-03T13:02:59Z",
323 | "merge_commit_sha": "fa4827ff14a3f9db52cb6bf133c1e7796335aa09",
324 | "assignee": {
325 | "login": "AvdLee",
326 | "id": 4329185,
327 | "node_id": "MDQ6VXNlcjQzMjkxODU=",
328 | "avatar_url": "https://avatars2.githubusercontent.com/u/4329185?v=4",
329 | "gravatar_id": "",
330 | "url": "https://api.github.com/users/AvdLee",
331 | "html_url": "https://github.com/AvdLee",
332 | "followers_url": "https://api.github.com/users/AvdLee/followers",
333 | "following_url": "https://api.github.com/users/AvdLee/following{/other_user}",
334 | "gists_url": "https://api.github.com/users/AvdLee/gists{/gist_id}",
335 | "starred_url": "https://api.github.com/users/AvdLee/starred{/owner}{/repo}",
336 | "subscriptions_url": "https://api.github.com/users/AvdLee/subscriptions",
337 | "organizations_url": "https://api.github.com/users/AvdLee/orgs",
338 | "repos_url": "https://api.github.com/users/AvdLee/repos",
339 | "events_url": "https://api.github.com/users/AvdLee/events{/privacy}",
340 | "received_events_url": "https://api.github.com/users/AvdLee/received_events",
341 | "type": "User",
342 | "site_admin": false
343 | },
344 | "assignees": [{
345 | "login": "AvdLee",
346 | "id": 4329185,
347 | "node_id": "MDQ6VXNlcjQzMjkxODU=",
348 | "avatar_url": "https://avatars2.githubusercontent.com/u/4329185?v=4",
349 | "gravatar_id": "",
350 | "url": "https://api.github.com/users/AvdLee",
351 | "html_url": "https://github.com/AvdLee",
352 | "followers_url": "https://api.github.com/users/AvdLee/followers",
353 | "following_url": "https://api.github.com/users/AvdLee/following{/other_user}",
354 | "gists_url": "https://api.github.com/users/AvdLee/gists{/gist_id}",
355 | "starred_url": "https://api.github.com/users/AvdLee/starred{/owner}{/repo}",
356 | "subscriptions_url": "https://api.github.com/users/AvdLee/subscriptions",
357 | "organizations_url": "https://api.github.com/users/AvdLee/orgs",
358 | "repos_url": "https://api.github.com/users/AvdLee/repos",
359 | "events_url": "https://api.github.com/users/AvdLee/events{/privacy}",
360 | "received_events_url": "https://api.github.com/users/AvdLee/received_events",
361 | "type": "User",
362 | "site_admin": false
363 | }],
364 | "requested_reviewers": [
365 |
366 | ],
367 | "requested_teams": [
368 |
369 | ],
370 | "labels": [
371 |
372 | ],
373 | "milestone": null,
374 | "commits_url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/49/commits",
375 | "review_comments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/49/comments",
376 | "review_comment_url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/comments{/number}",
377 | "comments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/49/comments",
378 | "statuses_url": "https://api.github.com/repos/WeTransfer/Diagnostics/statuses/f0e8bd17cff045a8670a35410bb8b211237786f1",
379 | "head": {
380 | "label": "WeTransfer:feature/39-pod-warning-fix",
381 | "ref": "feature/39-pod-warning-fix",
382 | "sha": "f0e8bd17cff045a8670a35410bb8b211237786f1",
383 | "user": {
384 | "login": "WeTransfer",
385 | "id": 14807662,
386 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0ODA3NjYy",
387 | "avatar_url": "https://avatars3.githubusercontent.com/u/14807662?v=4",
388 | "gravatar_id": "",
389 | "url": "https://api.github.com/users/WeTransfer",
390 | "html_url": "https://github.com/WeTransfer",
391 | "followers_url": "https://api.github.com/users/WeTransfer/followers",
392 | "following_url": "https://api.github.com/users/WeTransfer/following{/other_user}",
393 | "gists_url": "https://api.github.com/users/WeTransfer/gists{/gist_id}",
394 | "starred_url": "https://api.github.com/users/WeTransfer/starred{/owner}{/repo}",
395 | "subscriptions_url": "https://api.github.com/users/WeTransfer/subscriptions",
396 | "organizations_url": "https://api.github.com/users/WeTransfer/orgs",
397 | "repos_url": "https://api.github.com/users/WeTransfer/repos",
398 | "events_url": "https://api.github.com/users/WeTransfer/events{/privacy}",
399 | "received_events_url": "https://api.github.com/users/WeTransfer/received_events",
400 | "type": "Organization",
401 | "site_admin": false
402 | },
403 | "repo": {
404 | "id": 225350163,
405 | "node_id": "MDEwOlJlcG9zaXRvcnkyMjUzNTAxNjM=",
406 | "name": "Diagnostics",
407 | "full_name": "WeTransfer/Diagnostics",
408 | "private": false,
409 | "owner": {
410 | "login": "WeTransfer",
411 | "id": 14807662,
412 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0ODA3NjYy",
413 | "avatar_url": "https://avatars3.githubusercontent.com/u/14807662?v=4",
414 | "gravatar_id": "",
415 | "url": "https://api.github.com/users/WeTransfer",
416 | "html_url": "https://github.com/WeTransfer",
417 | "followers_url": "https://api.github.com/users/WeTransfer/followers",
418 | "following_url": "https://api.github.com/users/WeTransfer/following{/other_user}",
419 | "gists_url": "https://api.github.com/users/WeTransfer/gists{/gist_id}",
420 | "starred_url": "https://api.github.com/users/WeTransfer/starred{/owner}{/repo}",
421 | "subscriptions_url": "https://api.github.com/users/WeTransfer/subscriptions",
422 | "organizations_url": "https://api.github.com/users/WeTransfer/orgs",
423 | "repos_url": "https://api.github.com/users/WeTransfer/repos",
424 | "events_url": "https://api.github.com/users/WeTransfer/events{/privacy}",
425 | "received_events_url": "https://api.github.com/users/WeTransfer/received_events",
426 | "type": "Organization",
427 | "site_admin": false
428 | },
429 | "html_url": "https://github.com/WeTransfer/Diagnostics",
430 | "description": "Allow users to easily share Diagnostics with your support team to improve the flow of fixing bugs.",
431 | "fork": false,
432 | "url": "https://api.github.com/repos/WeTransfer/Diagnostics",
433 | "forks_url": "https://api.github.com/repos/WeTransfer/Diagnostics/forks",
434 | "keys_url": "https://api.github.com/repos/WeTransfer/Diagnostics/keys{/key_id}",
435 | "collaborators_url": "https://api.github.com/repos/WeTransfer/Diagnostics/collaborators{/collaborator}",
436 | "teams_url": "https://api.github.com/repos/WeTransfer/Diagnostics/teams",
437 | "hooks_url": "https://api.github.com/repos/WeTransfer/Diagnostics/hooks",
438 | "issue_events_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/events{/number}",
439 | "events_url": "https://api.github.com/repos/WeTransfer/Diagnostics/events",
440 | "assignees_url": "https://api.github.com/repos/WeTransfer/Diagnostics/assignees{/user}",
441 | "branches_url": "https://api.github.com/repos/WeTransfer/Diagnostics/branches{/branch}",
442 | "tags_url": "https://api.github.com/repos/WeTransfer/Diagnostics/tags",
443 | "blobs_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/blobs{/sha}",
444 | "git_tags_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/tags{/sha}",
445 | "git_refs_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/refs{/sha}",
446 | "trees_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/trees{/sha}",
447 | "statuses_url": "https://api.github.com/repos/WeTransfer/Diagnostics/statuses/{sha}",
448 | "languages_url": "https://api.github.com/repos/WeTransfer/Diagnostics/languages",
449 | "stargazers_url": "https://api.github.com/repos/WeTransfer/Diagnostics/stargazers",
450 | "contributors_url": "https://api.github.com/repos/WeTransfer/Diagnostics/contributors",
451 | "subscribers_url": "https://api.github.com/repos/WeTransfer/Diagnostics/subscribers",
452 | "subscription_url": "https://api.github.com/repos/WeTransfer/Diagnostics/subscription",
453 | "commits_url": "https://api.github.com/repos/WeTransfer/Diagnostics/commits{/sha}",
454 | "git_commits_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/commits{/sha}",
455 | "comments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/comments{/number}",
456 | "issue_comment_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/comments{/number}",
457 | "contents_url": "https://api.github.com/repos/WeTransfer/Diagnostics/contents/{+path}",
458 | "compare_url": "https://api.github.com/repos/WeTransfer/Diagnostics/compare/{base}...{head}",
459 | "merges_url": "https://api.github.com/repos/WeTransfer/Diagnostics/merges",
460 | "archive_url": "https://api.github.com/repos/WeTransfer/Diagnostics/{archive_format}{/ref}",
461 | "downloads_url": "https://api.github.com/repos/WeTransfer/Diagnostics/downloads",
462 | "issues_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues{/number}",
463 | "pulls_url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls{/number}",
464 | "milestones_url": "https://api.github.com/repos/WeTransfer/Diagnostics/milestones{/number}",
465 | "notifications_url": "https://api.github.com/repos/WeTransfer/Diagnostics/notifications{?since,all,participating}",
466 | "labels_url": "https://api.github.com/repos/WeTransfer/Diagnostics/labels{/name}",
467 | "releases_url": "https://api.github.com/repos/WeTransfer/Diagnostics/releases{/id}",
468 | "deployments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/deployments",
469 | "created_at": "2019-12-02T10:43:07Z",
470 | "updated_at": "2020-01-16T02:01:31Z",
471 | "pushed_at": "2020-01-06T12:46:36Z",
472 | "git_url": "git://github.com/WeTransfer/Diagnostics.git",
473 | "ssh_url": "git@github.com:WeTransfer/Diagnostics.git",
474 | "clone_url": "https://github.com/WeTransfer/Diagnostics.git",
475 | "svn_url": "https://github.com/WeTransfer/Diagnostics",
476 | "homepage": null,
477 | "size": 2118,
478 | "stargazers_count": 377,
479 | "watchers_count": 377,
480 | "language": "Swift",
481 | "has_issues": true,
482 | "has_projects": true,
483 | "has_downloads": true,
484 | "has_wiki": true,
485 | "has_pages": false,
486 | "forks_count": 20,
487 | "mirror_url": null,
488 | "archived": false,
489 | "disabled": false,
490 | "open_issues_count": 3,
491 | "license": {
492 | "key": "mit",
493 | "name": "MIT License",
494 | "spdx_id": "MIT",
495 | "url": "https://api.github.com/licenses/mit",
496 | "node_id": "MDc6TGljZW5zZTEz"
497 | },
498 | "forks": 20,
499 | "open_issues": 3,
500 | "watchers": 377,
501 | "default_branch": "master"
502 | }
503 | },
504 | "base": {
505 | "label": "WeTransfer:master",
506 | "ref": "master",
507 | "sha": "464db9ef3a7459c664db82f254e69479afb236be",
508 | "user": {
509 | "login": "WeTransfer",
510 | "id": 14807662,
511 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0ODA3NjYy",
512 | "avatar_url": "https://avatars3.githubusercontent.com/u/14807662?v=4",
513 | "gravatar_id": "",
514 | "url": "https://api.github.com/users/WeTransfer",
515 | "html_url": "https://github.com/WeTransfer",
516 | "followers_url": "https://api.github.com/users/WeTransfer/followers",
517 | "following_url": "https://api.github.com/users/WeTransfer/following{/other_user}",
518 | "gists_url": "https://api.github.com/users/WeTransfer/gists{/gist_id}",
519 | "starred_url": "https://api.github.com/users/WeTransfer/starred{/owner}{/repo}",
520 | "subscriptions_url": "https://api.github.com/users/WeTransfer/subscriptions",
521 | "organizations_url": "https://api.github.com/users/WeTransfer/orgs",
522 | "repos_url": "https://api.github.com/users/WeTransfer/repos",
523 | "events_url": "https://api.github.com/users/WeTransfer/events{/privacy}",
524 | "received_events_url": "https://api.github.com/users/WeTransfer/received_events",
525 | "type": "Organization",
526 | "site_admin": false
527 | },
528 | "repo": {
529 | "id": 225350163,
530 | "node_id": "MDEwOlJlcG9zaXRvcnkyMjUzNTAxNjM=",
531 | "name": "Diagnostics",
532 | "full_name": "WeTransfer/Diagnostics",
533 | "private": false,
534 | "owner": {
535 | "login": "WeTransfer",
536 | "id": 14807662,
537 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0ODA3NjYy",
538 | "avatar_url": "https://avatars3.githubusercontent.com/u/14807662?v=4",
539 | "gravatar_id": "",
540 | "url": "https://api.github.com/users/WeTransfer",
541 | "html_url": "https://github.com/WeTransfer",
542 | "followers_url": "https://api.github.com/users/WeTransfer/followers",
543 | "following_url": "https://api.github.com/users/WeTransfer/following{/other_user}",
544 | "gists_url": "https://api.github.com/users/WeTransfer/gists{/gist_id}",
545 | "starred_url": "https://api.github.com/users/WeTransfer/starred{/owner}{/repo}",
546 | "subscriptions_url": "https://api.github.com/users/WeTransfer/subscriptions",
547 | "organizations_url": "https://api.github.com/users/WeTransfer/orgs",
548 | "repos_url": "https://api.github.com/users/WeTransfer/repos",
549 | "events_url": "https://api.github.com/users/WeTransfer/events{/privacy}",
550 | "received_events_url": "https://api.github.com/users/WeTransfer/received_events",
551 | "type": "Organization",
552 | "site_admin": false
553 | },
554 | "html_url": "https://github.com/WeTransfer/Diagnostics",
555 | "description": "Allow users to easily share Diagnostics with your support team to improve the flow of fixing bugs.",
556 | "fork": false,
557 | "url": "https://api.github.com/repos/WeTransfer/Diagnostics",
558 | "forks_url": "https://api.github.com/repos/WeTransfer/Diagnostics/forks",
559 | "keys_url": "https://api.github.com/repos/WeTransfer/Diagnostics/keys{/key_id}",
560 | "collaborators_url": "https://api.github.com/repos/WeTransfer/Diagnostics/collaborators{/collaborator}",
561 | "teams_url": "https://api.github.com/repos/WeTransfer/Diagnostics/teams",
562 | "hooks_url": "https://api.github.com/repos/WeTransfer/Diagnostics/hooks",
563 | "issue_events_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/events{/number}",
564 | "events_url": "https://api.github.com/repos/WeTransfer/Diagnostics/events",
565 | "assignees_url": "https://api.github.com/repos/WeTransfer/Diagnostics/assignees{/user}",
566 | "branches_url": "https://api.github.com/repos/WeTransfer/Diagnostics/branches{/branch}",
567 | "tags_url": "https://api.github.com/repos/WeTransfer/Diagnostics/tags",
568 | "blobs_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/blobs{/sha}",
569 | "git_tags_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/tags{/sha}",
570 | "git_refs_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/refs{/sha}",
571 | "trees_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/trees{/sha}",
572 | "statuses_url": "https://api.github.com/repos/WeTransfer/Diagnostics/statuses/{sha}",
573 | "languages_url": "https://api.github.com/repos/WeTransfer/Diagnostics/languages",
574 | "stargazers_url": "https://api.github.com/repos/WeTransfer/Diagnostics/stargazers",
575 | "contributors_url": "https://api.github.com/repos/WeTransfer/Diagnostics/contributors",
576 | "subscribers_url": "https://api.github.com/repos/WeTransfer/Diagnostics/subscribers",
577 | "subscription_url": "https://api.github.com/repos/WeTransfer/Diagnostics/subscription",
578 | "commits_url": "https://api.github.com/repos/WeTransfer/Diagnostics/commits{/sha}",
579 | "git_commits_url": "https://api.github.com/repos/WeTransfer/Diagnostics/git/commits{/sha}",
580 | "comments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/comments{/number}",
581 | "issue_comment_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/comments{/number}",
582 | "contents_url": "https://api.github.com/repos/WeTransfer/Diagnostics/contents/{+path}",
583 | "compare_url": "https://api.github.com/repos/WeTransfer/Diagnostics/compare/{base}...{head}",
584 | "merges_url": "https://api.github.com/repos/WeTransfer/Diagnostics/merges",
585 | "archive_url": "https://api.github.com/repos/WeTransfer/Diagnostics/{archive_format}{/ref}",
586 | "downloads_url": "https://api.github.com/repos/WeTransfer/Diagnostics/downloads",
587 | "issues_url": "https://api.github.com/repos/WeTransfer/Diagnostics/issues{/number}",
588 | "pulls_url": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls{/number}",
589 | "milestones_url": "https://api.github.com/repos/WeTransfer/Diagnostics/milestones{/number}",
590 | "notifications_url": "https://api.github.com/repos/WeTransfer/Diagnostics/notifications{?since,all,participating}",
591 | "labels_url": "https://api.github.com/repos/WeTransfer/Diagnostics/labels{/name}",
592 | "releases_url": "https://api.github.com/repos/WeTransfer/Diagnostics/releases{/id}",
593 | "deployments_url": "https://api.github.com/repos/WeTransfer/Diagnostics/deployments",
594 | "created_at": "2019-12-02T10:43:07Z",
595 | "updated_at": "2020-01-16T02:01:31Z",
596 | "pushed_at": "2020-01-06T12:46:36Z",
597 | "git_url": "git://github.com/WeTransfer/Diagnostics.git",
598 | "ssh_url": "git@github.com:WeTransfer/Diagnostics.git",
599 | "clone_url": "https://github.com/WeTransfer/Diagnostics.git",
600 | "svn_url": "https://github.com/WeTransfer/Diagnostics",
601 | "homepage": null,
602 | "size": 2118,
603 | "stargazers_count": 377,
604 | "watchers_count": 377,
605 | "language": "Swift",
606 | "has_issues": true,
607 | "has_projects": true,
608 | "has_downloads": true,
609 | "has_wiki": true,
610 | "has_pages": false,
611 | "forks_count": 20,
612 | "mirror_url": null,
613 | "archived": false,
614 | "disabled": false,
615 | "open_issues_count": 3,
616 | "license": {
617 | "key": "mit",
618 | "name": "MIT License",
619 | "spdx_id": "MIT",
620 | "url": "https://api.github.com/licenses/mit",
621 | "node_id": "MDc6TGljZW5zZTEz"
622 | },
623 | "forks": 20,
624 | "open_issues": 3,
625 | "watchers": 377,
626 | "default_branch": "master"
627 | }
628 | },
629 | "_links": {
630 | "self": {
631 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/49"
632 | },
633 | "html": {
634 | "href": "https://github.com/WeTransfer/Diagnostics/pull/49"
635 | },
636 | "issue": {
637 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/49"
638 | },
639 | "comments": {
640 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/issues/49/comments"
641 | },
642 | "review_comments": {
643 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/49/comments"
644 | },
645 | "review_comment": {
646 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/comments{/number}"
647 | },
648 | "commits": {
649 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/pulls/49/commits"
650 | },
651 | "statuses": {
652 | "href": "https://api.github.com/repos/WeTransfer/Diagnostics/statuses/f0e8bd17cff045a8670a35410bb8b211237786f1"
653 | }
654 | },
655 | "author_association": "MEMBER"
656 | }
657 | ]
658 |
659 | """
660 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/TestHelpers/ReleaseJSON.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReleaseJSON.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 05/02/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable line_length
12 | let ReleaseJSON = """
13 |
14 | {
15 | "id": 23401163,
16 | "draft": false,
17 | "published_at": "2020-02-05T08:49:48Z",
18 | "assets": [
19 |
20 | ],
21 | "prerelease": false,
22 | "author": {
23 | "id": 4329185,
24 | "organizations_url": "https://api.github.com/users/AvdLee/orgs",
25 | "received_events_url": "https://api.github.com/users/AvdLee/received_events",
26 | "following_url": "https://api.github.com/users/AvdLee/following{/other_user}",
27 | "login": "AvdLee",
28 | "avatar_url": "https://avatars2.githubusercontent.com/u/4329185?v=4",
29 | "url": "https://api.github.com/users/AvdLee",
30 | "node_id": "MDQ6VXNlcjQzMjkxODU=",
31 | "subscriptions_url": "https://api.github.com/users/AvdLee/subscriptions",
32 | "repos_url": "https://api.github.com/users/AvdLee/repos",
33 | "type": "User",
34 | "html_url": "https://github.com/AvdLee",
35 | "events_url": "https://api.github.com/users/AvdLee/events{/privacy}",
36 | "site_admin": false,
37 | "starred_url": "https://api.github.com/users/AvdLee/starred{/owner}{/repo}",
38 | "gists_url": "https://api.github.com/users/AvdLee/gists{/gist_id}",
39 | "gravatar_id": "",
40 | "followers_url": "https://api.github.com/users/AvdLee/followers"
41 | },
42 | "created_at": "2020-01-28T16:47:53Z",
43 | "zipball_url": "https://api.github.com/repos/WeTransfer/ChangelogProducer/zipball/1.0.1",
44 | "url": "https://api.github.com/repos/WeTransfer/ChangelogProducer/releases/23401163",
45 | "node_id": "MDc6UmVsZWFzZTIzNDAxMTYz",
46 | "body": "- Merge release 1.0.0 into master ([#8](https://github.com/WeTransfer/ChangelogProducer/pull/8))\\n- Update the readme with an example output ([#4](https://github.com/WeTransfer/ChangelogProducer/issues/4)) via @AvdLee",
47 | "target_commitish": "master",
48 | "tarball_url": "https://api.github.com/repos/WeTransfer/ChangelogProducer/tarball/1.0.1",
49 | "html_url": "https://github.com/WeTransfer/ChangelogProducer/releases/tag/1.0.1",
50 | "assets_url": "https://api.github.com/repos/WeTransfer/ChangelogProducer/releases/23401163/assets",
51 | "upload_url": "https://uploads.github.com/repos/WeTransfer/ChangelogProducer/releases/23401163/assets{?name,label}",
52 | "tag_name": "1.0.1",
53 | "name": "1.0.1"
54 | }
55 | """
56 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/TestHelpers/ReleaseNotesJSON.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReleaseNotesJSON.swift
3 | //
4 | //
5 | // Created by Antoine van der Lee on 13/06/2023.
6 | // Copyright © 2023 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // swiftlint:disable line_length
12 | let ReleaseNotesJSON = """
13 |
14 | {
15 | "name": "Release v1.0.0 is now available!",
16 | "body": "##Changes in Release v1.0.0 ... ##Contributors @monalisa"
17 | }
18 | """
19 |
--------------------------------------------------------------------------------
/Tests/GitBuddyTests/TestHelpers/XCTestExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCTestExtensions.swift
3 | // GitBuddyTests
4 | //
5 | // Created by Antoine van der Lee on 09/04/2020.
6 | //
7 |
8 | import Foundation
9 | @testable import GitBuddyCore
10 | import XCTest
11 |
12 | extension XCTest {
13 | func AssertEqualStringsIgnoringTrailingWhitespace(_ string1: String, _ string2: String, file: StaticString = #file,
14 | line: UInt = #line)
15 | {
16 | let lines1 = string1.split(separator: "\n", omittingEmptySubsequences: false)
17 | let lines2 = string2.split(separator: "\n", omittingEmptySubsequences: false)
18 |
19 | XCTAssertEqual(lines1.count, lines2.count, "Strings have different numbers of lines.", file: file, line: line)
20 | for (line1, line2) in zip(lines1, lines2) {
21 | XCTAssertEqual(line1.trimmed(), line2.trimmed(), file: file, line: line)
22 | }
23 | }
24 |
25 | /// Executes the command and throws the execution error if any occur.
26 | /// - Parameters:
27 | /// - command: The command to execute. This command can be exactly the same as you would use in the terminal.
28 | /// E.g. "gitbuddy changelog".
29 | /// - expected: The expected outcome printed in the console.
30 | /// - Throws: The error occured while executing the command.
31 | func AssertExecuteCommand(_ command: String, expected: String, file: StaticString = #file, line: UInt = #line) throws {
32 | let output = try executeCommand(command)
33 | AssertEqualStringsIgnoringTrailingWhitespace(expected, output, file: file, line: line)
34 | }
35 |
36 | @discardableResult
37 | func executeCommand(_ command: String) throws -> String {
38 | let splitCommand = command.split(separator: " ")
39 | let arguments = splitCommand.dropFirst().map(String.init)
40 |
41 | var gitBuddyCommand = try GitBuddy.parseAsRoot(arguments)
42 |
43 | var output = ""
44 | Log.pipe = { message in
45 | output += message
46 | }
47 |
48 | try gitBuddyCommand.run()
49 |
50 | return output
51 | }
52 | }
53 |
54 | extension Substring {
55 | func trimmed() -> Substring {
56 | guard let i = lastIndex(where: { $0 != " " }) else {
57 | return ""
58 | }
59 | return self[...i]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | fatalError("Running tests like this is unsupported. Run the tests again by using `swift test --enable-test-discovery`")
2 |
--------------------------------------------------------------------------------
/fastlane/.gitignore:
--------------------------------------------------------------------------------
1 | installer/
2 | test_output/
3 | README.md
4 | report.xml
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # Fastlane requirements
2 | fastlane_version "1.109.0"
3 |
4 | import "./../Submodules/WeTransfer-iOS-CI/Fastlane/testing_lanes.rb"
5 | import "./../Submodules/WeTransfer-iOS-CI/Fastlane/shared_lanes.rb"
6 |
7 | desc "Run the tests and prepare for Danger"
8 | lane :test do |options|
9 | test_package(
10 | package_name: 'GitBuddy',
11 | package_path: ENV['PWD'],
12 | device: nil,
13 | destination: "platform=macOS"
14 | )
15 | end
16 |
17 | desc "Updates the local version before creating the regular release"
18 | lane :create_tag_release do |options|
19 | update_version
20 | release_from_tag
21 | end
22 |
23 | desc "Updates the version number to match the releasing tag"
24 | lane :update_version do
25 | tag_name = ENV["BITRISE_GIT_TAG"]
26 |
27 | file_path = "../Sources/GitBuddyCore/Commands/GitBuddy.swift"
28 | text = File.read(file_path)
29 | new_contents = text.gsub(/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(\..*)?/, tag_name)
30 | File.open(file_path, "w") {|file| file.puts new_contents }
31 | end
32 |
--------------------------------------------------------------------------------