All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openrewrite.ExcludeFileFromGitignore Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2024 the original author or authors.
 * 

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.openrewrite; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Value; import org.jspecify.annotations.Nullable; import org.openrewrite.internal.StringUtils; import org.openrewrite.jgit.ignore.FastIgnoreRule; import org.openrewrite.jgit.ignore.IgnoreNode; import org.openrewrite.text.PlainText; import org.openrewrite.text.PlainTextVisitor; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.*; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.toList; import static org.apache.commons.lang3.StringUtils.join; import static org.openrewrite.ExcludeFileFromGitignore.Repository; import static org.openrewrite.PathUtils.separatorsToUnix; import static org.openrewrite.jgit.ignore.IgnoreNode.MatchResult.*; @Value @EqualsAndHashCode(callSuper = false) public class ExcludeFileFromGitignore extends ScanningRecipe { @Option(displayName = "Paths", description = "The paths to find and remove from the gitignore files.", example = "/folder/file.txt") List paths; @Override public String getDisplayName() { return "Remove ignoral of files or directories from .gitignore"; } @Override public String getDescription() { return "This recipe will remove a file or directory from the .gitignore file. " + "If the file or directory is already in the .gitignore file, it will be removed or negated. " + "If the file or directory is not in the .gitignore file, no action will be taken."; } @Override public Repository getInitialValue(ExecutionContext ctx) { return new Repository(); } @Override public TreeVisitor getScanner(Repository acc) { return Preconditions.check(new FindSourceFiles("**/.gitignore"), new PlainTextVisitor() { @Override public PlainText visitText(PlainText text, ExecutionContext ctx) { try { acc.addGitignoreFile(text); } catch (IOException e) { throw new RecipeException("Failed to parse the .gitignore file", e); } return super.visitText(text, ctx); } }); } @Override public Collection generate(Repository acc, ExecutionContext ctx) { for (String path : paths) { acc.exclude(path); } return emptyList(); } @Override public TreeVisitor getVisitor(Repository acc) { return Preconditions.check(new FindSourceFiles("**/.gitignore"), new PlainTextVisitor() { @Override public PlainText visitText(PlainText text, ExecutionContext ctx) { CustomIgnoreNode ignoreNode = acc.rules.get(asGitignoreFileLocation(text)); if (ignoreNode != null) { String separator = text.getText().contains("\r\n") ? "\r\n" : "\n"; List newRules = ignoreNode.getRules().stream().map(IgnoreRule::getText).collect(toList()); String[] currentContent = text.getText().split(separator); text = text.withText(join(sortRules(currentContent, newRules), separator)); } return text; } private List sortRules(String[] originalRules, List newRules) { LinkedList results = new LinkedList<>(); Arrays.stream(originalRules).filter(line -> { if (StringUtils.isBlank(line) || line.startsWith("#")) { return true; } return newRules.stream().anyMatch(line::equalsIgnoreCase); }).forEach(results::add); int resultsIndexCurrentlyAt = 0; for (String newRule : newRules) { List resultsSubList = results.subList(resultsIndexCurrentlyAt, results.size()); if (resultsSubList.stream().noneMatch(rule -> rule.equalsIgnoreCase(newRule))) { if (resultsIndexCurrentlyAt >= results.size()) { results.add(newRule); } else { results.add(resultsIndexCurrentlyAt, newRule); } } else { resultsIndexCurrentlyAt += resultsSubList.indexOf(newRule); } resultsIndexCurrentlyAt++; } return distinctValuesStartingReversed(results); } private List distinctValuesStartingReversed(List list) { LinkedList filteredList = new LinkedList<>(); ListIterator iterator = list.listIterator(list.size()); while (iterator.hasPrevious()) { String previous = iterator.previous(); if (StringUtils.isBlank(previous) || previous.startsWith("#") || !filteredList.contains(previous)) { filteredList.addFirst(previous); } } return filteredList; } }); } public static class Repository { private final Map rules = new HashMap<>(); public void exclude(String path) { path = separatorsToUnix(path); String normalizedPath = path.startsWith("/") ? path : "/" + path; List impactingFiles = rules.keySet() .stream() .filter(k -> normalizedPath.toLowerCase().startsWith(k.toLowerCase())) .sorted(comparingInt(String::length).reversed()) .collect(toList()); for (String impactingFile : impactingFiles) { CustomIgnoreNode ignoreNode = rules.get(impactingFile); String nestedPath = normalizedPath.substring(impactingFile.length() - 1); while (IGNORED == ignoreNode.isIgnored(nestedPath)) { List existingRules = ignoreNode.getRules(); LinkedHashSet remainingRules = new LinkedHashSet<>(); for (int i = existingRules.size() - 1; i > -1; i--) { IgnoreRule rule = existingRules.get(i); remainingRules.addAll(rule.negateIfNecessary(nestedPath)); } ArrayList ignoreRules = new ArrayList<>(remainingRules); Collections.reverse(ignoreRules); ignoreNode = new CustomIgnoreNode(ignoreRules, ignoreNode.getPath()); if (ignoreRules.size() == existingRules.size()) { break; } } rules.put(impactingFile, ignoreNode); if (CHECK_PARENT == ignoreNode.isIgnored(nestedPath)) { continue; } // There is already an ignore rule for the path, so not needed to check parent rules. break; } } public void addGitignoreFile(PlainText text) throws IOException { CustomIgnoreNode ignoreNode = CustomIgnoreNode.of(text); rules.put(ignoreNode.path, ignoreNode); } } @Getter private static class CustomIgnoreNode { private final List rules; private final String path; public CustomIgnoreNode(List rules, String path) { this.rules = rules.stream().map(IgnoreRule::new).collect(toList()); this.path = path; } static CustomIgnoreNode of(PlainText text) throws IOException { String gitignoreFileName = asGitignoreFileLocation(text); IgnoreNode ignoreNode = new IgnoreNode(); ignoreNode.parse(gitignoreFileName, new ByteArrayInputStream(text.getText().getBytes())); return new CustomIgnoreNode(ignoreNode.getRules(), gitignoreFileName); } public IgnoreNode.MatchResult isIgnored(String path) { for (int i = rules.size() - 1; i > -1; i--) { IgnoreRule rule = rules.get(i); if (rule.isMatch(path)) { if (rule.getResult()) { return IGNORED; } else { return NOT_IGNORED; } } } return CHECK_PARENT; } } private static class IgnoreRule { private final FastIgnoreRule rule; @Getter private final String text; public IgnoreRule(FastIgnoreRule rule) { this.rule = rule; this.text = rule.toString(); } public boolean isMatch(String path) { return rule.isMatch(path, true, false) || rule.isMatch(path, true, true); } public boolean getResult() { return rule.getResult(); } public List negateIfNecessary(String nestedPath) { if (!isMatch(nestedPath) || !getResult()) { // If this rule has nothing to do with the path to remove, we keep it. // OR if this rule is a negation, we keep it. return Collections.singletonList(rule); } else if (text.equals(nestedPath)) { // If this rule is an exact match to the path to remove, we remove it. return emptyList(); } else if (isMatch(nestedPath)) { if (text.contains("*")) { return getWildcardRules(nestedPath); } if (("/" + text).equals(nestedPath)) { // An entry not starting with a slash, but exact match otherwise needs to be negated using exact path as that leftover entry can match nested paths also. return Arrays.asList(new FastIgnoreRule("!" + nestedPath), rule); } if (!rule.dirOnly()) { // If the rule does not end with a slash, it's a matcher for both filenames and directories, so we must negate it with an exact path. return Arrays.asList(new FastIgnoreRule("!" + nestedPath), rule); } return traversePaths(text, nestedPath, null, null); } // If we still have the rule, we keep it. --> not making changes to an unknown flow. return Collections.singletonList(rule); } @Override public String toString() { return text; } private List getWildcardRules(String nestedPath) { if (!isMatch(nestedPath)) { return singletonList(rule); } if (text.startsWith("!")) { return singletonList(rule); } if (isWildcardedBetween(1, -1) || (splitRuleParts().length > 1 && isWildcardedBetween(0, 1) && isWildcardedBetween(-1, 0))) { // No support for wildcard in the middle of the path (yet?). So, we keep the rule. // No support for wildcards in both beginning and end. return singletonList(rule); } if (!hasOnlyOneWildcardGroup()) { // No support for multiple wildcard groups (yet?). So, we keep the rule. // No support for wildcards + text (yet?). So, we keep the rule. return singletonList(rule); } if (!isFullWildcard()) { return Arrays.asList(new FastIgnoreRule("!" + nestedPath), rule); } String wildcard = "*"; if (text.contains("**")) { wildcard = "**"; } if (isWildcardedBetween(0, 1)) { return traversePaths(text, nestedPath, null, (text.startsWith("/") ? "/" : "") + wildcard); } if (isWildcardedBetween(-1, 0)) { // If the wildcard is at the end of the path, we should negate the rule. return traversePaths(text, nestedPath, wildcard + (text.endsWith("/") ? "/" : ""), null); } // In any other case, we will keep the rule. return singletonList(rule); } private boolean isFullWildcard() { if (!text.contains("*")) { return false; } // only / or empty before and after the wildcard int begin = text.indexOf("*"); int end = text.lastIndexOf("*"); return (begin == 0 || text.charAt(begin - 1) == '/') && (end == text.length() - 1 || text.charAt(end + 1) == '/'); } private boolean hasOnlyOneWildcardGroup() { if (!text.contains("*")) { return false; } int firstWildcard = text.indexOf("*"); int lastWildcard = text.lastIndexOf("*"); return firstWildcard == lastWildcard || lastWildcard - firstWildcard == 1; } private boolean isWildcardedBetween(int start, int end) { if (!text.contains("*")) { return false; } String[] parts = splitRuleParts(); int startIdx = start; if (startIdx < 0) { startIdx = parts.length + start; } int endIdx = end; if (endIdx <= 0) { endIdx = parts.length + end; } for (int i = startIdx; i < endIdx; i++) { if (parts[i].contains("*")) { return true; } } return false; } private String[] splitRuleParts() { String rulePath = text; if (rulePath.startsWith("!")) { rulePath = rulePath.substring(1); } if (rulePath.startsWith("/")) { rulePath = rulePath.substring(1); } if (rulePath.endsWith("/")) { rulePath = rulePath.substring(0, rulePath.length() - 1); } return rulePath.split("/"); } private static List traversePaths(String originalRule, String path, @Nullable String wildcardSuffix, @Nullable String wildcardPrefix) { String rule = originalRule; ArrayList traversedRemainingRules = new ArrayList<>(); if (wildcardSuffix != null && rule.endsWith(wildcardSuffix)) { rule = rule.substring(0, rule.length()-wildcardSuffix.length()); } if (wildcardPrefix != null && rule.startsWith(wildcardPrefix)) { rule = path.substring(0, path.indexOf(rule.substring(wildcardPrefix.length()))) + rule.substring(wildcardPrefix.length()); traversedRemainingRules.add(new FastIgnoreRule(originalRule + (originalRule.endsWith("/") ? "*" : "/*"))); traversedRemainingRules.add(new FastIgnoreRule("!" + rule)); } StringBuilder rulePath = new StringBuilder(rule); String pathToTraverse = path.substring(rule.length()); if (originalRule.contains("*")) { if (pathToTraverse.isEmpty() && wildcardSuffix != null) { return Arrays.asList(new FastIgnoreRule("!" + rule), new FastIgnoreRule(originalRule + (originalRule.endsWith("/") ? "*" : "/*"))); } else if (pathToTraverse.isEmpty() && wildcardPrefix != null) { return Arrays.asList(new FastIgnoreRule("!" + rule), new FastIgnoreRule(originalRule)); } } else { if (pathToTraverse.replace("/", "").isEmpty()) { return Arrays.asList(new FastIgnoreRule("!" + rule), new FastIgnoreRule(originalRule)); } } String pathToSplit = pathToTraverse.startsWith("/") ? pathToTraverse.substring(1) : pathToTraverse; pathToSplit = pathToSplit.endsWith("/") ? pathToSplit.substring(0, pathToSplit.length() - 1) : pathToSplit; String[] splitPath = pathToSplit.split("/"); for (int j = 0; j < splitPath.length; j++) { String s = splitPath[j]; traversedRemainingRules.add(new FastIgnoreRule(rulePath + (wildcardSuffix != null ? wildcardSuffix : "*"))); rulePath.append(s); traversedRemainingRules.add(new FastIgnoreRule("!" + rulePath + (j < splitPath.length - 1 || path.endsWith("/") ? "/" : ""))); rulePath.append("/"); } Collections.reverse(traversedRemainingRules); return traversedRemainingRules; } } private static String asGitignoreFileLocation(PlainText text) { String gitignoreFileName = separatorsToUnix(text.getSourcePath().toString()); gitignoreFileName = gitignoreFileName.startsWith("/") ? gitignoreFileName : "/" + gitignoreFileName; return gitignoreFileName.substring(0, gitignoreFileName.lastIndexOf("/") + 1); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy