View Javadoc
1   /*
2    * Copyright (C) 2008, 2013 Shawn O. Pearce <spearce@spearce.org>
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  
44  package org.eclipse.jgit.transport;
45  
46  import java.io.Serializable;
47  import java.text.MessageFormat;
48  
49  import org.eclipse.jgit.internal.JGitText;
50  import org.eclipse.jgit.lib.Constants;
51  import org.eclipse.jgit.lib.Ref;
52  
53  /**
54   * Describes how refs in one repository copy into another repository.
55   * <p>
56   * A ref specification provides matching support and limited rules to rewrite a
57   * reference in one repository to another reference in another repository.
58   */
59  public class RefSpec implements Serializable {
60  	private static final long serialVersionUID = 1L;
61  
62  	/**
63  	 * Suffix for wildcard ref spec component, that indicate matching all refs
64  	 * with specified prefix.
65  	 */
66  	public static final String WILDCARD_SUFFIX = "/*"; //$NON-NLS-1$
67  
68  	/**
69  	 * Check whether provided string is a wildcard ref spec component.
70  	 *
71  	 * @param s
72  	 *            ref spec component - string to test. Can be null.
73  	 * @return true if provided string is a wildcard ref spec component.
74  	 */
75  	public static boolean isWildcard(final String s) {
76  		return s != null && s.contains("*"); //$NON-NLS-1$
77  	}
78  
79  	/** Does this specification ask for forced updated (rewind/reset)? */
80  	private boolean force;
81  
82  	/** Is this specification actually a wildcard match? */
83  	private boolean wildcard;
84  
85  	/** Name of the ref(s) we would copy from. */
86  	private String srcName;
87  
88  	/** Name of the ref(s) we would copy into. */
89  	private String dstName;
90  
91  	/**
92  	 * Construct an empty RefSpec.
93  	 * <p>
94  	 * A newly created empty RefSpec is not suitable for use in most
95  	 * applications, as at least one field must be set to match a source name.
96  	 */
97  	public RefSpec() {
98  		force = false;
99  		wildcard = false;
100 		srcName = Constants.HEAD;
101 		dstName = null;
102 	}
103 
104 	/**
105 	 * Parse a ref specification for use during transport operations.
106 	 * <p>
107 	 * Specifications are typically one of the following forms:
108 	 * <ul>
109 	 * <li><code>refs/heads/master</code></li>
110 	 * <li><code>refs/heads/master:refs/remotes/origin/master</code></li>
111 	 * <li><code>refs/heads/*:refs/remotes/origin/*</code></li>
112 	 * <li><code>+refs/heads/master</code></li>
113 	 * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li>
114 	 * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li>
115 	 * <li><code>+refs/pull/&#42;/head:refs/remotes/origin/pr/*</code></li>
116 	 * <li><code>:refs/heads/master</code></li>
117 	 * </ul>
118 	 *
119 	 * @param spec
120 	 *            string describing the specification.
121 	 * @throws IllegalArgumentException
122 	 *             the specification is invalid.
123 	 */
124 	public RefSpec(final String spec) {
125 		String s = spec;
126 		if (s.startsWith("+")) { //$NON-NLS-1$
127 			force = true;
128 			s = s.substring(1);
129 		}
130 
131 		final int c = s.lastIndexOf(':');
132 		if (c == 0) {
133 			s = s.substring(1);
134 			if (isWildcard(s))
135 				throw new IllegalArgumentException(MessageFormat.format(JGitText.get().invalidWildcards, spec));
136 			dstName = checkValid(s);
137 		} else if (c > 0) {
138 			String src = s.substring(0, c);
139 			String dst = s.substring(c + 1);
140 			if (isWildcard(src) && isWildcard(dst)) {
141 				// Both contain wildcard
142 				wildcard = true;
143 			} else if (isWildcard(src) || isWildcard(dst)) {
144 				// If either source or destination has wildcard, the other one
145 				// must have as well.
146 				throw new IllegalArgumentException(MessageFormat.format(JGitText.get().invalidWildcards, spec));
147 			}
148 			srcName = checkValid(src);
149 			dstName = checkValid(dst);
150 		} else {
151 			if (isWildcard(s))
152 				throw new IllegalArgumentException(MessageFormat.format(JGitText.get().invalidWildcards, spec));
153 			srcName = checkValid(s);
154 		}
155 	}
156 
157 	private RefSpec(final RefSpec p) {
158 		force = p.isForceUpdate();
159 		wildcard = p.isWildcard();
160 		srcName = p.getSource();
161 		dstName = p.getDestination();
162 	}
163 
164 	/**
165 	 * Check if this specification wants to forcefully update the destination.
166 	 *
167 	 * @return true if this specification asks for updates without merge tests.
168 	 */
169 	public boolean isForceUpdate() {
170 		return force;
171 	}
172 
173 	/**
174 	 * Create a new RefSpec with a different force update setting.
175 	 *
176 	 * @param forceUpdate
177 	 *            new value for force update in the returned instance.
178 	 * @return a new RefSpec with force update as specified.
179 	 */
180 	public RefSpec setForceUpdate(final boolean forceUpdate) {
181 		final RefSpec r = new RefSpec(this);
182 		r.force = forceUpdate;
183 		return r;
184 	}
185 
186 	/**
187 	 * Check if this specification is actually a wildcard pattern.
188 	 * <p>
189 	 * If this is a wildcard pattern then the source and destination names
190 	 * returned by {@link #getSource()} and {@link #getDestination()} will not
191 	 * be actual ref names, but instead will be patterns.
192 	 *
193 	 * @return true if this specification could match more than one ref.
194 	 */
195 	public boolean isWildcard() {
196 		return wildcard;
197 	}
198 
199 	/**
200 	 * Get the source ref description.
201 	 * <p>
202 	 * During a fetch this is the name of the ref on the remote repository we
203 	 * are fetching from. During a push this is the name of the ref on the local
204 	 * repository we are pushing out from.
205 	 *
206 	 * @return name (or wildcard pattern) to match the source ref.
207 	 */
208 	public String getSource() {
209 		return srcName;
210 	}
211 
212 	/**
213 	 * Create a new RefSpec with a different source name setting.
214 	 *
215 	 * @param source
216 	 *            new value for source in the returned instance.
217 	 * @return a new RefSpec with source as specified.
218 	 * @throws IllegalStateException
219 	 *             There is already a destination configured, and the wildcard
220 	 *             status of the existing destination disagrees with the
221 	 *             wildcard status of the new source.
222 	 */
223 	public RefSpec setSource(final String source) {
224 		final RefSpec r = new RefSpec(this);
225 		r.srcName = checkValid(source);
226 		if (isWildcard(r.srcName) && r.dstName == null)
227 			throw new IllegalStateException(JGitText.get().destinationIsNotAWildcard);
228 		if (isWildcard(r.srcName) != isWildcard(r.dstName))
229 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
230 		return r;
231 	}
232 
233 	/**
234 	 * Get the destination ref description.
235 	 * <p>
236 	 * During a fetch this is the local tracking branch that will be updated
237 	 * with the new ObjectId after fetching is complete. During a push this is
238 	 * the remote ref that will be updated by the remote's receive-pack process.
239 	 * <p>
240 	 * If null during a fetch no tracking branch should be updated and the
241 	 * ObjectId should be stored transiently in order to prepare a merge.
242 	 * <p>
243 	 * If null during a push, use {@link #getSource()} instead.
244 	 *
245 	 * @return name (or wildcard) pattern to match the destination ref.
246 	 */
247 	public String getDestination() {
248 		return dstName;
249 	}
250 
251 	/**
252 	 * Create a new RefSpec with a different destination name setting.
253 	 *
254 	 * @param destination
255 	 *            new value for destination in the returned instance.
256 	 * @return a new RefSpec with destination as specified.
257 	 * @throws IllegalStateException
258 	 *             There is already a source configured, and the wildcard status
259 	 *             of the existing source disagrees with the wildcard status of
260 	 *             the new destination.
261 	 */
262 	public RefSpec setDestination(final String destination) {
263 		final RefSpec r = new RefSpec(this);
264 		r.dstName = checkValid(destination);
265 		if (isWildcard(r.dstName) && r.srcName == null)
266 			throw new IllegalStateException(JGitText.get().sourceIsNotAWildcard);
267 		if (isWildcard(r.srcName) != isWildcard(r.dstName))
268 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
269 		return r;
270 	}
271 
272 	/**
273 	 * Create a new RefSpec with a different source/destination name setting.
274 	 *
275 	 * @param source
276 	 *            new value for source in the returned instance.
277 	 * @param destination
278 	 *            new value for destination in the returned instance.
279 	 * @return a new RefSpec with destination as specified.
280 	 * @throws IllegalArgumentException
281 	 *             The wildcard status of the new source disagrees with the
282 	 *             wildcard status of the new destination.
283 	 */
284 	public RefSpec setSourceDestination(final String source,
285 			final String destination) {
286 		if (isWildcard(source) != isWildcard(destination))
287 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
288 		final RefSpec r = new RefSpec(this);
289 		r.wildcard = isWildcard(source);
290 		r.srcName = source;
291 		r.dstName = destination;
292 		return r;
293 	}
294 
295 	/**
296 	 * Does this specification's source description match the ref name?
297 	 *
298 	 * @param r
299 	 *            ref name that should be tested.
300 	 * @return true if the names match; false otherwise.
301 	 */
302 	public boolean matchSource(final String r) {
303 		return match(r, getSource());
304 	}
305 
306 	/**
307 	 * Does this specification's source description match the ref?
308 	 *
309 	 * @param r
310 	 *            ref whose name should be tested.
311 	 * @return true if the names match; false otherwise.
312 	 */
313 	public boolean matchSource(final Ref r) {
314 		return match(r.getName(), getSource());
315 	}
316 
317 	/**
318 	 * Does this specification's destination description match the ref name?
319 	 *
320 	 * @param r
321 	 *            ref name that should be tested.
322 	 * @return true if the names match; false otherwise.
323 	 */
324 	public boolean matchDestination(final String r) {
325 		return match(r, getDestination());
326 	}
327 
328 	/**
329 	 * Does this specification's destination description match the ref?
330 	 *
331 	 * @param r
332 	 *            ref whose name should be tested.
333 	 * @return true if the names match; false otherwise.
334 	 */
335 	public boolean matchDestination(final Ref r) {
336 		return match(r.getName(), getDestination());
337 	}
338 
339 	/**
340 	 * Expand this specification to exactly match a ref name.
341 	 * <p>
342 	 * Callers must first verify the passed ref name matches this specification,
343 	 * otherwise expansion results may be unpredictable.
344 	 *
345 	 * @param r
346 	 *            a ref name that matched our source specification. Could be a
347 	 *            wildcard also.
348 	 * @return a new specification expanded from provided ref name. Result
349 	 *         specification is wildcard if and only if provided ref name is
350 	 *         wildcard.
351 	 */
352 	public RefSpec expandFromSource(final String r) {
353 		return isWildcard() ? new RefSpec(this).expandFromSourceImp(r) : this;
354 	}
355 
356 	private RefSpec expandFromSourceImp(final String name) {
357 		final String psrc = srcName, pdst = dstName;
358 		wildcard = false;
359 		srcName = name;
360 		dstName = expandWildcard(name, psrc, pdst);
361 		return this;
362 	}
363 
364 	/**
365 	 * Expand this specification to exactly match a ref.
366 	 * <p>
367 	 * Callers must first verify the passed ref matches this specification,
368 	 * otherwise expansion results may be unpredictable.
369 	 *
370 	 * @param r
371 	 *            a ref that matched our source specification. Could be a
372 	 *            wildcard also.
373 	 * @return a new specification expanded from provided ref name. Result
374 	 *         specification is wildcard if and only if provided ref name is
375 	 *         wildcard.
376 	 */
377 	public RefSpec expandFromSource(final Ref r) {
378 		return expandFromSource(r.getName());
379 	}
380 
381 	/**
382 	 * Expand this specification to exactly match a ref name.
383 	 * <p>
384 	 * Callers must first verify the passed ref name matches this specification,
385 	 * otherwise expansion results may be unpredictable.
386 	 *
387 	 * @param r
388 	 *            a ref name that matched our destination specification. Could
389 	 *            be a wildcard also.
390 	 * @return a new specification expanded from provided ref name. Result
391 	 *         specification is wildcard if and only if provided ref name is
392 	 *         wildcard.
393 	 */
394 	public RefSpec expandFromDestination(final String r) {
395 		return isWildcard() ? new RefSpec(this).expandFromDstImp(r) : this;
396 	}
397 
398 	private RefSpec expandFromDstImp(final String name) {
399 		final String psrc = srcName, pdst = dstName;
400 		wildcard = false;
401 		srcName = expandWildcard(name, pdst, psrc);
402 		dstName = name;
403 		return this;
404 	}
405 
406 	/**
407 	 * Expand this specification to exactly match a ref.
408 	 * <p>
409 	 * Callers must first verify the passed ref matches this specification,
410 	 * otherwise expansion results may be unpredictable.
411 	 *
412 	 * @param r
413 	 *            a ref that matched our destination specification.
414 	 * @return a new specification expanded from provided ref name. Result
415 	 *         specification is wildcard if and only if provided ref name is
416 	 *         wildcard.
417 	 */
418 	public RefSpec expandFromDestination(final Ref r) {
419 		return expandFromDestination(r.getName());
420 	}
421 
422 	private boolean match(final String name, final String s) {
423 		if (s == null)
424 			return false;
425 		if (isWildcard()) {
426 			int wildcardIndex = s.indexOf('*');
427 			String prefix = s.substring(0, wildcardIndex);
428 			String suffix = s.substring(wildcardIndex + 1);
429 			return name.length() > prefix.length() + suffix.length()
430 					&& name.startsWith(prefix) && name.endsWith(suffix);
431 		}
432 		return name.equals(s);
433 	}
434 
435 	private static String expandWildcard(String name, String patternA,
436 			String patternB) {
437 		int a = patternA.indexOf('*');
438 		int trailingA = patternA.length() - (a + 1);
439 		int b = patternB.indexOf('*');
440 		String match = name.substring(a, name.length() - trailingA);
441 		return patternB.substring(0, b) + match + patternB.substring(b + 1);
442 	}
443 
444 	private static String checkValid(String spec) {
445 		if (spec != null && !isValid(spec))
446 			throw new IllegalArgumentException(MessageFormat.format(
447 					JGitText.get().invalidRefSpec, spec));
448 		return spec;
449 	}
450 
451 	private static boolean isValid(final String s) {
452 		if (s.startsWith("/")) //$NON-NLS-1$
453 			return false;
454 		if (s.contains("//")) //$NON-NLS-1$
455 			return false;
456 		int i = s.indexOf('*');
457 		if (i != -1) {
458 			if (s.indexOf('*', i + 1) > i)
459 				return false;
460 			if (i > 0 && s.charAt(i - 1) != '/')
461 				return false;
462 			if (i < s.length() - 1 && s.charAt(i + 1) != '/')
463 				return false;
464 		}
465 		return true;
466 	}
467 
468 	public int hashCode() {
469 		int hc = 0;
470 		if (getSource() != null)
471 			hc = hc * 31 + getSource().hashCode();
472 		if (getDestination() != null)
473 			hc = hc * 31 + getDestination().hashCode();
474 		return hc;
475 	}
476 
477 	public boolean equals(final Object obj) {
478 		if (!(obj instanceof RefSpec))
479 			return false;
480 		final RefSpec b = (RefSpec) obj;
481 		if (isForceUpdate() != b.isForceUpdate())
482 			return false;
483 		if (isWildcard() != b.isWildcard())
484 			return false;
485 		if (!eq(getSource(), b.getSource()))
486 			return false;
487 		if (!eq(getDestination(), b.getDestination()))
488 			return false;
489 		return true;
490 	}
491 
492 	private static boolean eq(final String a, final String b) {
493 		if (a == b)
494 			return true;
495 		if (a == null || b == null)
496 			return false;
497 		return a.equals(b);
498 	}
499 
500 	public String toString() {
501 		final StringBuilder r = new StringBuilder();
502 		if (isForceUpdate())
503 			r.append('+');
504 		if (getSource() != null)
505 			r.append(getSource());
506 		if (getDestination() != null) {
507 			r.append(':');
508 			r.append(getDestination());
509 		}
510 		return r.toString();
511 	}
512 }