1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.common;
18  
19  import static com.google.common.base.Preconditions.checkArgument;
20  import static java.util.Objects.requireNonNull;
21  
22  import java.util.regex.Pattern;
23  
24  import javax.annotation.Nullable;
25  
26  import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
27  import com.fasterxml.jackson.databind.annotation.JsonSerialize;
28  
29  import com.linecorp.centraldogma.internal.Util;
30  
31  /**
32   * A revision number of a {@link Commit}.
33   *
34   * <p>A revision number is an integer which refers to a specific point of repository history.
35   * When a repository is created, it starts with an initial commit whose revision is {@code 1}.
36   * As new commits are added, each commit gets its own revision number, monotonically increasing from the
37   * previous commit's revision. i.e. 1, 2, 3, ...
38   *
39   * <p>A revision number can also be represented as a negative integer. When a revision number is negative,
40   * we start from {@code -1} which refers to the latest commit in repository history, which is often called
41   * 'HEAD' of the repository. A smaller revision number refers to the older commit. e.g. -2 refers to the
42   * commit before the latest commit, and so on.
43   *
44   * <p>A revision with a negative integer is called 'relative revision'. By contrast, a revision with
45   * a positive integer is called 'absolute revision'.
46   */
47  @JsonSerialize(using = RevisionJsonSerializer.class)
48  @JsonDeserialize(using = RevisionJsonDeserializer.class)
49  public class Revision implements Comparable<Revision> {
50  
51      private static final Pattern REVISION_PATTERN = Pattern.compile("^-?[0-9]+(?:\\.0)?$");
52  
53      /**
54       * Revision {@code -1}, also known as 'HEAD'.
55       */
56      public static final Revision HEAD = new Revision(-1);
57  
58      /**
59       * Revision {@code 1}, also known as 'INIT'.
60       */
61      public static final Revision INIT = new Revision(1);
62  
63      private final int major;
64      private final String text;
65  
66      /**
67       * Creates a new instance with the specified revision number.
68       */
69      public Revision(int major) {
70          if (major == 0) {
71              throw new IllegalArgumentException("major: 0 (expected: a non-zero integer)");
72          }
73  
74          this.major = major;
75          text = generateText(major);
76      }
77  
78      /**
79       * Creates a new instance.
80       *
81       * @deprecated Use {@link #Revision(int)} instead. Minor revisions are not used anymore.
82       */
83      @Deprecated
84      public Revision(int major, int minor) {
85          this(major);
86          checkArgument(minor == 0, "minor: %s (expected: 0)", minor);
87      }
88  
89      /**
90       * Create a new instance from a string representation. e.g. {@code "42", "-1"}
91       */
92      public Revision(String revisionStr) {
93          requireNonNull(revisionStr, "revisionStr");
94          if ("head".equalsIgnoreCase(revisionStr) || "-1".equals(revisionStr)) {
95              major = HEAD.major;
96          } else {
97              if (!REVISION_PATTERN.matcher(revisionStr).matches()) {
98                  throw illegalRevisionArgumentException(revisionStr);
99              }
100 
101             try {
102                 major = Integer.parseInt(
103                         !revisionStr.endsWith(".0") ? revisionStr
104                                                     : revisionStr.substring(0, revisionStr.length() - 2));
105                 if (major == 0) {
106                     throw illegalRevisionArgumentException(revisionStr);
107                 }
108             } catch (NumberFormatException ignored) {
109                 throw illegalRevisionArgumentException(revisionStr);
110             }
111         }
112         text = generateText(major);
113     }
114 
115     /**
116      * Returns the revision number.
117      */
118     public int major() {
119         return major;
120     }
121 
122     /**
123      * Returns {@code 0}.
124      *
125      * @deprecated Do not use. Minor revisions are not used anymore.
126      */
127     @Deprecated
128     public int minor() {
129         return 0;
130     }
131 
132     /**
133      * Returns the textual representation of the revision. e.g. {@code "42", "-1"}.
134      */
135     public String text() {
136         return text;
137     }
138 
139     /**
140      * Returns {@code true}.
141      *
142      * @deprecated Do not use. Minor revisions are not used anymore.
143      */
144     @Deprecated
145     public boolean onMainLane() {
146         return true;
147     }
148 
149     /**
150      * Returns a new {@link Revision} whose revision number is earlier than this {@link Revision}.
151      *
152      * @param count the number of commits to go backward
153      */
154     public Revision backward(int count) {
155         if (count == 0) {
156             return this;
157         }
158         if (count < 0) {
159             throw new IllegalArgumentException("count " + count + " (expected: a non-negative integer)");
160         }
161 
162         return new Revision(subtract(major, count));
163     }
164 
165     private static int subtract(int revNum, int delta) {
166         if (revNum > 0) {
167             return Math.max(1, revNum - delta);
168         } else {
169             if (revNum < Integer.MIN_VALUE + delta) {
170                 return Integer.MIN_VALUE;
171             } else {
172                 return revNum - delta;
173             }
174         }
175     }
176 
177     /**
178      * Returns a new {@link Revision} whose revision number is later than this {@link Revision}.
179      *
180      * @param count the number of commits to go forward
181      */
182     public Revision forward(int count) {
183         if (count == 0) {
184             return this;
185         }
186         if (count < 0) {
187             throw new IllegalArgumentException("count " + count + " (expected: a non-negative integer)");
188         }
189 
190         return new Revision(add(major, count));
191     }
192 
193     private static int add(int revNum, int delta) {
194         if (revNum > 0) {
195             if (revNum > Integer.MAX_VALUE - delta) {
196                 return Integer.MAX_VALUE;
197             } else {
198                 return revNum + delta;
199             }
200         } else {
201             return Math.min(-1, revNum + delta);
202         }
203     }
204 
205     @Override
206     public int hashCode() {
207         return major;
208     }
209 
210     @Override
211     public boolean equals(@Nullable Object o) {
212         if (this == o) {
213             return true;
214         }
215         if (!(o instanceof Revision)) {
216             return false;
217         }
218 
219         return major == ((Revision) o).major;
220     }
221 
222     @Override
223     public String toString() {
224 
225         final StringBuilder buf = new StringBuilder();
226 
227         buf.append(Util.simpleTypeName(this));
228         buf.append('(');
229         buf.append(major);
230         buf.append(')');
231 
232         return buf.toString();
233     }
234 
235     @Override
236     public int compareTo(Revision o) {
237         return Integer.compare(major, o.major);
238     }
239 
240     /**
241      * Returns whether this {@link Revision} is relative.
242      */
243     public boolean isRelative() {
244         return major < 0;
245     }
246 
247     private static String generateText(int major) {
248         return String.valueOf(major);
249     }
250 
251     private static IllegalArgumentException illegalRevisionArgumentException(String revisionStr) {
252         return new IllegalArgumentException(
253                 "revisionStr: " + revisionStr +
254                 " (expected: \"major\" or \"major.0\" where major is non-zero integer)");
255     }
256 }