Skip to content

Commit 1513888

Browse files
Support apps in non-root locations (#40)
* Allow passing in a filename transformer * attempt to pull out git wrapper * Easy PR comments (rename, types, comments) * comment * add some tests * Fix sorbet, increment version * Fix the last cop Ended up pulling some functions private to get around the max 100 lines cop.
1 parent 9c1b404 commit 1513888

File tree

11 files changed

+353
-139
lines changed

11 files changed

+353
-139
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ GIT
1717
PATH
1818
remote: .
1919
specs:
20-
danger-packwerk (0.14.1)
20+
danger-packwerk (0.14.2)
2121
code_ownership
2222
danger-plugin-api (~> 1.0)
2323
packwerk

lib/danger-packwerk/danger_package_todo_yml_changes.rb

Lines changed: 21 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,25 @@ class DangerPackageTodoYmlChanges < Danger::Plugin
2929
offenses_formatter: T.nilable(Update::OffensesFormatter),
3030
before_comment: BeforeComment,
3131
max_comments: Integer,
32-
violation_types: T::Array[String]
32+
violation_types: T::Array[String],
33+
root_path: T.nilable(String)
3334
).void
3435
end
3536
def check(
3637
offenses_formatter: nil,
3738
before_comment: DEFAULT_BEFORE_COMMENT,
3839
max_comments: DEFAULT_MAX_COMMENTS,
39-
violation_types: DEFAULT_VIOLATION_TYPES
40+
violation_types: DEFAULT_VIOLATION_TYPES,
41+
root_path: nil
4042
)
4143
offenses_formatter ||= Update::DefaultFormatter.new
4244
repo_link = github.pr_json[:base][:repo][:html_url]
4345
org_name = github.pr_json[:base][:repo][:owner][:login]
4446

45-
changed_package_todo_ymls = (git.modified_files + git.added_files + git.deleted_files).grep(PACKAGE_TODO_PATTERN)
47+
git_filesystem = Private::GitFilesystem.new(git: git, root: root_path || '')
48+
changed_package_todo_ymls = (git_filesystem.modified_files + git_filesystem.added_files + git_filesystem.deleted_files).grep(PACKAGE_TODO_PATTERN)
4649

47-
violation_diff = get_violation_diff(violation_types)
50+
violation_diff = get_violation_diff(violation_types, root_path: root_path)
4851

