1 /*
2 * Copyright (C) 2014, Andrey Loskutov <loskutov@gmx.de>
3 * and other copyright owners as documented in the project's IP log.
4 *
5 * This program and the accompanying materials are made available
6 * under the terms of the Eclipse Distribution License v1.0 which
7 * accompanies this distribution, is reproduced below, and is
8 * available at http://www.eclipse.org/org/documents/edl-v10.php
9 *
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or
13 * without modification, are permitted provided that the following
14 * conditions are met:
15 *
16 * - Redistributions of source code must retain the above copyright
17 * notice, this list of conditions and the following disclaimer.
18 *
19 * - Redistributions in binary form must reproduce the above
20 * copyright notice, this list of conditions and the following
21 * disclaimer in the documentation and/or other materials provided
22 * with the distribution.
23 *
24 * - Neither the name of the Eclipse Foundation, Inc. nor the
25 * names of its contributors may be used to endorse or promote
26 * products derived from this software without specific prior
27 * written permission.
28 *
29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 */
43 package org.eclipse.jgit.ignore;
44
45 import static org.eclipse.jgit.ignore.internal.IMatcher.NO_MATCH;
46 import static org.eclipse.jgit.ignore.internal.Strings.isDirectoryPattern;
47 import static org.eclipse.jgit.ignore.internal.Strings.stripTrailing;
48 import static org.eclipse.jgit.ignore.internal.Strings.stripTrailingWhitespace;
49
50 import org.eclipse.jgit.errors.InvalidPatternException;
51 import org.eclipse.jgit.ignore.internal.IMatcher;
52 import org.eclipse.jgit.ignore.internal.PathMatcher;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 /**
57 * "Fast" (compared with IgnoreRule) git ignore rule implementation supporting
58 * also double star {@code **} pattern.
59 * <p>
60 * This class is immutable and thread safe.
61 *
62 * @since 3.6
63 */
64 public class FastIgnoreRule {
65 private final static Logger LOG = LoggerFactory
66 .getLogger(FastIgnoreRule.class);
67
68 /**
69 * Character used as default path separator for ignore entries
70 */
71 public static final char PATH_SEPARATOR = '/';
72
73 private final IMatcher matcher;
74
75 private final boolean inverse;
76
77 private final boolean dirOnly;
78
79 /**
80 * Constructor for FastIgnoreRule
81 *
82 * @param pattern
83 * ignore pattern as described in <a href=
84 * "https://www.kernel.org/pub/software/scm/git/docs/gitignore.html"
85 * >git manual</a>. If pattern is invalid or is not a pattern
86 * (comment), this rule doesn't match anything.
87 */
88 public FastIgnoreRule(String pattern) {
89 if (pattern == null)
90 throw new IllegalArgumentException("Pattern must not be null!"); //$NON-NLS-1$
91 if (pattern.length() == 0) {
92 dirOnly = false;
93 inverse = false;
94 this.matcher = NO_MATCH;
95 return;
96 }
97 inverse = pattern.charAt(0) == '!';
98 if (inverse) {
99 pattern = pattern.substring(1);
100 if (pattern.length() == 0) {
101 dirOnly = false;
102 this.matcher = NO_MATCH;
103 return;
104 }
105 }
106 if (pattern.charAt(0) == '#') {
107 this.matcher = NO_MATCH;
108 dirOnly = false;
109 return;
110 }
111 if (pattern.charAt(0) == '\\' && pattern.length() > 1) {
112 char next = pattern.charAt(1);
113 if (next == '!' || next == '#') {
114 // remove backslash escaping first special characters
115 pattern = pattern.substring(1);
116 }
117 }
118 dirOnly = isDirectoryPattern(pattern);
119 if (dirOnly) {
120 pattern = stripTrailingWhitespace(pattern);
121 pattern = stripTrailing(pattern, PATH_SEPARATOR);
122 if (pattern.length() == 0) {
123 this.matcher = NO_MATCH;
124 return;
125 }
126 }
127 IMatcher m;
128 try {
129 m = PathMatcher.createPathMatcher(pattern,
130 Character.valueOf(PATH_SEPARATOR), dirOnly);
131 } catch (InvalidPatternException e) {
132 m = NO_MATCH;
133 LOG.error(e.getMessage(), e);
134 }
135 this.matcher = m;
136 }
137
138 /**
139 * Returns true if a match was made. <br>
140 * This function does NOT return the actual ignore status of the target!
141 * Please consult {@link #getResult()} for the negation status. The actual
142 * ignore status may be true or false depending on whether this rule is an
143 * ignore rule or a negation rule.
144 *
145 * @param path
146 * Name pattern of the file, relative to the base directory of
147 * this rule
148 * @param directory
149 * Whether the target file is a directory or not
150 * @return True if a match was made. This does not necessarily mean that the
151 * target is ignored. Call {@link #getResult() getResult()} for the
152 * result.
153 */
154 public boolean isMatch(String path, boolean directory) {
155 return isMatch(path, directory, false);
156 }
157
158 /**
159 * Returns true if a match was made. <br>
160 * This function does NOT return the actual ignore status of the target!
161 * Please consult {@link #getResult()} for the negation status. The actual
162 * ignore status may be true or false depending on whether this rule is an
163 * ignore rule or a negation rule.
164 *
165 * @param path
166 * Name pattern of the file, relative to the base directory of
167 * this rule
168 * @param directory
169 * Whether the target file is a directory or not
170 * @param pathMatch
171 * {@code true} if the match is for the full path: see
172 * {@link IMatcher#matches(String, int, int)}
173 * @return True if a match was made. This does not necessarily mean that the
174 * target is ignored. Call {@link #getResult() getResult()} for the
175 * result.
176 * @since 4.11
177 */
178 public boolean isMatch(String path, boolean directory, boolean pathMatch) {
179 if (path == null)
180 return false;
181 if (path.length() == 0)
182 return false;
183 boolean match = matcher.matches(path, directory, pathMatch);
184 return match;
185 }
186
187 /**
188 * Whether the pattern is just a file name and not a path
189 *
190 * @return {@code true} if the pattern is just a file name and not a path
191 */
192 public boolean getNameOnly() {
193 return !(matcher instanceof PathMatcher);
194 }
195
196 /**
197 * Whether the pattern should match directories only
198 *
199 * @return {@code true} if the pattern should match directories only
200 */
201 public boolean dirOnly() {
202 return dirOnly;
203 }
204
205 /**
206 * Indicates whether the rule is non-negation or negation.
207 *
208 * @return True if the pattern had a "!" in front of it
209 */
210 public boolean getNegation() {
211 return inverse;
212 }
213
214 /**
215 * Indicates whether the rule is non-negation or negation.
216 *
217 * @return True if the target is to be ignored, false otherwise.
218 */
219 public boolean getResult() {
220 return !inverse;
221 }
222
223 /**
224 * Whether the rule never matches
225 *
226 * @return {@code true} if the rule never matches (comment line or broken
227 * pattern)
228 * @since 4.1
229 */
230 public boolean isEmpty() {
231 return matcher == NO_MATCH;
232 }
233
234 /** {@inheritDoc} */
235 @Override
236 public String toString() {
237 StringBuilder sb = new StringBuilder();
238 if (inverse)
239 sb.append('!');
240 sb.append(matcher);
241 if (dirOnly)
242 sb.append(PATH_SEPARATOR);
243 return sb.toString();
244
245 }
246
247 /** {@inheritDoc} */
248 @Override
249 public int hashCode() {
250 final int prime = 31;
251 int result = 1;
252 result = prime * result + (inverse ? 1231 : 1237);
253 result = prime * result + (dirOnly ? 1231 : 1237);
254 result = prime * result + ((matcher == null) ? 0 : matcher.hashCode());
255 return result;
256 }
257
258 /** {@inheritDoc} */
259 @Override
260 public boolean equals(Object obj) {
261 if (this == obj)
262 return true;
263 if (!(obj instanceof FastIgnoreRule))
264 return false;
265
266 FastIgnoreRule other = (FastIgnoreRule) obj;
267 if (inverse != other.inverse)
268 return false;
269 if (dirOnly != other.dirOnly)
270 return false;
271 return matcher.equals(other.matcher);
272 }
273 }