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 /** Is this a negative refspec. */
57 private boolean negative;
58
59 /**
60 * How strict to be about wildcards.
61 *
62 * @since 4.5
63 */
64 public enum WildcardMode {
65 /**
66 * Reject refspecs with an asterisk on the source side and not the
67 * destination side or vice versa. This is the mode used by FetchCommand
68 * and PushCommand to create a one-to-one mapping between source and
69 * destination refs.
70 */
71 REQUIRE_MATCH,
72 /**
73 * Allow refspecs with an asterisk on only one side. This can create a
74 * many-to-one mapping between source and destination refs, so
75 * expandFromSource and expandFromDestination are not usable in this
76 * mode.
77 */
78 ALLOW_MISMATCH
79 }
80
81 /** Whether a wildcard is allowed on one side but not the other. */
82 private WildcardMode allowMismatchedWildcards;
83
84 /** Name of the ref(s) we would copy from. */
85 private String srcName;
86
87 /** Name of the ref(s) we would copy into. */
88 private String dstName;
89
90 /**
91 * Construct an empty RefSpec.
92 * <p>
93 * A newly created empty RefSpec is not suitable for use in most
94 * applications, as at least one field must be set to match a source name.
95 */
96 public RefSpec() {
97 matching = false;
98 force = false;
99 wildcard = false;
100 srcName = Constants.HEAD;
101 dstName = null;
102 negative =false;
103 allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH;
104 }
105
106 /**
107 * Parse a ref specification for use during transport operations.
108 * <p>
109 * {@link RefSpec}s can be regular or negative, regular RefSpecs indicate
110 * what to include in transport operations while negative RefSpecs indicate
111 * what to exclude in fetch.
112 * <p>
113 * Negative {@link RefSpec}s can't be force, must have only source or
114 * destination. Wildcard patterns are also supported in negative RefSpecs
115 * but they can not go with {@code WildcardMode.REQUIRE_MATCH} because they
116 * are natually one to many mappings.
117 *
118 * <p>
119 * Specifications are typically one of the following forms:
120 * <ul>
121 * <li><code>refs/heads/master</code></li>
122 * <li><code>refs/heads/master:refs/remotes/origin/master</code></li>
123 * <li><code>refs/heads/*:refs/remotes/origin/*</code></li>
124 * <li><code>+refs/heads/master</code></li>
125 * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li>
126 * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li>
127 * <li><code>+refs/pull/*/head:refs/remotes/origin/pr/*</code></li>
128 * <li><code>:refs/heads/master</code></li>
129 * </ul>
130 *
131 * If the wildcard mode allows mismatches, then these ref specs are also
132 * valid:
133 * <ul>
134 * <li><code>refs/heads/*</code></li>
135 * <li><code>refs/heads/*:refs/heads/master</code></li>
136 * </ul>
137 *
138 * Negative specifications are usually like:
139 * <ul>
140 * <li><code>^:refs/heads/master</code></li>
141 * <li><code>^refs/heads/*</code></li>
142 * </ul>
143 *
144 * @param spec
145 * string describing the specification.
146 * @param mode
147 * whether to allow a wildcard on one side without a wildcard on
148 * the other.
149 * @throws java.lang.IllegalArgumentException
150 * the specification is invalid.
151 * @since 4.5
152 */
153 public RefSpec(String spec, WildcardMode mode) {
154 this.allowMismatchedWildcards = mode;
155 String s = spec;
156
157 if (s.startsWith("^+") || s.startsWith("+^")) {
158 throw new IllegalArgumentException(
159 JGitText.get().invalidNegativeAndForce);
160 }
161
162 if (s.startsWith("+")) { //$NON-NLS-1$
163 force = true;
164 s = s.substring(1);
165 }
166
167 if(s.startsWith("^")) {
168 negative = true;
169 s = s.substring(1);
170 }
171
172 boolean matchPushSpec = false;
173 final int c = s.lastIndexOf(':');
174 if (c == 0) {
175 s = s.substring(1);
176 if (s.isEmpty()) {
177 matchPushSpec = true;
178 wildcard = true;
179 srcName = Constants.R_HEADS + '*';
180 dstName = srcName;
181 } else {
182 if (isWildcard(s)) {
183 wildcard = true;
184 if (mode == WildcardMode.REQUIRE_MATCH) {
185 throw new IllegalArgumentException(MessageFormat
186 .format(JGitText.get().invalidWildcards, spec));
187 }
188 }
189 dstName = checkValid(s);
190 }
191 } else if (c > 0) {
192 String src = s.substring(0, c);
193 String dst = s.substring(c + 1);
194 if (isWildcard(src) && isWildcard(dst)) {
195 // Both contain wildcard
196 wildcard = true;
197 } else if (isWildcard(src) || isWildcard(dst)) {
198 wildcard = true;
199 if (mode == WildcardMode.REQUIRE_MATCH)
200 throw new IllegalArgumentException(MessageFormat
201 .format(JGitText.get().invalidWildcards, spec));
202 }
203 srcName = checkValid(src);
204 dstName = checkValid(dst);
205 } else {
206 if (isWildcard(s)) {
207 if (mode == WildcardMode.REQUIRE_MATCH) {
208 throw new IllegalArgumentException(MessageFormat
209 .format(JGitText.get().invalidWildcards, spec));
210 }
211 wildcard = true;
212 }
213 srcName = checkValid(s);
214 }
215
216 // Negative refspecs must only have dstName or srcName.
217 if (isNegative()) {
218 if (isNullOrEmpty(srcName) && isNullOrEmpty(dstName)) {
219 throw new IllegalArgumentException(MessageFormat
220 .format(JGitText.get().invalidRefSpec, spec));
221 }
222 if (!isNullOrEmpty(srcName) && !isNullOrEmpty(dstName)) {
223 throw new IllegalArgumentException(MessageFormat
224 .format(JGitText.get().invalidRefSpec, spec));
225 }
226 if(wildcard && mode == WildcardMode.REQUIRE_MATCH) {
227 throw new IllegalArgumentException(MessageFormat
228 .format(JGitText.get().invalidRefSpec, spec));}
229 }
230 matching = matchPushSpec;
231 }
232
233 /**
234 * Parse a ref specification for use during transport operations.
235 * <p>
236 * Specifications are typically one of the following forms:
237 * <ul>
238 * <li><code>refs/heads/master</code></li>
239 * <li><code>refs/heads/master:refs/remotes/origin/master</code></li>
240 * <li><code>refs/heads/*:refs/remotes/origin/*</code></li>
241 * <li><code>+refs/heads/master</code></li>
242 * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li>
243 * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li>
244 * <li><code>+refs/pull/*/head:refs/remotes/origin/pr/*</code></li>
245 * <li><code>:refs/heads/master</code></li>
246 * </ul>
247 *
248 * @param spec
249 * string describing the specification.
250 * @throws java.lang.IllegalArgumentException
251 * the specification is invalid.
252 */
253 public RefSpec(String spec) {
254 this(spec, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH
255 : WildcardMode.REQUIRE_MATCH);
256 }
257
258 private RefSpec(RefSpec p) {
259 matching = false;
260 force = p.isForceUpdate();
261 wildcard = p.isWildcard();
262 negative = p.isNegative();
263 srcName = p.getSource();
264 dstName = p.getDestination();
265 allowMismatchedWildcards = p.allowMismatchedWildcards;
266 }
267
268 /**
269 * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":"
270 * for pushing.
271 *
272 * @return whether this is a "matching" RefSpec
273 * @since 6.1
274 */
275 public boolean isMatching() {
276 return matching;
277 }
278
279 /**
280 * Check if this specification wants to forcefully update the destination.
281 *
282 * @return true if this specification asks for updates without merge tests.
283 */
284 public boolean isForceUpdate() {
285 return force;
286 }
287
288 /**
289 * Create a new RefSpec with a different force update setting.
290 *
291 * @param forceUpdate
292 * new value for force update in the returned instance.
293 * @return a new RefSpec with force update as specified.
294 */
295 public RefSpec setForceUpdate(boolean forceUpdate) {
296 final RefSpec r = new RefSpec(this);
297 if (forceUpdate && isNegative()) {
298 throw new IllegalArgumentException(
299 JGitText.get().invalidNegativeAndForce);
300 }
301 r.matching = matching;
302 r.force = forceUpdate;
303 return r;
304 }
305
306 /**
307 * Check if this specification is actually a wildcard pattern.
308 * <p>
309 * If this is a wildcard pattern then the source and destination names
310 * returned by {@link #getSource()} and {@link #getDestination()} will not
311 * be actual ref names, but instead will be patterns.
312 *
313 * @return true if this specification could match more than one ref.
314 */
315 public boolean isWildcard() {
316 return wildcard;
317 }
318
319 /**
320 * Check if this specification is a negative one.
321 *
322 * @return true if this specification is negative.
323 * @since 6.2
324 */
325 public boolean isNegative() {
326 return negative;
327 }
328
329 /**
330 * Get the source ref description.
331 * <p>
332 * During a fetch this is the name of the ref on the remote repository we
333 * are fetching from. During a push this is the name of the ref on the local
334 * repository we are pushing out from.
335 *
336 * @return name (or wildcard pattern) to match the source ref.
337 */
338 public String getSource() {
339 return srcName;
340 }
341
342 /**
343 * Create a new RefSpec with a different source name setting.
344 *
345 * @param source
346 * new value for source in the returned instance.
347 * @return a new RefSpec with source as specified.
348 * @throws java.lang.IllegalStateException
349 * There is already a destination configured, and the wildcard
350 * status of the existing destination disagrees with the
351 * wildcard status of the new source.
352 */
353 public RefSpec setSource(String source) {
354 final RefSpec r = new RefSpec(this);
355 r.srcName = checkValid(source);
356 if (isWildcard(r.srcName) && r.dstName == null)
357 throw new IllegalStateException(JGitText.get().destinationIsNotAWildcard);
358 if (isWildcard(r.srcName) != isWildcard(r.dstName))
359 throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
360 return r;
361 }
362
363 /**
364 * Get the destination ref description.
365 * <p>
366 * During a fetch this is the local tracking branch that will be updated
367 * with the new ObjectId after fetching is complete. During a push this is
368 * the remote ref that will be updated by the remote's receive-pack process.
369 * <p>
370 * If null during a fetch no tracking branch should be updated and the
371 * ObjectId should be stored transiently in order to prepare a merge.
372 * <p>
373 * If null during a push, use {@link #getSource()} instead.
374 *
375 * @return name (or wildcard) pattern to match the destination ref.
376 */
377 public String getDestination() {
378 return dstName;
379 }
380
381 /**
382 * Create a new RefSpec with a different destination name setting.
383 *
384 * @param destination
385 * new value for destination in the returned instance.
386 * @return a new RefSpec with destination as specified.
387 * @throws java.lang.IllegalStateException
388 * There is already a source configured, and the wildcard status
389 * of the existing source disagrees with the wildcard status of
390 * the new destination.
391 */
392 public RefSpec setDestination(String destination) {
393 final RefSpec r = new RefSpec(this);
394 r.dstName = checkValid(destination);
395 if (isWildcard(r.dstName) && r.srcName == null)
396 throw new IllegalStateException(JGitText.get().sourceIsNotAWildcard);
397 if (isWildcard(r.srcName) != isWildcard(r.dstName))
398 throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
399 return r;
400 }
401
402 /**
403 * Create a new RefSpec with a different source/destination name setting.
404 *
405 * @param source
406 * new value for source in the returned instance.
407 * @param destination
408 * new value for destination in the returned instance.
409 * @return a new RefSpec with destination as specified.
410 * @throws java.lang.IllegalArgumentException
411 * The wildcard status of the new source disagrees with the
412 * wildcard status of the new destination.
413 */
414 public RefSpec setSourceDestination(String source, String destination) {
415 if (isWildcard(source) != isWildcard(destination))
416 throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
417 final RefSpec r = new RefSpec(this);
418 r.wildcard = isWildcard(source);
419 r.srcName = source;
420 r.dstName = destination;
421 return r;
422 }
423
424 /**
425 * Does this specification's source description match the ref name?
426 *
427 * @param r
428 * ref name that should be tested.
429 * @return true if the names match; false otherwise.
430 */
431 public boolean matchSource(String r) {
432 return match(r, getSource());
433 }
434
435 /**
436 * Does this specification's source description match the ref?
437 *
438 * @param r
439 * ref whose name should be tested.
440 * @return true if the names match; false otherwise.
441 */
442 public boolean matchSource(Ref r) {
443 return match(r.getName(), getSource());
444 }
445
446 /**
447 * Does this specification's destination description match the ref name?
448 *
449 * @param r
450 * ref name that should be tested.
451 * @return true if the names match; false otherwise.
452 */
453 public boolean matchDestination(String r) {
454 return match(r, getDestination());
455 }
456
457 /**
458 * Does this specification's destination description match the ref?
459 *
460 * @param r
461 * ref whose name should be tested.
462 * @return true if the names match; false otherwise.
463 */
464 public boolean matchDestination(Ref r) {
465 return match(r.getName(), getDestination());
466 }
467
468 /**
469 * Expand this specification to exactly match a ref name.
470 * <p>
471 * Callers must first verify the passed ref name matches this specification,
472 * otherwise expansion results may be unpredictable.
473 *
474 * @param r
475 * a ref name that matched our source specification. Could be a
476 * wildcard also.
477 * @return a new specification expanded from provided ref name. Result
478 * specification is wildcard if and only if provided ref name is
479 * wildcard.
480 * @throws java.lang.IllegalStateException
481 * when the RefSpec was constructed with wildcard mode that
482 * doesn't require matching wildcards.
483 */
484 public RefSpec expandFromSource(String r) {
485 if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) {
486 throw new IllegalStateException(
487 JGitText.get().invalidExpandWildcard);
488 }
489 return isWildcard() ? new RefSpec(this).expandFromSourceImp(r) : this;
490 }
491
492 private RefSpec expandFromSourceImp(String name) {
493 final String psrc = srcName, pdst = dstName;
494 wildcard = false;
495 srcName = name;
496 dstName = expandWildcard(name, psrc, pdst);
497 return this;
498 }
499
500 private static boolean isNullOrEmpty(String refName) {
501 return refName == null || refName.isEmpty();
502 }
503
504 /**
505 * Expand this specification to exactly match a ref.
506 * <p>
507 * Callers must first verify the passed ref matches this specification,
508 * otherwise expansion results may be unpredictable.
509 *
510 * @param r
511 * a ref that matched our source specification. Could be a
512 * wildcard also.
513 * @return a new specification expanded from provided ref name. Result
514 * specification is wildcard if and only if provided ref name is
515 * wildcard.
516 * @throws java.lang.IllegalStateException
517 * when the RefSpec was constructed with wildcard mode that
518 * doesn't require matching wildcards.
519 */
520 public RefSpec expandFromSource(Ref r) {
521 return expandFromSource(r.getName());
522 }
523
524 /**
525 * Expand this specification to exactly match a ref name.
526 * <p>
527 * Callers must first verify the passed ref name matches this specification,
528 * otherwise expansion results may be unpredictable.
529 *
530 * @param r
531 * a ref name that matched our destination specification. Could
532 * be a wildcard also.
533 * @return a new specification expanded from provided ref name. Result
534 * specification is wildcard if and only if provided ref name is
535 * wildcard.
536 * @throws java.lang.IllegalStateException
537 * when the RefSpec was constructed with wildcard mode that
538 * doesn't require matching wildcards.
539 */
540 public RefSpec expandFromDestination(String r) {
541 if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) {
542 throw new IllegalStateException(
543 JGitText.get().invalidExpandWildcard);
544 }
545 return isWildcard() ? new RefSpec(this).expandFromDstImp(r) : this;
546 }
547
548 private RefSpec expandFromDstImp(String name) {
549 final String psrc = srcName, pdst = dstName;
550 wildcard = false;
551 srcName = expandWildcard(name, pdst, psrc);
552 dstName = name;
553 return this;
554 }
555
556 /**
557 * Expand this specification to exactly match a ref.
558 * <p>
559 * Callers must first verify the passed ref matches this specification,
560 * otherwise expansion results may be unpredictable.
561 *
562 * @param r
563 * a ref that matched our destination specification.
564 * @return a new specification expanded from provided ref name. Result
565 * specification is wildcard if and only if provided ref name is
566 * wildcard.
567 * @throws java.lang.IllegalStateException
568 * when the RefSpec was constructed with wildcard mode that
569 * doesn't require matching wildcards.
570 */
571 public RefSpec expandFromDestination(Ref r) {
572 return expandFromDestination(r.getName());
573 }
574
575 private boolean match(String name, String s) {
576 if (s == null)
577 return false;
578 if (isWildcard(s)) {
579 int wildcardIndex = s.indexOf('*');
580 String prefix = s.substring(0, wildcardIndex);
581 String suffix = s.substring(wildcardIndex + 1);
582 return name.length() > prefix.length() + suffix.length()
583 && name.startsWith(prefix) && name.endsWith(suffix);
584 }
585 return name.equals(s);
586 }
587
588 private static String expandWildcard(String name, String patternA,
589 String patternB) {
590 int a = patternA.indexOf('*');
591 int trailingA = patternA.length() - (a + 1);
592 int b = patternB.indexOf('*');
593 String match = name.substring(a, name.length() - trailingA);
594 return patternB.substring(0, b) + match + patternB.substring(b + 1);
595 }
596
597 private static String checkValid(String spec) {
598 if (spec != null && !isValid(spec))
599 throw new IllegalArgumentException(MessageFormat.format(
600 JGitText.get().invalidRefSpec, spec));
601 return spec;
602 }
603
604 private static boolean isValid(String s) {
605 if (s.startsWith("/")) //$NON-NLS-1$
606 return false;
607 if (s.contains("//")) //$NON-NLS-1$
608 return false;
609 if (s.endsWith("/")) //$NON-NLS-1$
610 return false;
611 int i = s.indexOf('*');
612 if (i != -1) {
613 if (s.indexOf('*', i + 1) > i)
614 return false;
615 }
616 return true;
617 }
618
619 /** {@inheritDoc} */
620 @Override
621 public int hashCode() {
622 int hc = 0;
623 if (getSource() != null)
624 hc = hc * 31 + getSource().hashCode();
625 if (getDestination() != null)
626 hc = hc * 31 + getDestination().hashCode();
627 return hc;
628 }
629
630 /** {@inheritDoc} */
631 @Override
632 public boolean equals(Object obj) {
633 if (!(obj instanceof RefSpec))
634 return false;
635 final RefSpec b = (RefSpec) obj;
636 if (isForceUpdate() != b.isForceUpdate()) {
637 return false;
638 }
639 if(isNegative() != b.isNegative()) {
640 return false;
641 }
642 if (isMatching()) {
643 return b.isMatching();
644 } else if (b.isMatching()) {
645 return false;
646 }
647 return isWildcard() == b.isWildcard()
648 && Objects.equals(getSource(), b.getSource())
649 && Objects.equals(getDestination(), b.getDestination());
650 }
651
652 /** {@inheritDoc} */
653 @Override
654 public String toString() {
655 final StringBuilder r = new StringBuilder();
656 if (isForceUpdate()) {
657 r.append('+');
658 }
659 if(isNegative()) {
660 r.append('^');
661 }
662 if (isMatching()) {
663 r.append(':');
664 } else {
665 if (getSource() != null) {
666 r.append(getSource());
667 }
668 if (getDestination() != null) {
669 r.append(':');
670 r.append(getDestination());
671 }
672 }
673 return r.toString();
674 }
675 }