danger-dangermattic 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.buildkite/gem-push.sh +15 -0
- data/.buildkite/pipeline.yml +69 -0
- data/.bundle/config +2 -0
- data/.github/workflows/reusable-check-labels-on-issues.yml +91 -0
- data/.github/workflows/reusable-run-danger.yml +54 -0
- data/.gitignore +30 -0
- data/.rubocop.yml +67 -0
- data/.ruby-version +1 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +191 -0
- data/Guardfile +21 -0
- data/LICENSE +373 -0
- data/README.md +68 -0
- data/Rakefile +24 -0
- data/danger-dangermattic.gemspec +58 -0
- data/lib/danger_dangermattic.rb +3 -0
- data/lib/danger_plugin.rb +4 -0
- data/lib/dangermattic/gem_version.rb +5 -0
- data/lib/dangermattic/plugins/android_release_checker.rb +50 -0
- data/lib/dangermattic/plugins/android_strings_checker.rb +31 -0
- data/lib/dangermattic/plugins/android_unit_test_checker.rb +187 -0
- data/lib/dangermattic/plugins/common/common_release_checker.rb +113 -0
- data/lib/dangermattic/plugins/common/git_utils.rb +166 -0
- data/lib/dangermattic/plugins/common/github_utils.rb +68 -0
- data/lib/dangermattic/plugins/common/reporter.rb +38 -0
- data/lib/dangermattic/plugins/ios_release_checker.rb +106 -0
- data/lib/dangermattic/plugins/labels_checker.rb +74 -0
- data/lib/dangermattic/plugins/manifest_pr_checker.rb +106 -0
- data/lib/dangermattic/plugins/milestone_checker.rb +98 -0
- data/lib/dangermattic/plugins/podfile_checker.rb +122 -0
- data/lib/dangermattic/plugins/pr_size_checker.rb +125 -0
- data/lib/dangermattic/plugins/tracks_checker.rb +72 -0
- data/lib/dangermattic/plugins/view_changes_checker.rb +46 -0
- data/spec/android_release_checker_spec.rb +93 -0
- data/spec/android_strings_checker_spec.rb +185 -0
- data/spec/android_unit_test_checker_spec.rb +343 -0
- data/spec/common_release_checker_spec.rb +70 -0
- data/spec/fixtures/android_unit_test_checker/Abc.java +7 -0
- data/spec/fixtures/android_unit_test_checker/AbcFeatureConfig.java +7 -0
- data/spec/fixtures/android_unit_test_checker/Abcdef.kt +5 -0
- data/spec/fixtures/android_unit_test_checker/AbcdefgViewHelper.java +7 -0
- data/spec/fixtures/android_unit_test_checker/AnotherViewHelper.kt +7 -0
- data/spec/fixtures/android_unit_test_checker/MyNewClass.java +7 -0
- data/spec/fixtures/android_unit_test_checker/Polygon.kt +3 -0
- data/spec/fixtures/android_unit_test_checker/Shape.kt +10 -0
- data/spec/fixtures/android_unit_test_checker/TestsINeedThem.java +5 -0
- data/spec/fixtures/android_unit_test_checker/TestsINeedThem.kt +7 -0
- data/spec/fixtures/android_unit_test_checker/TestsINeedThem2.kt +12 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/activities/MyActivity.kt +7 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/activities/MyJavaActivity.java +7 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/fragments/MyFragment.kt +6 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/fragments/MyNewJavaFragment.java +7 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/module/MyModule.java +13 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/view/ActionCardViewHolder.kt +22 -0
- data/spec/fixtures/android_unit_test_checker/src/android/java/org/view/MyRecyclerView.java +7 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/AbcTests.java +5 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/AnotherTestClass.java +7 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/PolygonTest.kt +4 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/TestMyNewClass.java +9 -0
- data/spec/fixtures/android_unit_test_checker/src/androidTest/java/org/test/ToolTest.kt +5 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/org/wordpress/android/widgets/NestedWebView.kt +32 -0
- data/spec/fixtures/android_unit_test_checker/src/main/java/org/wordpress/util/config/BloggingPromptsFeatureConfig.kt +23 -0
- data/spec/fixtures/android_unit_test_checker/src/test/java/org/test/TestsINeedThem.java +9 -0
- data/spec/github_utils_spec.rb +110 -0
- data/spec/ios_release_checker_spec.rb +191 -0
- data/spec/labels_checker_spec.rb +169 -0
- data/spec/manifest_pr_checker_spec.rb +140 -0
- data/spec/milestone_checker_spec.rb +222 -0
- data/spec/podfile_checker_spec.rb +595 -0
- data/spec/pr_size_checker_spec.rb +250 -0
- data/spec/spec_helper.rb +115 -0
- data/spec/tracks_checker_spec.rb +156 -0
- data/spec/view_changes_checker_spec.rb +168 -0
- metadata +341 -0
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
# This plugin provides methods to check for the presence of unit tests for newly added classes in a pull request.
|
5
|
+
#
|
6
|
+
# @example Check missing unit tests using the default parameters:
|
7
|
+
# android_unit_test_checker.check_missing_tests
|
8
|
+
#
|
9
|
+
# @example Check missing unit tests while excluding certain classes, subclasses, and paths:
|
10
|
+
# android_unit_test_checker.check_missing_tests(
|
11
|
+
# classes_exceptions: [/ViewHolder$/],
|
12
|
+
# subclasses_exceptions: [/RecyclerView/],
|
13
|
+
# path_exceptions: ['*.java', 'org/app/ui/**']
|
14
|
+
# )
|
15
|
+
#
|
16
|
+
# @example Check missing unit tests with a custom bypass label:
|
17
|
+
# android_unit_test_checker.check_missing_tests(bypass_label: 'BypassTestCheck')
|
18
|
+
#
|
19
|
+
# @example Check missing unit tests excluding certain classes, subclasses and paths:
|
20
|
+
# android_unit_test_checker.check_missing_tests(classes_exceptions: [/ViewHolder$/], subclasses_exceptions: [/RecyclerView/], path_exceptions: ['*.java', 'org/app/ui/**'])
|
21
|
+
#
|
22
|
+
# @see Automattic/dangermattic
|
23
|
+
# @tags android, unit test, github, pull request
|
24
|
+
#
|
25
|
+
class AndroidUnitTestChecker < Plugin
|
26
|
+
ANY_CLASS_DETECTOR = /class\s+([A-Z]\w+)\s*(.*?)\s*{/m
|
27
|
+
NON_PRIVATE_CLASS_DETECTOR = /(?:\s|public|internal|protected|final|abstract|static)*class\s+([A-Z]\w+)\s*(.*?)\s*{/m
|
28
|
+
DEFAULT_CLASSES_EXCEPTIONS = [
|
29
|
+
/ViewHolder$/,
|
30
|
+
/Module$/,
|
31
|
+
/Button$/
|
32
|
+
].freeze
|
33
|
+
|
34
|
+
DEFAULT_SUBCLASSES_EXCEPTIONS = [
|
35
|
+
/(Fragment|Activity)\b/,
|
36
|
+
/RecyclerView/,
|
37
|
+
/^BroadcastReceiver$/,
|
38
|
+
/^ContentProvider$/,
|
39
|
+
/Service$/,
|
40
|
+
/View$/,
|
41
|
+
/ViewGroup$/,
|
42
|
+
/Layout$/
|
43
|
+
].freeze
|
44
|
+
|
45
|
+
DEFAULT_UNIT_TESTS_BYPASS_PR_LABEL = 'unit-tests-exemption'
|
46
|
+
|
47
|
+
# Check and warns about missing unit tests for a Git diff, with optional classes/subclasses to ignore and an
|
48
|
+
# optional PR label to bypass the checks.
|
49
|
+
#
|
50
|
+
# @param classes_exceptions [Array<String>] Optional list of regexes matching class names to exclude from the
|
51
|
+
# check.
|
52
|
+
# Defaults to DEFAULT_CLASSES_EXCEPTIONS.
|
53
|
+
# @param subclasses_exceptions [Array<String>] Optional list of regexes matching base class names to exclude from
|
54
|
+
# the check.
|
55
|
+
# Defaults to DEFAULT_SUBCLASSES_EXCEPTIONS.
|
56
|
+
# @param path_exceptions [Array<String>] Optional list of file paths to exclude from the check.
|
57
|
+
# Defaults to [].
|
58
|
+
# @param bypass_label [String] Optional label to indicate we can bypass the check. Defaults to
|
59
|
+
# DEFAULT_UNIT_TESTS_BYPASS_PR_LABEL.
|
60
|
+
#
|
61
|
+
# @return [void]
|
62
|
+
def check_missing_tests(classes_exceptions: DEFAULT_CLASSES_EXCEPTIONS,
|
63
|
+
subclasses_exceptions: DEFAULT_SUBCLASSES_EXCEPTIONS,
|
64
|
+
path_exceptions: [],
|
65
|
+
bypass_label: DEFAULT_UNIT_TESTS_BYPASS_PR_LABEL)
|
66
|
+
list = find_classes_missing_tests(
|
67
|
+
git_diff: git.diff,
|
68
|
+
classes_exceptions: classes_exceptions,
|
69
|
+
subclasses_exceptions: subclasses_exceptions,
|
70
|
+
path_exceptions: path_exceptions
|
71
|
+
)
|
72
|
+
|
73
|
+
return if list.empty?
|
74
|
+
|
75
|
+
if danger.github.pr_labels.include?(bypass_label)
|
76
|
+
list.each do |c|
|
77
|
+
warn("Class `#{c.classname}` is missing tests, but `#{bypass_label}` label was set to ignore this.")
|
78
|
+
end
|
79
|
+
else
|
80
|
+
list.each do |c|
|
81
|
+
failure("Please add tests for class `#{c.classname}` (or add `#{bypass_label}` label to ignore this).")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
ClassViolation = Struct.new(:classname, :file)
|
89
|
+
|
90
|
+
# @param git_diff [Git::Diff] the git diff object
|
91
|
+
# @param classes_exceptions [Array<String>] Regexes matching class names to exclude from the check.
|
92
|
+
# @param subclasses_exceptions [Array<String>] Regexes matching base class names to exclude from the check
|
93
|
+
# @param path_exceptions [Array<String>] Regexes matching base class names to exclude from the check
|
94
|
+
#
|
95
|
+
# @return [Array<ClassViolation>] An array of `ClassViolation` objects for each added class that is missing a test
|
96
|
+
def find_classes_missing_tests(git_diff:, classes_exceptions:, subclasses_exceptions:, path_exceptions:)
|
97
|
+
violations = []
|
98
|
+
removed_classes = []
|
99
|
+
added_test_lines = []
|
100
|
+
|
101
|
+
# Parse the diff of each file, storing test lines for test files, and added/removed classes for non-test files
|
102
|
+
git_diff.each do |file_diff|
|
103
|
+
file_path = file_diff.path
|
104
|
+
|
105
|
+
next if path_exceptions.any? { |exception| File.fnmatch?(exception, file_path) }
|
106
|
+
|
107
|
+
if test_file?(path: file_path)
|
108
|
+
# Store added test lines from test files
|
109
|
+
added_test_lines += file_diff.patch.each_line.select do |line|
|
110
|
+
git_utils.change_type(diff_line: line) == :added
|
111
|
+
end
|
112
|
+
else
|
113
|
+
# Detect added classes (violations) and removed classes in non-test files
|
114
|
+
patch = file_diff.patch
|
115
|
+
|
116
|
+
violations += find_violations(
|
117
|
+
path: file_path,
|
118
|
+
diff_patch: patch,
|
119
|
+
classes_exceptions: classes_exceptions,
|
120
|
+
subclasses_exceptions: subclasses_exceptions
|
121
|
+
)
|
122
|
+
|
123
|
+
removed_classes += find_removed_classes(diff_patch: patch)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# We only want newly added classes, not if class signature was modified or line was moved
|
128
|
+
violations.reject! { |v| removed_classes.include?(v.classname) }
|
129
|
+
|
130
|
+
# For each remaining candidate, only keep the ones _not_ used in a new test.
|
131
|
+
# The regex will match usages of this class in any test file
|
132
|
+
violations.select { |v| added_test_lines.none? { |line| line =~ /\b#{v.classname}\b/ } }
|
133
|
+
end
|
134
|
+
|
135
|
+
# Finds added classes that potentially will require a test (violations) in the given file based on the changes in the diff patch.
|
136
|
+
#
|
137
|
+
# @param path [String] The file in the diff to check for violations.
|
138
|
+
# @param diff_patch [String] The diff patch containing the changes to the file.
|
139
|
+
# @param classes_exceptions [Array<String>] An array of class names that are exceptions and should be ignored.
|
140
|
+
# @param subclasses_exceptions [Array<String>] An array of class names whose subclasses should be ignored as well.
|
141
|
+
#
|
142
|
+
# @return [Array<ClassViolation>] An array of ClassViolation objects representing the violations found.
|
143
|
+
def find_violations(path:, diff_patch:, classes_exceptions:, subclasses_exceptions:)
|
144
|
+
added_lines = git_utils.added_lines(diff_patch: diff_patch)
|
145
|
+
matches = added_lines.scan(NON_PRIVATE_CLASS_DETECTOR)
|
146
|
+
matches.reject! do |m|
|
147
|
+
class_match_is_exception?(
|
148
|
+
m,
|
149
|
+
path,
|
150
|
+
classes_exceptions,
|
151
|
+
subclasses_exceptions
|
152
|
+
)
|
153
|
+
end
|
154
|
+
|
155
|
+
matches.map { |m| ClassViolation.new(m[0], path) }
|
156
|
+
end
|
157
|
+
|
158
|
+
# Finds the names of removed classes based on the removals the diff patch.
|
159
|
+
#
|
160
|
+
# @param diff_patch [String] The diff patch containing the changes to the file.
|
161
|
+
#
|
162
|
+
# @return [Array<String>] An array with the class names of the classes that were removed in the diff.
|
163
|
+
def find_removed_classes(diff_patch:)
|
164
|
+
removed_lines = git_utils.removed_lines(diff_patch: diff_patch)
|
165
|
+
matches = removed_lines.scan(ANY_CLASS_DETECTOR)
|
166
|
+
matches.map { |m| m[0] }
|
167
|
+
end
|
168
|
+
|
169
|
+
# @param match [Array<String>] match an array of captured substrings matching our `*_CLASS_DETECTOR` for a given line
|
170
|
+
# @param file [String] file the path to the file where that class declaration line was matched
|
171
|
+
# @param classes_exceptions [Array<String>] Regexes matching class names to exclude from the check.
|
172
|
+
# @param subclasses_exceptions [Array<String>] Regexes matching base class names to exclude from the check
|
173
|
+
#
|
174
|
+
# @return [void]
|
175
|
+
def class_match_is_exception?(match, file, classes_exceptions, subclasses_exceptions)
|
176
|
+
return true if classes_exceptions.any? { |re| match[0] =~ re }
|
177
|
+
|
178
|
+
subclass_regexp = File.extname(file) == '.java' ? /extends\s+([A-Z]\w+)/m : /\s*:\s*([A-Z]\w+)/m
|
179
|
+
subclass = match[1].scan(subclass_regexp)&.last&.last
|
180
|
+
subclasses_exceptions.any? { |re| subclass =~ re }
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_file?(path:)
|
184
|
+
path.match? %r{/(test|androidTest).*\.(java|kt)$}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
# Plugin to perform generic checks related to releases.
|
5
|
+
# It can be used directly or via the specialised plugins `AndroidReleaseChecker` and `IosReleaseChecker`.
|
6
|
+
#
|
7
|
+
# @example Checking if a specific file has changed on a release branch:
|
8
|
+
# common_release_checker.check_file_changed(
|
9
|
+
# file_comparison: ->(path) { path == 'metadata/full_release_notes.txt' },
|
10
|
+
# message: 'Release notes have been modified on a release branch.',
|
11
|
+
# on_release_branch: true
|
12
|
+
# )
|
13
|
+
#
|
14
|
+
# @example Checking if release notes and store strings have changed:
|
15
|
+
# common_release_checker.check_release_notes_and_store_strings(
|
16
|
+
# release_notes_file: 'metadata/release_notes.txt',
|
17
|
+
# po_file: 'metadata/PlayStoreStrings.po'
|
18
|
+
# )
|
19
|
+
#
|
20
|
+
# @example Checking for changes in internal release notes:
|
21
|
+
# common_release_checker.check_internal_release_notes_changed
|
22
|
+
#
|
23
|
+
# @see Automattic/dangermattic
|
24
|
+
# @tags util, process, release
|
25
|
+
#
|
26
|
+
class CommonReleaseChecker < Plugin
|
27
|
+
DEFAULT_INTERNAL_RELEASE_NOTES = 'RELEASE-NOTES.txt'
|
28
|
+
|
29
|
+
MESSAGE_STORE_FILE_NOT_CHANGED = 'The `%s` file should be updated if the editorialized release notes file `%s` is being changed.'
|
30
|
+
MESSAGE_INTERNAL_RELEASE_NOTES_CHANGED = <<~WARNING
|
31
|
+
This PR contains changes to `%s`.
|
32
|
+
Note that these changes won't affect the final version of the release notes as this version is in code freeze.
|
33
|
+
Please, get in touch with a release manager if you want to update the final release notes.
|
34
|
+
WARNING
|
35
|
+
|
36
|
+
# Check if certain files have been modified, returning a warning or failure message based on the branch type.
|
37
|
+
#
|
38
|
+
# @param file_comparison [Proc] Function used to compare modified file paths.
|
39
|
+
# It should take a single argument, which is the path to a modified file,
|
40
|
+
# and return true if the file matches the desired condition.
|
41
|
+
# Example: `file_comparison = ->(file_path) { file_path.include?('app/') }`
|
42
|
+
#
|
43
|
+
# @param message [String] The message to display in the warning or failure output if the condition is met.
|
44
|
+
#
|
45
|
+
# @param on_release_branch [Boolean] If true, the check will only run on release branches, otherwise on non-release branches.
|
46
|
+
#
|
47
|
+
# @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
|
48
|
+
#
|
49
|
+
# @example Check if any modified file is under the 'app/' directory and emit a warning on release branches:
|
50
|
+
# check_file_changed(file_comparison: ->(file_path) { file_path.include?('app/') },
|
51
|
+
# message: 'Some files in the "app/" directory have been modified. Please review the changes.',
|
52
|
+
# on_release_branch: true)
|
53
|
+
#
|
54
|
+
# @example Check if a specific file has been modified and emit a failure on non-release branches:
|
55
|
+
# check_file_changed(file_comparison: ->(file_path) { file_path == 'path/to/file/DoNotChange.java' },
|
56
|
+
# message: 'The "DoNotChange.java" file has been modified. This change is not allowed on non-release branches.',
|
57
|
+
# on_release_branch: false,
|
58
|
+
# report_type: :error)
|
59
|
+
#
|
60
|
+
# @return [void]
|
61
|
+
def check_file_changed(file_comparison:, message:, on_release_branch:, report_type: :warning)
|
62
|
+
has_modified_file = git_utils.all_changed_files.any?(&file_comparison)
|
63
|
+
|
64
|
+
should_be_changed = (on_release_branch == github_utils.release_branch?)
|
65
|
+
return unless should_be_changed && has_modified_file
|
66
|
+
|
67
|
+
reporter.report(message: message, type: report_type)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check if the release notes and store strings files are correctly updated after a modification to the release notes.
|
71
|
+
#
|
72
|
+
# @param release_notes_file [String] The name of the release notes file that should be checked for modifications.
|
73
|
+
# Example: 'metadata/release_notes.txt'
|
74
|
+
#
|
75
|
+
# @param po_file [String] The name of the store strings file that should be checked for modifications.
|
76
|
+
# Example: 'metadata/PlayStoreStrings.po'
|
77
|
+
#
|
78
|
+
# @example Check if the release notes file 'release_notes.txt' is modified, and the 'PlayStoreStrings.po' file is not modified, posting a message:
|
79
|
+
# check_release_notes_and_store_strings(release_notes_file: 'release_notes.txt', po_file: 'PlayStoreStrings.po')
|
80
|
+
#
|
81
|
+
# @return [void]
|
82
|
+
def check_release_notes_and_store_strings(release_notes_file:, po_file:)
|
83
|
+
has_modified_release_notes = danger.git.modified_files.any? { |f| f == release_notes_file }
|
84
|
+
has_modified_app_store_strings = danger.git.modified_files.any? { |f| f == po_file }
|
85
|
+
|
86
|
+
return unless has_modified_release_notes && !has_modified_app_store_strings
|
87
|
+
|
88
|
+
message(format(MESSAGE_STORE_FILE_NOT_CHANGED, po_file, release_notes_file))
|
89
|
+
end
|
90
|
+
|
91
|
+
# Check if there are changes to the internal release notes file in the release branch and emit a warning if that's the case.
|
92
|
+
#
|
93
|
+
# @param release_notes_file [String] (optional) The path to the internal release notes file.
|
94
|
+
# Defaults to the `DEFAULT_INTERNAL_RELEASE_NOTES` constant if not provided.
|
95
|
+
# @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
|
96
|
+
#
|
97
|
+
# @example Checking for changes in the default internal release notes file:
|
98
|
+
# check_internal_release_notes_changed
|
99
|
+
#
|
100
|
+
# @example Checking for changes in a custom internal release notes file at a specific path:
|
101
|
+
# check_internal_release_notes_changed(release_notes_file: '/path/to/internal_release_notes.txt')
|
102
|
+
#
|
103
|
+
# @return [void]
|
104
|
+
def check_internal_release_notes_changed(release_notes_file: DEFAULT_INTERNAL_RELEASE_NOTES, report_type: :warning)
|
105
|
+
check_file_changed(
|
106
|
+
file_comparison: ->(path) { path == release_notes_file },
|
107
|
+
message: format(MESSAGE_INTERNAL_RELEASE_NOTES_CHANGED, release_notes_file),
|
108
|
+
on_release_branch: true,
|
109
|
+
report_type: report_type
|
110
|
+
)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
# Represents a utility plugin for working with Git: to check added and modified lines in Git diffs,
|
5
|
+
# to determine the type of change (added, removed, or other) in a diff line, and to retrieve lists of
|
6
|
+
# added, modified, and deleted files.
|
7
|
+
#
|
8
|
+
# @example Check if there is a "TODO" in Ruby files:
|
9
|
+
# git_utils.check_added_diff_lines(
|
10
|
+
# file_selector: ->(path) { path.end_with?('.rb') },
|
11
|
+
# line_matcher: ->(line) { line.include?('TODO') },
|
12
|
+
# message: 'Found a TODO in a Ruby file'
|
13
|
+
# )
|
14
|
+
#
|
15
|
+
# @example Get added lines from a diff patch:
|
16
|
+
# added_lines = git_utils.added_lines(diff_patch: diff_patch)
|
17
|
+
#
|
18
|
+
# @example Get removed lines from a diff patch:
|
19
|
+
# removed_lines = git_utils.removed_lines(diff_patch: diff_patch)
|
20
|
+
#
|
21
|
+
# @example Determining the change type of a diff line:
|
22
|
+
# git_utils.change_type(diff_line: "+ new line added")
|
23
|
+
# #=> :added
|
24
|
+
#
|
25
|
+
# git_utils.change_type(diff_line: "- line removed")
|
26
|
+
# #=> :removed
|
27
|
+
#
|
28
|
+
# git_utils.change_type(diff_line: " context line")
|
29
|
+
# #=> :other
|
30
|
+
#
|
31
|
+
# @example Select removed lines from a diff patch:
|
32
|
+
# removed_lines = git_utils.select_lines(diff_patch: diff_patch, change_type: :removed)
|
33
|
+
#
|
34
|
+
# @see Automattic/dangermattic
|
35
|
+
# @tags tool, util, git
|
36
|
+
#
|
37
|
+
class GitUtils < Plugin
|
38
|
+
# Check added lines in a PR for a specific pattern and issue a warning or failure message when found.
|
39
|
+
#
|
40
|
+
# @param file_selector [Proc] A block to select the files in the PR.
|
41
|
+
# The block should take a file path as input and return true if the file should be checked.
|
42
|
+
#
|
43
|
+
# @param line_matcher [Proc] A block that will select the diff lines to report.
|
44
|
+
# The block should take a line of text as input and return true if the line matches the pattern.
|
45
|
+
#
|
46
|
+
# @param message [String] The warning or failure message to display when the pattern is found in a line.
|
47
|
+
#
|
48
|
+
# @param report_type [Symbol] (optional) Type of report (:error, :warning, :message) whenever a line matches the criteria. Default is :warning.
|
49
|
+
#
|
50
|
+
# @example Checking for added lines containing 'FIXME' and failing the build:
|
51
|
+
# check_added_diff_lines(file_selector: ->(path) { File.extname(path) == ('.swift') }, line_matcher: ->(line) { line.include?("FIXME") }, message: "A FIXME was added, failing build.", report_type: :error)
|
52
|
+
#
|
53
|
+
# @return [void]
|
54
|
+
def check_added_diff_lines(file_selector:, line_matcher:, message:, report_type: :warning)
|
55
|
+
modified_files = added_and_modified_files.select(&file_selector)
|
56
|
+
|
57
|
+
matches = matching_lines_in_diff_files(
|
58
|
+
files: modified_files,
|
59
|
+
line_matcher: line_matcher,
|
60
|
+
change_type: :added
|
61
|
+
)
|
62
|
+
|
63
|
+
matches.each do |match|
|
64
|
+
match.lines.each do |line|
|
65
|
+
final_message = <<~MESSAGE
|
66
|
+
#{message}
|
67
|
+
File `#{match.file}`:
|
68
|
+
```diff
|
69
|
+
#{line.chomp}
|
70
|
+
```
|
71
|
+
MESSAGE
|
72
|
+
|
73
|
+
reporter.report(message: final_message, type: report_type)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
MatchedData = Struct.new(:file, :lines)
|
79
|
+
|
80
|
+
# Matches diff lines in the provided files based on the line matcher and change type
|
81
|
+
#
|
82
|
+
# @param files [Array<String>] List of file names to check
|
83
|
+
# @param line_matcher [Proc] A callable that takes a line and returns true if it matches the desired pattern
|
84
|
+
# @param change_type [Symbol, nil] Change type to filter lines (e.g., :added, :removed) or nil for no filter
|
85
|
+
# @return [Array<MatchedData>] Array of MatchedData objects representing matched lines in files
|
86
|
+
def matching_lines_in_diff_files(files:, line_matcher:, change_type: nil)
|
87
|
+
matched_data = []
|
88
|
+
|
89
|
+
files.each do |file|
|
90
|
+
matched_lines = []
|
91
|
+
|
92
|
+
diff = danger.git.diff_for_file(file)
|
93
|
+
|
94
|
+
diff.patch.each_line do |line|
|
95
|
+
matched_lines << line if line_matcher.call(line) && (change_type.nil? || change_type(diff_line: line) == change_type)
|
96
|
+
end
|
97
|
+
|
98
|
+
matched_data << MatchedData.new(file, matched_lines) unless matched_lines.empty?
|
99
|
+
end
|
100
|
+
|
101
|
+
matched_data
|
102
|
+
end
|
103
|
+
|
104
|
+
# Determine the type of change for a given line in a git diff.
|
105
|
+
#
|
106
|
+
# @param diff_line [String] The line from a git diff that needs to be classified.
|
107
|
+
#
|
108
|
+
# @return [Symbol] The type of change for the given diff line. Possible values are:
|
109
|
+
# - :added for added lines
|
110
|
+
# - :removed for removed lines
|
111
|
+
# - :other for any other type of lines
|
112
|
+
def change_type(diff_line:)
|
113
|
+
if diff_line.start_with?('+') && !diff_line.start_with?('+++ ')
|
114
|
+
:added
|
115
|
+
elsif diff_line.start_with?('-') && !diff_line.start_with?('--- ')
|
116
|
+
:removed
|
117
|
+
else
|
118
|
+
:other
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Get the list of added and modified files in the current Pull Request.
|
123
|
+
#
|
124
|
+
# @return [Array<String>] An array containing the file paths of added and modified files.
|
125
|
+
def added_and_modified_files
|
126
|
+
danger.git.added_files + danger.git.modified_files
|
127
|
+
end
|
128
|
+
|
129
|
+
# Get the list of all files added, modified and deleted in the current Pull Request.
|
130
|
+
#
|
131
|
+
# @return [Array<String>] An array containing the file paths of all changed files.
|
132
|
+
#
|
133
|
+
def all_changed_files
|
134
|
+
danger.git.added_files + danger.git.modified_files + danger.git.deleted_files
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns the lines that were added in the given diff patch.
|
138
|
+
#
|
139
|
+
# @param diff_patch [String] The diff patch containing the changes.
|
140
|
+
#
|
141
|
+
# @return [String] A concatenated string of added lines.
|
142
|
+
def added_lines(diff_patch:)
|
143
|
+
select_lines(diff_patch: diff_patch, change_type: :added)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns the lines that were removed in the given diff patch.
|
147
|
+
#
|
148
|
+
# @param diff_patch [String] The diff patch containing the changes.
|
149
|
+
#
|
150
|
+
# @return [String] A concatenated string of removed lines.
|
151
|
+
def removed_lines(diff_patch:)
|
152
|
+
select_lines(diff_patch: diff_patch, change_type: :removed)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Selects lines of a specific change type (added or removed) from the given diff patch.
|
156
|
+
#
|
157
|
+
# @param diff_patch [String] The diff patch containing the changes.
|
158
|
+
# @param change_type [Symbol] The desired change type (:added or :removed).
|
159
|
+
#
|
160
|
+
# @return [String] A concatenated string of selected lines of the specified change type.
|
161
|
+
def select_lines(diff_patch:, change_type:)
|
162
|
+
selected_lines = diff_patch.lines.select { |line| change_type(diff_line: line) == change_type }
|
163
|
+
selected_lines.map { |line| line[1..] }.join
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
# Provides common GitHub utility methods related to Pull Requests in a Danger context.
|
5
|
+
#
|
6
|
+
# @example Checking if the branch is a release or hotfix branch:
|
7
|
+
# github_utils.release_branch? #=> true or false
|
8
|
+
#
|
9
|
+
# @example Checking if there are active reviewers on the PR:
|
10
|
+
# github_utils.active_reviewers? #=> true or false
|
11
|
+
#
|
12
|
+
# @example Checking if there are requested teams or reviewers on the PR:
|
13
|
+
# github_utils.requested_reviewers? #=> true or false
|
14
|
+
#
|
15
|
+
# @example Checking if the branch is a main branch (trunk, main, master, or develop):
|
16
|
+
# github_utils.main_branch? #=> true or false
|
17
|
+
#
|
18
|
+
# @example Checking if the PR is a work-in-progress (WIP) based on labels or title:
|
19
|
+
# github_utils.wip_feature? #=> true or false
|
20
|
+
#
|
21
|
+
# @see Automattic/dangermattic
|
22
|
+
# @tags tool, util
|
23
|
+
#
|
24
|
+
class GithubUtils < Plugin
|
25
|
+
# Checks if there are active reviewers providing feedback and potentially changing the state of the PR
|
26
|
+
# (e.g., approved, changes requested).
|
27
|
+
#
|
28
|
+
# @return [Boolean] True if there are active reviewers, otherwise false.
|
29
|
+
def active_reviewers?
|
30
|
+
repo_name = github.pr_json['base']['repo']['full_name']
|
31
|
+
pr_number = github.pr_json['number']
|
32
|
+
|
33
|
+
!github.api.pull_request_reviews(repo_name, pr_number).empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks if there are requested teams or reviewers who haven't reacted yet.
|
37
|
+
#
|
38
|
+
# @return [Boolean] True if there are requested teams or reviewers, otherwise false.
|
39
|
+
def requested_reviewers?
|
40
|
+
has_requested_reviews = !github.pr_json['requested_teams'].to_a.empty? || !github.pr_json['requested_reviewers'].to_a.empty?
|
41
|
+
has_requested_reviews || active_reviewers?
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks if the current branch is a main branch (trunk, main, master, or develop).
|
45
|
+
#
|
46
|
+
# @return [Boolean] True if the branch is a main branch, otherwise false.
|
47
|
+
def main_branch?
|
48
|
+
%w[trunk main master develop].include?(github.branch_for_base)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Checks if the current branch is a release or hotfix branch.
|
52
|
+
#
|
53
|
+
# @return [Boolean] True if the branch is a release or hotfix branch, otherwise false.
|
54
|
+
def release_branch?
|
55
|
+
github.branch_for_base.start_with?('release/') || github.branch_for_base.start_with?('hotfix/')
|
56
|
+
end
|
57
|
+
|
58
|
+
# Checks if the PR is a work-in-progress (WIP) based on labels or title.
|
59
|
+
#
|
60
|
+
# @return [Boolean] True if the PR is a work-in-progress, otherwise false.
|
61
|
+
def wip_feature?
|
62
|
+
has_wip_label = github.pr_labels.any? { |label| label.include?('WIP') }
|
63
|
+
has_wip_title = github.pr_title.include?('WIP')
|
64
|
+
|
65
|
+
has_wip_label || has_wip_title
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
# Handles reporting messages in the context of Danger, allowing the generation of
|
5
|
+
# warnings, errors, and simple messages in a Danger run.
|
6
|
+
#
|
7
|
+
# @example Reporting a Warning
|
8
|
+
# reporter.report(message: "This is a warning message", type: :warning)
|
9
|
+
#
|
10
|
+
# @example Reporting an Error
|
11
|
+
# reporter.report(message: "This is an error message", type: :error)
|
12
|
+
#
|
13
|
+
# @see Automattic/dangermattic
|
14
|
+
# @tags tool, util, danger
|
15
|
+
#
|
16
|
+
class Reporter < Plugin
|
17
|
+
# Report a message to be posted by Danger as an error (failing the build), a warning or a simple message.
|
18
|
+
#
|
19
|
+
# @param message [String] The message to be reported to Danger.
|
20
|
+
# @param type [Symbol] The type of report. Possible values:
|
21
|
+
# - :warning (default): Reports a warning.
|
22
|
+
# - :error: Reports an error.
|
23
|
+
# - :message: Reports a simple message.
|
24
|
+
# - Any other value or nil: Takes no action.
|
25
|
+
#
|
26
|
+
# @return [void]
|
27
|
+
def report(message:, type: :warning)
|
28
|
+
case type
|
29
|
+
when :error
|
30
|
+
failure(message)
|
31
|
+
when :warning
|
32
|
+
warn(message)
|
33
|
+
when :message
|
34
|
+
danger.message(message)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
# Plugin for performing iOS / macOS release-related checks in a pull request.
|
5
|
+
#
|
6
|
+
# @example Checking for changes in Core Data models on a release branch:
|
7
|
+
# ios_release_checker.check_core_data_model_changed
|
8
|
+
#
|
9
|
+
# @example Checking for modified Localizable.strings on a regular branch:
|
10
|
+
# ios_release_checker.check_modified_localizable_strings
|
11
|
+
#
|
12
|
+
# @example Checking for synchronization between release notes and App Store strings:
|
13
|
+
# ios_release_checker.check_release_notes_and_app_store_strings
|
14
|
+
#
|
15
|
+
# @see Automattic/dangermattic
|
16
|
+
# @tags ios, macos, process, release
|
17
|
+
#
|
18
|
+
class IosReleaseChecker < Plugin
|
19
|
+
LOCALIZABLE_STRINGS_FILE = 'Localizable.strings'
|
20
|
+
BASE_STRINGS_FILE = "en.lproj/#{LOCALIZABLE_STRINGS_FILE}".freeze
|
21
|
+
|
22
|
+
MESSAGE_STRINGS_FILE_UPDATED = "The `#{LOCALIZABLE_STRINGS_FILE}` files should only be updated on release branches, when the translations are downloaded by our automation.".freeze
|
23
|
+
MESSAGE_BASE_STRINGS_FILE_UPDATED = "The `#{BASE_STRINGS_FILE}` file should only be updated before creating a release branch.".freeze
|
24
|
+
MESSAGE_TRANSLATION_FILE_UPDATED = "Translation files `*.lproj/#{LOCALIZABLE_STRINGS_FILE}` should only be updated on a release branch.".freeze
|
25
|
+
MESSAGE_CORE_DATA_UPDATED = 'Do not edit an existing Core Data model in a release branch unless it hasn\'t been released to testers yet. ' \
|
26
|
+
'Instead create a new model version and merge back to develop soon.'
|
27
|
+
|
28
|
+
# Checks if an existing Core Data model has been edited in a release branch.
|
29
|
+
#
|
30
|
+
# @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
|
31
|
+
#
|
32
|
+
# @return [void]
|
33
|
+
def check_core_data_model_changed(report_type: :warning)
|
34
|
+
common_release_checker.check_file_changed(
|
35
|
+
file_comparison: ->(path) { File.extname(path) == '.xcdatamodeld' },
|
36
|
+
message: MESSAGE_CORE_DATA_UPDATED,
|
37
|
+
on_release_branch: true,
|
38
|
+
report_type: report_type
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Checks if any Localizable.strings file has been modified on a release branch, otherwise reporting a warning.
|
43
|
+
#
|
44
|
+
# @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
|
45
|
+
#
|
46
|
+
# @return [void]
|
47
|
+
def check_modified_localizable_strings_on_release(report_type: :warning)
|
48
|
+
common_release_checker.check_file_changed(
|
49
|
+
file_comparison: ->(path) { File.basename(path) == LOCALIZABLE_STRINGS_FILE },
|
50
|
+
message: MESSAGE_STRINGS_FILE_UPDATED,
|
51
|
+
on_release_branch: false,
|
52
|
+
report_type: report_type
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Checks if the en.lproj/Localizable.strings file has been modified on a regular branch, otherwise reporting a warning.
|
57
|
+
#
|
58
|
+
# @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
|
59
|
+
#
|
60
|
+
# @return [void]
|
61
|
+
def check_modified_en_strings_on_regular_branch(report_type: :warning)
|
62
|
+
common_release_checker.check_file_changed(
|
63
|
+
file_comparison: ->(path) { base_strings_file?(path: path) },
|
64
|
+
message: MESSAGE_BASE_STRINGS_FILE_UPDATED,
|
65
|
+
on_release_branch: true,
|
66
|
+
report_type: report_type
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Checks if a translation file (*.lproj/Localizable.strings) has been modified on a release branch, otherwise reporting a warning.
|
71
|
+
#
|
72
|
+
# @param report_type [Symbol] (optional) The type of report for the message. Types: :error, :warning (default), :message.
|
73
|
+
#
|
74
|
+
# @return [void]
|
75
|
+
def check_modified_translations_on_release_branch(report_type: :warning)
|
76
|
+
common_release_checker.check_file_changed(
|
77
|
+
file_comparison: ->(path) { !base_strings_file?(path: path) && File.basename(path) == LOCALIZABLE_STRINGS_FILE },
|
78
|
+
message: MESSAGE_TRANSLATION_FILE_UPDATED,
|
79
|
+
on_release_branch: false,
|
80
|
+
report_type: report_type
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Checks if changes made to the release notes are also followed by changes in the App Store strings file.
|
85
|
+
#
|
86
|
+
# @return [void]
|
87
|
+
def check_release_notes_and_app_store_strings
|
88
|
+
common_release_checker.check_release_notes_and_store_strings(
|
89
|
+
release_notes_file: 'Resources/release_notes.txt',
|
90
|
+
po_file: 'Resources/AppStoreStrings.po'
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Checks if a given path corresponds to the base (English) strings file, en.lproj/Localizable.strings.
|
97
|
+
#
|
98
|
+
# @return [Boolean] true if path is the base strings file
|
99
|
+
def base_strings_file?(path:)
|
100
|
+
base_strings_path_components = Pathname.new(BASE_STRINGS_FILE).each_filename.to_a
|
101
|
+
path_components = Pathname.new(path).each_filename.to_a
|
102
|
+
|
103
|
+
base_strings_path_components == path_components.last(base_strings_path_components.length)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|