4952
before_comment.call(
5053
violation_diff,
@@ -61,130 +64,30 @@ def check(
6164
markdown(
6265
offenses_formatter.format_offenses(violations, repo_link, org_name),
6366
line: location.line_number,
64-
file: location.file
67+
file: git_filesystem.convert_to_filesystem(location.file)
6568
)
6669

6770
current_comment_count += 1
6871
end
6972
end
7073

71-
sig { params(violation_types: T::Array[String]).returns(ViolationDiff) }
72-
def get_violation_diff(violation_types)
73-
added_violations = T.let([], T::Array[BasicReferenceOffense])
74-
removed_violations = T.let([], T::Array[BasicReferenceOffense])
75-
76-
git.added_files.grep(PACKAGE_TODO_PATTERN).each do |added_package_todo_yml_file|
77-
# Since the file is added, we know on the base commit there are no violations related to this pack,
78-
# and that all violations from this file are new
79-
added_violations += BasicReferenceOffense.from(added_package_todo_yml_file)
80-
end
81-
82-
git.deleted_files.grep(PACKAGE_TODO_PATTERN).each do |deleted_package_todo_yml_file|
83-
# Since the file is deleted, we know on the HEAD commit there are no violations related to this pack,
84-
# and that all violations from this file are deleted
85-
deleted_violations = get_violations_before_patch_for(deleted_package_todo_yml_file)
86-
removed_violations += deleted_violations
87-
end
88-
89-
# The format for git.renamed_files is a T::Array[{after: "some/path/new", before: "some/path/old"}]
90-
renamed_files_before = git.renamed_files.map { |before_after_file| before_after_file[:before] }
91-
renamed_files_after = git.renamed_files.map { |before_after_file| before_after_file[:after] }
92-
93-
git.modified_files.grep(PACKAGE_TODO_PATTERN).each do |modified_package_todo_yml_file|
94-
# We skip over modified files if one of the modified files is a renamed `package_todo.yml` file.
95-
# This allows us to rename packs while ignoring "new violations" in those renamed packs.
96-
next if renamed_files_before.include?(modified_package_todo_yml_file)
97-
98-
head_commit_violations = BasicReferenceOffense.from(modified_package_todo_yml_file)
99-
base_commit_violations = get_violations_before_patch_for(modified_package_todo_yml_file)
100-
added_violations += head_commit_violations - base_commit_violations
101-
removed_violations += base_commit_violations - head_commit_violations
102-
end
103-
104-
#
105-
# This implementation creates some false negatives:
106-
# That is – it doesn't capture some cases:
107-
# 1) A file has been renamed without renaming a constant.
108-
# That can happen if we change only the autoloaded portion of a filename.
109-
# For example: `packs/foo/app/services/my_class.rb` (defines: `MyClass`)
110-
# is changed to `packs/foo/app/public/my_class.rb` (still defines: `MyClass`)
111-
#
112-
# This implementation also doesn't cover these false positives:
113-
# That is – it leaves a comment when it should not.
114-
# 1) A CONSTANT within a class or module has been renamed.
115-
# e.g. `class MyClass; MY_CONSTANT = 1; end` becomes `class MyClass; RENAMED_CONSTANT = 1; end`
116-
# We would not detect based on file renames that `MY_CONSTANT` has been renamed.
117-
#
118-
renamed_constants = []
119-
120-
added_violations.each do |violation|
121-
filepath_that_defines_this_constant = Private.constant_resolver.resolve(violation.class_name)&.location
122-
renamed_constants << violation.class_name if renamed_files_after.include?(filepath_that_defines_this_constant)
123-
end
124-
125-
relevant_added_violations = added_violations.reject do |violation|
126-
renamed_files_after.include?(violation.file) ||
127-
renamed_constants.include?(violation.class_name) ||
128-
!violation_types.include?(violation.type)
129-
end
74+
sig do
75+
params(
76+
violation_types: T::Array[String],
77+
root_path: T.nilable(String)
78+
).returns(ViolationDiff)
79+
end
80+
def get_violation_diff(violation_types, root_path: nil)
81+
git_filesystem = Private::GitFilesystem.new(git: git, root: root_path || '')
13082

131-
relevant_removed_violations = removed_violations.select do |violation|
132-
violation_types.include?(violation.type)
133-
end
83+
added_violations, removed_violations = Private::TodoYmlChanges.get_reference_offenses(
84+
violation_types, git_filesystem
85+
)
13486

13587
ViolationDiff.new(
136-
added_violations: relevant_added_violations,
137-
removed_violations: relevant_removed_violations
88+
added_violations: added_violations,
89+
removed_violations: removed_violations
13890
)
13991
end
140-
141-
private
142-
143-
sig { params(package_todo_yml_file: String).returns(T::Array[BasicReferenceOffense]) }
144-
def get_violations_before_patch_for(package_todo_yml_file)
145-
# The strategy to get the violations before this PR is to reverse the patch on each `package_todo.yml`.
146-
# A previous strategy attempted to use `git merge-base --fork-point`, but there are many situations where it returns
147-
# empty values. That strategy is fickle because it depends on the state of the `reflog` within the CI suite, which appears
148-
# to not be reliable to depend on.
149-
#
150-
# Instead, just inverting the patch should hopefully provide a more reliable way to figure out what was the state of the file before
151-
# the PR without needing to use git commands that interpret the branch history based on local git history.
152-
#
153-
# We apply the patch to the original file so that we can seamlessly reverse the patch applied to that file (since patches are coupled to
154-
# the files they modify). After parsing the violations from that `package_todo.yml` file with the patch reversed,
155-
# we use a temporary copy of the original file to rewrite to it with the original contents.
156-
# Note that practically speaking, we don't need to rewrite the original contents (since we already fetched the
157-
# original contents above and the CI file system should be ephemeral). However, we do this anyways in case we later change these
158-
# assumptions, or another client's environment is different and expects these files not to be mutated.
159-
160-
# Keep track of the original file contents. If the original file has been deleted, then we delete the file after inverting the patch at the end, rather than rewriting it.
161-
package_todo_yml_file_copy = (File.read(package_todo_yml_file) if File.exist?(package_todo_yml_file))
162-
163-
Tempfile.create do |patch_file|
164-
# Normally we'd use `git.diff_for_file(package_todo_yml_file).patch` here, but there is a bug where it does not work for deleted files yet.
165-
# I have a fix for that here: https://github.com/danger/danger/pull/1357
166-
# Until that lands, I'm just using the underlying implementation of that method to get the diff for a file.
167-
# Note that I might want to use a safe escape operator, `&.patch` and return gracefully if the patch cannot be found.
168-
# However I'd be interested in why that ever happens, so for now going to proceed as is.
169-
# (Note that better yet we'd have observability into these so I can just log under those circumstances rather than surfacing an error to the user,
170-
# but we don't have that quite yet.)
171-
patch_for_file = git.diff[package_todo_yml_file].patch
172-
# This appears to be a known issue that patches require new lines at the end. It seems like this is an issue with Danger that
173-
# it gives us a patch without a newline.
174-
# https://stackoverflow.com/questions/18142870/git-error-fatal-corrupt-patch-at-line-36
175-
patch_file << "#{patch_for_file}\n"
176-
patch_file.rewind
177-
# https://git-scm.com/docs/git-apply
178-
_stdout, _stderr, _status = Open3.capture3("git apply --reverse #{patch_file.path}")
179-
# https://www.rubyguides.com/2019/05/ruby-tempfile/
180-
BasicReferenceOffense.from(package_todo_yml_file)
181-
end
182-
ensure
183-
if package_todo_yml_file_copy
184-
File.write(package_todo_yml_file, package_todo_yml_file_copy)
185-
else
186-
File.delete(package_todo_yml_file)
187-
end
188-
end
18992
end
19093
end

lib/danger-packwerk/danger_packwerk.rb

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require 'parse_packwerk'
77
require 'sorbet-runtime'
88
require 'danger-packwerk/packwerk_wrapper'
9+
require 'danger-packwerk/private/git'
910

1011
module DangerPackwerk
1112
# Note that Danger names the plugin (i.e. anything that inherits from `Danger::Plugin`) by taking the name of the class and gsubbing out "Danger"
@@ -43,7 +44,8 @@ class CommentGroupingStrategy < ::T::Enum
4344
failure_message: String,
4445
on_failure: OnFailure,
4546
violation_types: T::Array[String],
46-
grouping_strategy: CommentGroupingStrategy
47+
grouping_strategy: CommentGroupingStrategy,
48+
root_path: T.nilable(String)
4749
).void
4850
end
4951
def check(
@@ -53,7 +55,8 @@ def check(
5355
failure_message: DEFAULT_FAILURE_MESSAGE,
5456
on_failure: DEFAULT_ON_FAILURE,
5557
violation_types: DEFAULT_VIOLATION_TYPES,
56-
grouping_strategy: CommentGroupingStrategy::PerConstantPerLocation
58+
grouping_strategy: CommentGroupingStrategy::PerConstantPerLocation,
59+
root_path: nil
5760
)
5861
offenses_formatter ||= Check::DefaultFormatter.new
5962
repo_link = github.pr_json[:base][:repo][:html_url]
@@ -67,9 +70,12 @@ def check(
6770
# trigger the warning message, which is good, since we only want to trigger on new code.
6871
github.dismiss_out_of_range_messages
6972

73+
git_filesystem = Private::GitFilesystem.new(git: git, root: root_path || '')
74+
7075
# https://github.com/danger/danger/blob/eca19719d3e585fe1cc46bc5377f9aa955ebf609/lib/danger/danger_core/plugins/dangerfile_git_plugin.rb#L80
71-
renamed_files_after = git.renamed_files.map { |f| f[:after] }
72-
targeted_files = (git.modified_files + git.added_files + renamed_files_after).select do |f|
76+
renamed_files_after = git_filesystem.renamed_files.map { |f| f[:after] }
77+
78+
targeted_files = (git_filesystem.modified_files + git_filesystem.added_files + renamed_files_after).select do |f|
7379
path = Pathname.new(f)
7480

7581
# We probably want to check the `include` key of `packwerk.yml`. By default, this value is "**/*.{rb,rake,erb}",
@@ -92,7 +98,7 @@ def check(
9298

9399
packwerk_reference_offenses = PackwerkWrapper.get_offenses_for_files(targeted_files.to_a).compact
94100

95-
renamed_files = git.renamed_files.map { |before_after_file| before_after_file[:after] }
101+
renamed_files = git_filesystem.renamed_files.map { |before_after_file| before_after_file[:after] }
96102

97103
packwerk_reference_offenses_to_care_about = packwerk_reference_offenses.reject do |packwerk_reference_offense|
98104
constant_name = packwerk_reference_offense.reference.constant.name
@@ -131,8 +137,7 @@ def check(
131137
referencing_file = reference_offense.reference.relative_path
132138

133139
message = offenses_formatter.format_offenses(unique_packwerk_reference_offenses, repo_link, org_name)
134-
135-
markdown(message, file: referencing_file, line: line_number)
140+
markdown(message, file: git_filesystem.convert_to_filesystem(referencing_file), line: line_number)
136141
end
137142

138143
if current_comment_count > 0

lib/danger-packwerk/private.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# typed: strict
22

33
require 'danger-packwerk/private/ownership_information'
4+
require 'danger-packwerk/private/todo_yml_changes'
45
require 'constant_resolver'
56

67
module DangerPackwerk

lib/danger-packwerk/private/git.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# typed: strict
2+
3+
require 'code_ownership'
4+
require 'packs'
5+
6+
# In order to support running danger-packwerk from a non-root filepath, we need
7+
# to wrap some git functions in filesystem wrappers: packwerk runs relative to
8+
# the rails app root, whereas git returns paths on the actual filesystem.
9+
module DangerPackwerk
10+
module Private
11+
class GitFilesystem < T::Struct
12+
extend T::Sig
13+
14+
const :git, Danger::DangerfileGitPlugin
15+
const :root, String
16+
17+
sig { returns(T::Array[{ after: String, before: String }]) }
18+
def renamed_files
19+
@git.renamed_files.map do |f|
20+
{
21+
after: convert_file_from_filesystem(f[:after]),
22+
before: convert_file_from_filesystem(f[:before])
23+
}
24+
end
25+
end
26+
27+
sig { returns(T::Array[String]) }
28+
def modified_files
29+
convert_from_filesystem(@git.modified_files.to_a)
30+
end
31+
32+
sig { returns(T::Array[String]) }
33+
def deleted_files
34+
convert_from_filesystem(@git.deleted_files.to_a)
35+
end
36+
37+
sig { returns(T::Array[String]) }
38+
def added_files
39+
convert_from_filesystem(@git.added_files.to_a)
40+
end
41+
42+
sig { params(filename_on_disk: String).returns(::Git::Diff::DiffFile) }
43+
def diff(filename_on_disk)
44+
@git.diff[filename_on_disk]
45+
end
46+
47+
sig { params(path: String).returns(String) }
48+
def convert_to_filesystem(path)
49+
Pathname(@root).join(path).to_s
50+
end
51+
52+
private
53+
54+
sig { params(files: T::Array[String]).returns(T::Array[String]) }
55+
def convert_from_filesystem(files)
56+
files.map { |f| convert_file_from_filesystem(f) }
57+
end
58+
59+
sig { params(file: String).returns(String) }
60+
def convert_file_from_filesystem(file)
61+
Pathname(file).relative_path_from(@root).to_s
62+
end
63+
end
64+
end
65+
end

0 commit comments

Comments
 (0)