├── .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 | ![](Assets/issue_comment.png) 107 | 108 | **A comment posted on a pull request** 109 | ![](Assets/pr_comment.png) 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 | --------------------------------------------------------------------------------