View Javadoc

1   /*
2    * #%L
3    * JavaHg
4    * %%
5    * Copyright (C) 2011 aragost Trifork ag
6    * %%
7    * Permission is hereby granted, free of charge, to any person obtaining a copy
8    * of this software and associated documentation files (the "Software"), to deal
9    * in the Software without restriction, including without limitation the rights
10   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11   * copies of the Software, and to permit persons to whom the Software is
12   * furnished to do so, subject to the following conditions:
13   * 
14   * The above copyright notice and this permission notice shall be included in
15   * all copies or substantial portions of the Software.
16   * 
17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23   * THE SOFTWARE.
24   * #L%
25   */
26  package com.aragost.javahg;
27  
28  import java.io.IOException;
29  import java.util.List;
30  import java.util.Map;
31  
32  import com.aragost.javahg.commands.LogCommand;
33  import com.aragost.javahg.commands.StatusCommand;
34  import com.aragost.javahg.commands.StatusResult;
35  import com.aragost.javahg.internals.GenericLogCommand;
36  import com.aragost.javahg.internals.HgInputStream;
37  import com.aragost.javahg.internals.RuntimeIOException;
38  import com.aragost.javahg.internals.Utils;
39  import com.google.common.annotations.VisibleForTesting;
40  import com.google.common.base.Function;
41  import com.google.common.collect.ImmutableList;
42  import com.google.common.collect.ImmutableMap;
43  import com.google.common.collect.Lists;
44  import com.google.common.collect.Maps;
45  import com.google.common.collect.ImmutableList.Builder;
46  
47  /**
48   * Represent data for a single changeset.
49   * <p>
50   * A Changeset object can be created just with the node id. The
51   * actually data will be loaded on demand when it is accessed
52   */
53  public class Changeset {
54  
55      /**
56       * Character sequence that indicate the begin and end of the
57       * changeset in the command output.
58       * <p>
59       * This pattern is written by the CHANGESET_STYLE_PATH
60       */
61      private static final byte[] CHANGESET_PATTERN = Utils.randomBytes();
62  
63      /**
64       * Style file used among other with the log command to read
65       * changesets.
66       * <p>
67       * The style is parsed by the createFromInputStream() method.
68       */
69      public static final String CHANGESET_STYLE_PATH = Utils.resourceAsFile("/styles/changesets.style",
70              ImmutableMap.of("pattern", CHANGESET_PATTERN)).getPath();
71      
72      public static final String CHANGESET_EAGER_STYLE_PATH = Utils.resourceAsFile("/styles/changesets-eager.style",
73              ImmutableMap.of("pattern", CHANGESET_PATTERN)).getPath();
74  
75      /**
76       * The id for the null changeset
77       */
78      public static final String NULL_ID = "0000000000000000000000000000000000000000";
79  
80      private final String node;
81      private final Repository repository;
82  
83      /**
84       * The actual data for the Changeset
85       */
86      protected ChangesetData data;
87      
88      /**
89       * The actual file data for the Changeset
90       */
91      protected ChangesetFileData fileData;
92  
93      /**
94       * Mercurial's extra data. Lazy loaded.
95       */
96      private Extra extra;
97  
98      /**
99       * Use {@link Repository#changeset(String)} to create Changesets
100      * 
101      * @param repository
102      */
103     public Changeset(Repository repository, String node) {
104         this.repository = repository;
105         this.node = node;
106     }
107 
108     private static Changeset createFromInputStream(Repository repository, HgInputStream in, boolean eager) throws IOException {
109         byte[] node = in.next(40);
110         String nodeString = new String(node);
111         int revision = in.revisionUpTo('\n');
112         Changeset cset = repository.changeset(nodeString);
113         String user = in.textUpTo('\n');
114         DateTime timestamp = in.dateTimeUpTo('\n');
115         String branch = in.textUpTo('\n');
116         in.upTo(':');
117         String p1 = in.nextAsText(40);
118         in.upTo(':');
119         String p2 = in.nextAsText(40);
120         in.mustMatch(' '); // skip space part of {parents}
121         Changeset parent1 = repository.changeset(p1);
122         Changeset parent2 = repository.changeset(p2);
123         
124         // read files
125         if ( eager ){
126           Builder<String> addedBuilder = ImmutableList.builder();
127           Builder<String> modifiedBuilder = ImmutableList.builder();
128           Builder<String> deletedBuilder = ImmutableList.builder();
129 
130           String line = in.textUpTo('\n');
131           while ( line.length() > 0 ) {
132             if ( line.startsWith("a ") ){
133               addedBuilder.add( line.substring(2) );
134             } else if ( line.startsWith("m ") ){
135               modifiedBuilder.add( line.substring(2) );
136             } else if ( line.startsWith("d ")){
137               deletedBuilder.add( line.substring(2) );
138             }
139             line = in.textUpTo('\n');
140           }
141           
142           ChangesetFileData fileData = cset.fileData;
143           if (fileData == null){
144             fileData = new ChangesetFileData(addedBuilder.build(), modifiedBuilder.build(), deletedBuilder.build());
145             cset.fileData = fileData;
146           }
147         }
148         String message = in.textUpTo('\0');
149 
150         if (cset == null) {
151             // Revision -1:000000000000
152             return null;
153         }
154 
155         ChangesetData data = cset.data;
156         if (data == null) {
157             data = new ChangesetData(revision, user, timestamp, branch, parent1, parent2, 
158                        message);
159             cset.data = data;
160         } else if (revision != data.revision) {
161             // Handle revision specially because revision is not part of the node
162             data.revision = revision;
163         }
164         return cset;
165     }
166 
167     /**
168      * This method is an alias for {@link #readListFromStream(Repository, HgInputStream, boolean)} 
169      * with the eager parameter set to false.
170      * 
171      * @param repository
172      * @param in
173      * @return 
174      */
175     public static List<Changeset> readListFromStream(Repository repository, HgInputStream in) {
176       return readListFromStream(repository, in, false);
177     }
178 
179     /**
180      * Read the rest of the content of the stream and return a List of
181      * the Changeset found there.
182      * <p>
183      * Be aware the this method will read everything from the stream,
184      * what is after the changesets will simply be discarded.
185      * 
186      * @param repository
187      * @param in
188      * @param eager
189      * @return
190      */
191     public static List<Changeset> readListFromStream(Repository repository, HgInputStream in, boolean eager) {
192         List<Changeset> changesets = Lists.newArrayList();
193         try {
194             boolean found = in.find(CHANGESET_PATTERN);
195             if (found) {
196                 while (!in.match(CHANGESET_PATTERN)) {
197                     Changeset cset = Changeset.createFromInputStream(repository, in, eager);
198 
199                     if (cset != null) {
200                         changesets.add(cset);
201                     }
202                 }
203             }
204             // If the pattern is not found there is no changsets
205         } catch (IOException e) {
206             throw new RuntimeIOException(e);
207         } finally {
208            try {
209               Utils.consumeAll(in);
210            } catch (IOException e) {
211               throw new RuntimeIOException(e);
212            }
213         }
214         return changesets;
215     }
216 
217     public String getNode() {
218         return this.node;
219     }
220 
221     public int getRevision() {
222         ensureAllDataLoaded();
223         return this.data.revision;
224     }
225 
226     public String getUser() {
227         ensureAllDataLoaded();
228         return this.data.user;
229     }
230 
231     public DateTime getTimestamp() {
232         ensureAllDataLoaded();
233         return this.data.timestamp;
234     }
235 
236     public String getBranch() {
237         ensureAllDataLoaded();
238         return this.data.branch;
239     }
240 
241     public Changeset getParent1() {
242         ensureAllDataLoaded();
243         return this.data.parent1;
244     }
245 
246     public Changeset getParent2() {
247         ensureAllDataLoaded();
248         return this.data.parent2;
249     }
250 
251     public String getMessage() {
252         ensureAllDataLoaded();
253         return this.data.message;
254     }
255     
256     public List<String> getAddedFiles(){
257         ensureFileDataLoaded();
258         return this.fileData.addedFiles;
259     }
260     
261     public List<String> getModifiedFiles(){
262         ensureFileDataLoaded();
263         return this.fileData.modifiedFiles;
264     }
265     
266     public List<String> getDeletedFiles(){
267         ensureFileDataLoaded();
268         return this.fileData.deletedFiles;
269     }
270     
271     private void loadFileData(){
272       StatusResult result = new StatusCommand(repository).added().modified()
273                                   .removed().change(this.node).execute();
274       if ( result != null ){
275         fileData = new ChangesetFileData(result.getAdded(), result.getModified(), 
276                                          result.getRemoved());
277       } else {
278         throw new IllegalStateException("could not load file data from status");
279       }
280     }
281     
282     private void ensureFileDataLoaded(){
283         if (this.fileData != null) {
284           return;
285         }
286         loadFileData();
287         if (this.fileData == null) {
288             throw new IllegalStateException("could not load file data");
289         }
290     }
291 
292     private void ensureAllDataLoaded() {
293         if (this.data != null) {
294             return;
295         }
296         LogCommand.on(this.repository).rev(getNode()).execute();
297         if (this.data == null) {
298             throw new IllegalStateException("data was not loaded");
299         }
300     }
301 
302     @Deprecated
303     public Phase readPhase() {
304         return phase();
305     }
306 
307     /**
308      * 
309      * @return the phase for this changeset.
310      */
311     public Phase phase() {
312         Map<Changeset, Phase> phases = getRepository().phases(getNode());
313         return phases.get(this);
314     }
315 
316     /**
317      * Return tags that is pointing the this changeset
318      */
319     public List<String> tags() {
320         GenericLogCommand cmd = new GenericLogCommand(getRepository()).style("tags");
321         cmd.rev(getNode());
322         HgInputStream stream = cmd.stream();
323         List<String> result = Lists.newArrayList();
324         try {
325             while (!stream.isEof()) {
326                 String tag = stream.textUpTo(0);
327                 if (!"tip".equals(tag)) {
328                     result.add(tag);
329                 }
330             }
331         } catch (IOException e) {
332             throw new RuntimeIOException(e);
333         } finally {
334         	try {
335 				stream.consumeAll();
336 			} catch (IOException e) {
337 				throw new RuntimeIOException(e);
338 			}
339         }
340         return result;
341     }
342 
343     /**
344      * 
345      * @return Mercurial's extra dictionary
346      */
347     public synchronized Extra getExtra() {
348         if (this.extra == null) {
349             GenericLogCommand cmd = new GenericLogCommand(getRepository()).style("extras");
350             cmd.rev(getNode());
351             this.extra = new Extra(cmd.stream());
352         }
353         return this.extra;
354     }
355 
356     @Override
357     public boolean equals(Object that) {
358         if (that instanceof Changeset) {
359             return equals((Changeset) that);
360         } else {
361             return false;
362         }
363     }
364 
365     public boolean equals(Changeset that) {
366         if (this.repository != that.repository) {
367             return false;
368         }
369         return this.getNode().equals(that.getNode());
370     }
371 
372     @Override
373     public int hashCode() {
374         return getNode().hashCode();
375     }
376 
377     @Override
378     public String toString() {
379         StringBuilder builder = new StringBuilder("changeset[");
380         builder.append(this.data == null ? "?" : this.data.revision).append(':').append(this.node).append(']');
381         return builder.toString();
382     }
383 
384     @VisibleForTesting
385     ChangesetData getData() {
386         return this.data;
387     }
388     
389     @VisibleForTesting
390     ChangesetFileData getFileData(){
391       return this.fileData;
392     }
393 
394     Repository getRepository() {
395         return repository;
396     }
397 
398     private String decodeBytes(byte[] bytes) {
399         return Utils.decodeBytes(bytes, getRepository().newDecoder());
400     }
401 
402     /**
403      * Class representing the extra dictionary Mercurial has for each
404      * changeset.
405      * <p>
406      * The values can be binary data, but is typically strings. For
407      * this reason there is accessor methods to access the values as
408      * both byte array and String.
409      */
410     public class Extra {
411 
412         private final Map<String, byte[]> map;
413 
414         private Extra(HgInputStream stream) {
415             try {
416                 this.map = Maps.newHashMap();
417                 // The value is binary data, node is used as delimiter
418                 byte[] node = stream.upTo(0);
419                 while (!stream.isEof()) {
420                     String key = stream.textUpTo(0);
421                     byte[] value = stream.upTo(node);
422                     this.map.put(key, value);
423                 }
424             } catch (IOException e) {
425                 throw new RuntimeIOException(e);
426             } finally {
427             	try {
428 					stream.consumeAll();
429 				} catch (IOException e) {
430 					throw new RuntimeIOException(e);
431 				}
432             }
433         }
434 
435         /**
436          * @param key
437          * @return The extra data for the key as a String
438          */
439         public String getString(String key) {
440             byte[] bytes = getBytes(key);
441             if (bytes == null) {
442                 return null;
443             } else {
444                 return decodeBytes(bytes);
445             }
446         }
447 
448         /**
449          * @param key
450          * @return The extra data for the key as byte array
451          */
452         public byte[] getBytes(String key) {
453             return map.get(key);
454         }
455 
456         /**
457          * @return a view on the extra data dictionary where values
458          *         are Strings.
459          */
460         public Map<String, String> stringValuedMap() {
461             Function<byte[], String> f = new Function<byte[], String>() {
462                 public String apply(byte[] input) {
463                     return decodeBytes(input);
464                 }
465             };
466             return Maps.transformValues(this.map, f);
467         }
468 
469         /**
470          * @return a view on the extra data dictionary where values
471          *         are byte arrays.
472          */
473         public Map<String, byte[]> byteArrayValuedMap() {
474             return this.map;
475         }
476 
477     }
478 
479 }
480 
481 
482 class ChangesetFileData {
483   
484   public List<String> addedFiles;
485   public List<String> modifiedFiles;
486   public List<String> deletedFiles;
487 
488   public ChangesetFileData(List<String> addedFiles,
489     List<String> modifiedFiles,
490     List<String> deletedFiles)
491   {
492     this.addedFiles = addedFiles;
493     this.modifiedFiles = modifiedFiles;
494     this.deletedFiles = deletedFiles;
495   }
496   
497 }
498 class ChangesetData {
499     public int revision;
500     public String user;
501     public DateTime timestamp;
502     public String branch;
503     public Changeset parent1;
504     public Changeset parent2;
505     public String message;
506 
507     public ChangesetData(int revision, String user, DateTime timestamp, String branch, Changeset parent1,
508             Changeset parent2, String message) {
509         this.revision = revision;
510         this.user = user;
511         this.timestamp = timestamp;
512         this.branch = branch;
513         this.parent1 = parent1;
514         this.parent2 = parent2;
515         this.message = message;
516     }
517 }