View Javadoc
1   /*
2    * Copyright (C) 2008, 2013 Shawn O. Pearce <spearce@spearce.org> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.transport;
12  
13  import java.io.Serializable;
14  import java.text.MessageFormat;
15  import java.util.Objects;
16  
17  import org.eclipse.jgit.internal.JGitText;
18  import org.eclipse.jgit.lib.Constants;
19  import org.eclipse.jgit.lib.Ref;
20  
21  /**
22   * Describes how refs in one repository copy into another repository.
23   * <p>
24   * A ref specification provides matching support and limited rules to rewrite a
25   * reference in one repository to another reference in another repository.
26   */
27  public class RefSpec implements Serializable {
28  	private static final long serialVersionUID = 1L;
29  
30  	/**
31  	 * Suffix for wildcard ref spec component, that indicate matching all refs
32  	 * with specified prefix.
33  	 */
34  	public static final String WILDCARD_SUFFIX = "/*"; //$NON-NLS-1$
35  
36  	/**
37  	 * Check whether provided string is a wildcard ref spec component.
38  	 *
39  	 * @param s
40  	 *            ref spec component - string to test. Can be null.
41  	 * @return true if provided string is a wildcard ref spec component.
42  	 */
43  	public static boolean isWildcard(String s) {
44  		return s != null && s.contains("*"); //$NON-NLS-1$
45  	}
46  
47  	/** Does this specification ask for forced updated (rewind/reset)? */
48  	private boolean force;
49  
50  	/** Is this specification actually a wildcard match? */
51  	private boolean wildcard;
52  
53  	/** Is this the special ":" RefSpec? */
54  	private boolean matching;
55  
56  	/**
57  	 * How strict to be about wildcards.
58  	 *
59  	 * @since 4.5
60  	 */
61  	public enum WildcardMode {
62  		/**
63  		 * Reject refspecs with an asterisk on the source side and not the
64  		 * destination side or vice versa. This is the mode used by FetchCommand
65  		 * and PushCommand to create a one-to-one mapping between source and
66  		 * destination refs.
67  		 */
68  		REQUIRE_MATCH,
69  		/**
70  		 * Allow refspecs with an asterisk on only one side. This can create a
71  		 * many-to-one mapping between source and destination refs, so
72  		 * expandFromSource and expandFromDestination are not usable in this
73  		 * mode.
74  		 */
75  		ALLOW_MISMATCH
76  	}
77  
78  	/** Whether a wildcard is allowed on one side but not the other. */
79  	private WildcardMode allowMismatchedWildcards;
80  
81  	/** Name of the ref(s) we would copy from. */
82  	private String srcName;
83  
84  	/** Name of the ref(s) we would copy into. */
85  	private String dstName;
86  
87  	/**
88  	 * Construct an empty RefSpec.
89  	 * <p>
90  	 * A newly created empty RefSpec is not suitable for use in most
91  	 * applications, as at least one field must be set to match a source name.
92  	 */
93  	public RefSpec() {
94  		matching = false;
95  		force = false;
96  		wildcard = false;
97  		srcName = Constants.HEAD;
98  		dstName = null;
99  		allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH;
100 	}
101 
102 	/**
103 	 * Parse a ref specification for use during transport operations.
104 	 * <p>
105 	 * Specifications are typically one of the following forms:
106 	 * <ul>
107 	 * <li><code>refs/heads/master</code></li>
108 	 * <li><code>refs/heads/master:refs/remotes/origin/master</code></li>
109 	 * <li><code>refs/heads/*:refs/remotes/origin/*</code></li>
110 	 * <li><code>+refs/heads/master</code></li>
111 	 * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li>
112 	 * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li>
113 	 * <li><code>+refs/pull/&#42;/head:refs/remotes/origin/pr/*</code></li>
114 	 * <li><code>:refs/heads/master</code></li>
115 	 * </ul>
116 	 *
117 	 * If the wildcard mode allows mismatches, then these ref specs are also
118 	 * valid:
119 	 * <ul>
120 	 * <li><code>refs/heads/*</code></li>
121 	 * <li><code>refs/heads/*:refs/heads/master</code></li>
122 	 * </ul>
123 	 *
124 	 * @param spec
125 	 *            string describing the specification.
126 	 * @param mode
127 	 *            whether to allow a wildcard on one side without a wildcard on
128 	 *            the other.
129 	 * @throws java.lang.IllegalArgumentException
130 	 *             the specification is invalid.
131 	 * @since 4.5
132 	 */
133 	public RefSpec(String spec, WildcardMode mode) {
134 		this.allowMismatchedWildcards = mode;
135 		String s = spec;
136 		if (s.startsWith("+")) { //$NON-NLS-1$
137 			force = true;
138 			s = s.substring(1);
139 		}
140 
141 		boolean matchPushSpec = false;
142 		final int c = s.lastIndexOf(':');
143 		if (c == 0) {
144 			s = s.substring(1);
145 			if (s.isEmpty()) {
146 				matchPushSpec = true;
147 				wildcard = true;
148 				srcName = Constants.R_HEADS + '*';
149 				dstName = srcName;
150 			} else {
151 				if (isWildcard(s)) {
152 					wildcard = true;
153 					if (mode == WildcardMode.REQUIRE_MATCH) {
154 						throw new IllegalArgumentException(MessageFormat
155 								.format(JGitText.get().invalidWildcards, spec));
156 					}
157 				}
158 				dstName = checkValid(s);
159 			}
160 		} else if (c > 0) {
161 			String src = s.substring(0, c);
162 			String dst = s.substring(c + 1);
163 			if (isWildcard(src) && isWildcard(dst)) {
164 				// Both contain wildcard
165 				wildcard = true;
166 			} else if (isWildcard(src) || isWildcard(dst)) {
167 				wildcard = true;
168 				if (mode == WildcardMode.REQUIRE_MATCH)
169 					throw new IllegalArgumentException(MessageFormat
170 							.format(JGitText.get().invalidWildcards, spec));
171 			}
172 			srcName = checkValid(src);
173 			dstName = checkValid(dst);
174 		} else {
175 			if (isWildcard(s)) {
176 				if (mode == WildcardMode.REQUIRE_MATCH) {
177 					throw new IllegalArgumentException(MessageFormat
178 							.format(JGitText.get().invalidWildcards, spec));
179 				}
180 				wildcard = true;
181 			}
182 			srcName = checkValid(s);
183 		}
184 		matching = matchPushSpec;
185 	}
186 
187 	/**
188 	 * Parse a ref specification for use during transport operations.
189 	 * <p>
190 	 * Specifications are typically one of the following forms:
191 	 * <ul>
192 	 * <li><code>refs/heads/master</code></li>
193 	 * <li><code>refs/heads/master:refs/remotes/origin/master</code></li>
194 	 * <li><code>refs/heads/*:refs/remotes/origin/*</code></li>
195 	 * <li><code>+refs/heads/master</code></li>
196 	 * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li>
197 	 * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li>
198 	 * <li><code>+refs/pull/&#42;/head:refs/remotes/origin/pr/*</code></li>
199 	 * <li><code>:refs/heads/master</code></li>
200 	 * </ul>
201 	 *
202 	 * @param spec
203 	 *            string describing the specification.
204 	 * @throws java.lang.IllegalArgumentException
205 	 *             the specification is invalid.
206 	 */
207 	public RefSpec(String spec) {
208 		this(spec, WildcardMode.REQUIRE_MATCH);
209 	}
210 
211 	private RefSpec(RefSpec p) {
212 		matching = false;
213 		force = p.isForceUpdate();
214 		wildcard = p.isWildcard();
215 		srcName = p.getSource();
216 		dstName = p.getDestination();
217 		allowMismatchedWildcards = p.allowMismatchedWildcards;
218 	}
219 
220 	/**
221 	 * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":"
222 	 * for pushing.
223 	 *
224 	 * @return whether this is a "matching" RefSpec
225 	 * @since 6.1
226 	 */
227 	public boolean isMatching() {
228 		return matching;
229 	}
230 
231 	/**
232 	 * Check if this specification wants to forcefully update the destination.
233 	 *
234 	 * @return true if this specification asks for updates without merge tests.
235 	 */
236 	public boolean isForceUpdate() {
237 		return force;
238 	}
239 
240 	/**
241 	 * Create a new RefSpec with a different force update setting.
242 	 *
243 	 * @param forceUpdate
244 	 *            new value for force update in the returned instance.
245 	 * @return a new RefSpec with force update as specified.
246 	 */
247 	public RefSpec setForceUpdate(boolean forceUpdate) {
248 		final RefSpec r = new RefSpec(this);
249 		r.matching = matching;
250 		r.force = forceUpdate;
251 		return r;
252 	}
253 
254 	/**
255 	 * Check if this specification is actually a wildcard pattern.
256 	 * <p>
257 	 * If this is a wildcard pattern then the source and destination names
258 	 * returned by {@link #getSource()} and {@link #getDestination()} will not
259 	 * be actual ref names, but instead will be patterns.
260 	 *
261 	 * @return true if this specification could match more than one ref.
262 	 */
263 	public boolean isWildcard() {
264 		return wildcard;
265 	}
266 
267 	/**
268 	 * Get the source ref description.
269 	 * <p>
270 	 * During a fetch this is the name of the ref on the remote repository we
271 	 * are fetching from. During a push this is the name of the ref on the local
272 	 * repository we are pushing out from.
273 	 *
274 	 * @return name (or wildcard pattern) to match the source ref.
275 	 */
276 	public String getSource() {
277 		return srcName;
278 	}
279 
280 	/**
281 	 * Create a new RefSpec with a different source name setting.
282 	 *
283 	 * @param source
284 	 *            new value for source in the returned instance.
285 	 * @return a new RefSpec with source as specified.
286 	 * @throws java.lang.IllegalStateException
287 	 *             There is already a destination configured, and the wildcard
288 	 *             status of the existing destination disagrees with the
289 	 *             wildcard status of the new source.
290 	 */
291 	public RefSpec setSource(String source) {
292 		final RefSpec r = new RefSpec(this);
293 		r.srcName = checkValid(source);
294 		if (isWildcard(r.srcName) && r.dstName == null)
295 			throw new IllegalStateException(JGitText.get().destinationIsNotAWildcard);
296 		if (isWildcard(r.srcName) != isWildcard(r.dstName))
297 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
298 		return r;
299 	}
300 
301 	/**
302 	 * Get the destination ref description.
303 	 * <p>
304 	 * During a fetch this is the local tracking branch that will be updated
305 	 * with the new ObjectId after fetching is complete. During a push this is
306 	 * the remote ref that will be updated by the remote's receive-pack process.
307 	 * <p>
308 	 * If null during a fetch no tracking branch should be updated and the
309 	 * ObjectId should be stored transiently in order to prepare a merge.
310 	 * <p>
311 	 * If null during a push, use {@link #getSource()} instead.
312 	 *
313 	 * @return name (or wildcard) pattern to match the destination ref.
314 	 */
315 	public String getDestination() {
316 		return dstName;
317 	}
318 
319 	/**
320 	 * Create a new RefSpec with a different destination name setting.
321 	 *
322 	 * @param destination
323 	 *            new value for destination in the returned instance.
324 	 * @return a new RefSpec with destination as specified.
325 	 * @throws java.lang.IllegalStateException
326 	 *             There is already a source configured, and the wildcard status
327 	 *             of the existing source disagrees with the wildcard status of
328 	 *             the new destination.
329 	 */
330 	public RefSpec setDestination(String destination) {
331 		final RefSpec r = new RefSpec(this);
332 		r.dstName = checkValid(destination);
333 		if (isWildcard(r.dstName) && r.srcName == null)
334 			throw new IllegalStateException(JGitText.get().sourceIsNotAWildcard);
335 		if (isWildcard(r.srcName) != isWildcard(r.dstName))
336 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
337 		return r;
338 	}
339 
340 	/**
341 	 * Create a new RefSpec with a different source/destination name setting.
342 	 *
343 	 * @param source
344 	 *            new value for source in the returned instance.
345 	 * @param destination
346 	 *            new value for destination in the returned instance.
347 	 * @return a new RefSpec with destination as specified.
348 	 * @throws java.lang.IllegalArgumentException
349 	 *             The wildcard status of the new source disagrees with the
350 	 *             wildcard status of the new destination.
351 	 */
352 	public RefSpec setSourceDestination(String source, String destination) {
353 		if (isWildcard(source) != isWildcard(destination))
354 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
355 		final RefSpec r = new RefSpec(this);
356 		r.wildcard = isWildcard(source);
357 		r.srcName = source;
358 		r.dstName = destination;
359 		return r;
360 	}
361 
362 	/**
363 	 * Does this specification's source description match the ref name?
364 	 *
365 	 * @param r
366 	 *            ref name that should be tested.
367 	 * @return true if the names match; false otherwise.
368 	 */
369 	public boolean matchSource(String r) {
370 		return match(r, getSource());
371 	}
372 
373 	/**
374 	 * Does this specification's source description match the ref?
375 	 *
376 	 * @param r
377 	 *            ref whose name should be tested.
378 	 * @return true if the names match; false otherwise.
379 	 */
380 	public boolean matchSource(Ref r) {
381 		return match(r.getName(), getSource());
382 	}
383 
384 	/**
385 	 * Does this specification's destination description match the ref name?
386 	 *
387 	 * @param r
388 	 *            ref name that should be tested.
389 	 * @return true if the names match; false otherwise.
390 	 */
391 	public boolean matchDestination(String r) {
392 		return match(r, getDestination());
393 	}
394 
395 	/**
396 	 * Does this specification's destination description match the ref?
397 	 *
398 	 * @param r
399 	 *            ref whose name should be tested.
400 	 * @return true if the names match; false otherwise.
401 	 */
402 	public boolean matchDestination(Ref r) {
403 		return match(r.getName(), getDestination());
404 	}
405 
406 	/**
407 	 * Expand this specification to exactly match a ref name.
408 	 * <p>
409 	 * Callers must first verify the passed ref name matches this specification,
410 	 * otherwise expansion results may be unpredictable.
411 	 *
412 	 * @param r
413 	 *            a ref name that matched our source specification. Could be a
414 	 *            wildcard also.
415 	 * @return a new specification expanded from provided ref name. Result
416 	 *         specification is wildcard if and only if provided ref name is
417 	 *         wildcard.
418 	 * @throws java.lang.IllegalStateException
419 	 *             when the RefSpec was constructed with wildcard mode that
420 	 *             doesn't require matching wildcards.
421 	 */
422 	public RefSpec expandFromSource(String r) {
423 		if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) {
424 			throw new IllegalStateException(
425 					JGitText.get().invalidExpandWildcard);
426 		}
427 		return isWildcard() ? new RefSpec(this).expandFromSourceImp(r) : this;
428 	}
429 
430 	private RefSpec expandFromSourceImp(String name) {
431 		final String psrc = srcName, pdst = dstName;
432 		wildcard = false;
433 		srcName = name;
434 		dstName = expandWildcard(name, psrc, pdst);
435 		return this;
436 	}
437 
438 	/**
439 	 * Expand this specification to exactly match a ref.
440 	 * <p>
441 	 * Callers must first verify the passed ref matches this specification,
442 	 * otherwise expansion results may be unpredictable.
443 	 *
444 	 * @param r
445 	 *            a ref that matched our source specification. Could be a
446 	 *            wildcard also.
447 	 * @return a new specification expanded from provided ref name. Result
448 	 *         specification is wildcard if and only if provided ref name is
449 	 *         wildcard.
450 	 * @throws java.lang.IllegalStateException
451 	 *             when the RefSpec was constructed with wildcard mode that
452 	 *             doesn't require matching wildcards.
453 	 */
454 	public RefSpec expandFromSource(Ref r) {
455 		return expandFromSource(r.getName());
456 	}
457 
458 	/**
459 	 * Expand this specification to exactly match a ref name.
460 	 * <p>
461 	 * Callers must first verify the passed ref name matches this specification,
462 	 * otherwise expansion results may be unpredictable.
463 	 *
464 	 * @param r
465 	 *            a ref name that matched our destination specification. Could
466 	 *            be a wildcard also.
467 	 * @return a new specification expanded from provided ref name. Result
468 	 *         specification is wildcard if and only if provided ref name is
469 	 *         wildcard.
470 	 * @throws java.lang.IllegalStateException
471 	 *             when the RefSpec was constructed with wildcard mode that
472 	 *             doesn't require matching wildcards.
473 	 */
474 	public RefSpec expandFromDestination(String r) {
475 		if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) {
476 			throw new IllegalStateException(
477 					JGitText.get().invalidExpandWildcard);
478 		}
479 		return isWildcard() ? new RefSpec(this).expandFromDstImp(r) : this;
480 	}
481 
482 	private RefSpec expandFromDstImp(String name) {
483 		final String psrc = srcName, pdst = dstName;
484 		wildcard = false;
485 		srcName = expandWildcard(name, pdst, psrc);
486 		dstName = name;
487 		return this;
488 	}
489 
490 	/**
491 	 * Expand this specification to exactly match a ref.
492 	 * <p>
493 	 * Callers must first verify the passed ref matches this specification,
494 	 * otherwise expansion results may be unpredictable.
495 	 *
496 	 * @param r
497 	 *            a ref that matched our destination specification.
498 	 * @return a new specification expanded from provided ref name. Result
499 	 *         specification is wildcard if and only if provided ref name is
500 	 *         wildcard.
501 	 * @throws java.lang.IllegalStateException
502 	 *             when the RefSpec was constructed with wildcard mode that
503 	 *             doesn't require matching wildcards.
504 	 */
505 	public RefSpec expandFromDestination(Ref r) {
506 		return expandFromDestination(r.getName());
507 	}
508 
509 	private boolean match(String name, String s) {
510 		if (s == null)
511 			return false;
512 		if (isWildcard(s)) {
513 			int wildcardIndex = s.indexOf('*');
514 			String prefix = s.substring(0, wildcardIndex);
515 			String suffix = s.substring(wildcardIndex + 1);
516 			return name.length() > prefix.length() + suffix.length()
517 					&& name.startsWith(prefix) && name.endsWith(suffix);
518 		}
519 		return name.equals(s);
520 	}
521 
522 	private static String expandWildcard(String name, String patternA,
523 			String patternB) {
524 		int a = patternA.indexOf('*');
525 		int trailingA = patternA.length() - (a + 1);
526 		int b = patternB.indexOf('*');
527 		String match = name.substring(a, name.length() - trailingA);
528 		return patternB.substring(0, b) + match + patternB.substring(b + 1);
529 	}
530 
531 	private static String checkValid(String spec) {
532 		if (spec != null && !isValid(spec))
533 			throw new IllegalArgumentException(MessageFormat.format(
534 					JGitText.get().invalidRefSpec, spec));
535 		return spec;
536 	}
537 
538 	private static boolean isValid(String s) {
539 		if (s.startsWith("/")) //$NON-NLS-1$
540 			return false;
541 		if (s.contains("//")) //$NON-NLS-1$
542 			return false;
543 		if (s.endsWith("/")) //$NON-NLS-1$
544 			return false;
545 		int i = s.indexOf('*');
546 		if (i != -1) {
547 			if (s.indexOf('*', i + 1) > i)
548 				return false;
549 		}
550 		return true;
551 	}
552 
553 	/** {@inheritDoc} */
554 	@Override
555 	public int hashCode() {
556 		int hc = 0;
557 		if (getSource() != null)
558 			hc = hc * 31 + getSource().hashCode();
559 		if (getDestination() != null)
560 			hc = hc * 31 + getDestination().hashCode();
561 		return hc;
562 	}
563 
564 	/** {@inheritDoc} */
565 	@Override
566 	public boolean equals(Object obj) {
567 		if (!(obj instanceof RefSpec))
568 			return false;
569 		final RefSpec b = (RefSpec) obj;
570 		if (isForceUpdate() != b.isForceUpdate()) {
571 			return false;
572 		}
573 		if (isMatching()) {
574 			return b.isMatching();
575 		} else if (b.isMatching()) {
576 			return false;
577 		}
578 		return isWildcard() == b.isWildcard()
579 				&& Objects.equals(getSource(), b.getSource())
580 				&& Objects.equals(getDestination(), b.getDestination());
581 	}
582 
583 	/** {@inheritDoc} */
584 	@Override
585 	public String toString() {
586 		final StringBuilder r = new StringBuilder();
587 		if (isForceUpdate()) {
588 			r.append('+');
589 		}
590 		if (isMatching()) {
591 			r.append(':');
592 		} else {
593 			if (getSource() != null) {
594 				r.append(getSource());
595 			}
596 			if (getDestination() != null) {
597 				r.append(':');
598 				r.append(getDestination());
599 			}
600 		}
601 		return r.toString();
602 	}
603 }