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/*/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/*/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 }