Coverage Report - com.aragost.javahg.Changeset
 
Classes in this File Line Coverage Branch Coverage Complexity
Changeset
83%
120/143
67%
31/46
2.211
Changeset$1
N/A
N/A
2.211
Changeset$Extra
72%
18/25
75%
3/4
2.211
Changeset$Extra$1
100%
2/2
N/A
2.211
ChangesetData
100%
9/9
N/A
2.211
ChangesetFileData
100%
5/5
N/A
2.211
 
 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  6
 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  1
     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  1
     public static final String CHANGESET_STYLE_PATH = Utils.resourceAsFile("/styles/changesets.style",
 70  
             ImmutableMap.of("pattern", CHANGESET_PATTERN)).getPath();
 71  
     
 72  1
     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  967
     public Changeset(Repository repository, String node) {
 104  967
         this.repository = repository;
 105  967
         this.node = node;
 106  967
     }
 107  
 
 108  
     private static Changeset createFromInputStream(Repository repository, HgInputStream in, boolean eager) throws IOException {
 109  110
         byte[] node = in.next(40);
 110  110
         String nodeString = new String(node);
 111  110
         int revision = in.revisionUpTo('\n');
 112  110
         Changeset cset = repository.changeset(nodeString);
 113  110
         String user = in.textUpTo('\n');
 114  110
         DateTime timestamp = in.dateTimeUpTo('\n');
 115  110
         String branch = in.textUpTo('\n');
 116  110
         in.upTo(':');
 117  110
         String p1 = in.nextAsText(40);
 118  110
         in.upTo(':');
 119  110
         String p2 = in.nextAsText(40);
 120  110
         in.mustMatch(' '); // skip space part of {parents}
 121  110
         Changeset parent1 = repository.changeset(p1);
 122  110
         Changeset parent2 = repository.changeset(p2);
 123  
         
 124  
         // read files
 125  110
         if ( eager ){
 126  1
           Builder<String> addedBuilder = ImmutableList.builder();
 127  1
           Builder<String> modifiedBuilder = ImmutableList.builder();
 128  1
           Builder<String> deletedBuilder = ImmutableList.builder();
 129  
 
 130  1
           String line = in.textUpTo('\n');
 131  2
           while ( line.length() > 0 ) {
 132  1
             if ( line.startsWith("a ") ){
 133  1
               addedBuilder.add( line.substring(2) );
 134  0
             } else if ( line.startsWith("m ") ){
 135  0
               modifiedBuilder.add( line.substring(2) );
 136  0
             } else if ( line.startsWith("d ")){
 137  0
               deletedBuilder.add( line.substring(2) );
 138  
             }
 139  1
             line = in.textUpTo('\n');
 140  
           }
 141  
           
 142  1
           ChangesetFileData fileData = cset.fileData;
 143  1
           if (fileData == null){
 144  1
             fileData = new ChangesetFileData(addedBuilder.build(), modifiedBuilder.build(), deletedBuilder.build());
 145  1
             cset.fileData = fileData;
 146  
           }
 147  
         }
 148  110
         String message = in.textUpTo('\0');
 149  
 
 150  110
         if (cset == null) {
 151  
             // Revision -1:000000000000
 152  0
             return null;
 153  
         }
 154  
 
 155  110
         ChangesetData data = cset.data;
 156  110
         if (data == null) {
 157  79
             data = new ChangesetData(revision, user, timestamp, branch, parent1, parent2, 
 158  
                        message);
 159  79
             cset.data = data;
 160  31
         } else if (revision != data.revision) {
 161  
             // Handle revision specially because revision is not part of the node
 162  0
             data.revision = revision;
 163  
         }
 164  110
         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  42
       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  79
         List<Changeset> changesets = Lists.newArrayList();
 193  
         try {
 194  79
             boolean found = in.find(CHANGESET_PATTERN);
 195  79
             if (found) {
 196  183
                 while (!in.match(CHANGESET_PATTERN)) {
 197  110
                     Changeset cset = Changeset.createFromInputStream(repository, in, eager);
 198  
 
 199  110
                     if (cset != null) {
 200  110
                         changesets.add(cset);
 201  
                     }
 202  110
                 }
 203  
             }
 204  
             // If the pattern is not found there is no changsets
 205  0
         } catch (IOException e) {
 206  0
             throw new RuntimeIOException(e);
 207  
         } finally {
 208  0
            try {
 209  79
               Utils.consumeAll(in);
 210  0
            } catch (IOException e) {
 211  0
               throw new RuntimeIOException(e);
 212  79
            }
 213  
         }
 214  79
         return changesets;
 215  
     }
 216  
 
 217  
     public String getNode() {
 218  631
         return this.node;
 219  
     }
 220  
 
 221  
     public int getRevision() {
 222  10
         ensureAllDataLoaded();
 223  10
         return this.data.revision;
 224  
     }
 225  
 
 226  
     public String getUser() {
 227  7
         ensureAllDataLoaded();
 228  7
         return this.data.user;
 229  
     }
 230  
 
 231  
     public DateTime getTimestamp() {
 232  1
         ensureAllDataLoaded();
 233  1
         return this.data.timestamp;
 234  
     }
 235  
 
 236  
     public String getBranch() {
 237  2
         ensureAllDataLoaded();
 238  2
         return this.data.branch;
 239  
     }
 240  
 
 241  
     public Changeset getParent1() {
 242  4
         ensureAllDataLoaded();
 243  4
         return this.data.parent1;
 244  
     }
 245  
 
 246  
     public Changeset getParent2() {
 247  3
         ensureAllDataLoaded();
 248  3
         return this.data.parent2;
 249  
     }
 250  
 
 251  
     public String getMessage() {
 252  5
         ensureAllDataLoaded();
 253  5
         return this.data.message;
 254  
     }
 255  
     
 256  
     public List<String> getAddedFiles(){
 257  4
         ensureFileDataLoaded();
 258  4
         return this.fileData.addedFiles;
 259  
     }
 260  
     
 261  
     public List<String> getModifiedFiles(){
 262  2
         ensureFileDataLoaded();
 263  2
         return this.fileData.modifiedFiles;
 264  
     }
 265  
     
 266  
     public List<String> getDeletedFiles(){
 267  1
         ensureFileDataLoaded();
 268  1
         return this.fileData.deletedFiles;
 269  
     }
 270  
     
 271  
     private void loadFileData(){
 272  4
       StatusResult result = new StatusCommand(repository).added().modified()
 273  
                                   .removed().change(this.node).execute();
 274  4
       if ( result != null ){
 275  4
         fileData = new ChangesetFileData(result.getAdded(), result.getModified(), 
 276  
                                          result.getRemoved());
 277  
       } else {
 278  0
         throw new IllegalStateException("could not load file data from status");
 279  
       }
 280  4
     }
 281  
     
 282  
     private void ensureFileDataLoaded(){
 283  7
         if (this.fileData != null) {
 284  3
           return;
 285  
         }
 286  4
         loadFileData();
 287  4
         if (this.fileData == null) {
 288  0
             throw new IllegalStateException("could not load file data");
 289  
         }
 290  4
     }
 291  
 
 292  
     private void ensureAllDataLoaded() {
 293  32
         if (this.data != null) {
 294  24
             return;
 295  
         }
 296  8
         LogCommand.on(this.repository).rev(getNode()).execute();
 297  8
         if (this.data == null) {
 298  0
             throw new IllegalStateException("data was not loaded");
 299  
         }
 300  8
     }
 301  
 
 302  
     @Deprecated
 303  
     public Phase readPhase() {
 304  0
         return phase();
 305  
     }
 306  
 
 307  
     /**
 308  
      * 
 309  
      * @return the phase for this changeset.
 310  
      */
 311  
     public Phase phase() {
 312  10
         Map<Changeset, Phase> phases = getRepository().phases(getNode());
 313  10
         return phases.get(this);
 314  
     }
 315  
 
 316  
     /**
 317  
      * Return tags that is pointing the this changeset
 318  
      */
 319  
     public List<String> tags() {
 320  3
         GenericLogCommand cmd = new GenericLogCommand(getRepository()).style("tags");
 321  3
         cmd.rev(getNode());
 322  3
         HgInputStream stream = cmd.stream();
 323  3
         List<String> result = Lists.newArrayList();
 324  
         try {
 325  7
             while (!stream.isEof()) {
 326  4
                 String tag = stream.textUpTo(0);
 327  4
                 if (!"tip".equals(tag)) {
 328  3
                     result.add(tag);
 329  
                 }
 330  4
             }
 331  0
         } catch (IOException e) {
 332  0
             throw new RuntimeIOException(e);
 333  
         } finally {
 334  0
                 try {
 335  3
                                 stream.consumeAll();
 336  0
                         } catch (IOException e) {
 337  0
                                 throw new RuntimeIOException(e);
 338  3
                         }
 339  
         }
 340  3
         return result;
 341  
     }
 342  
 
 343  
     /**
 344  
      * 
 345  
      * @return Mercurial's extra dictionary
 346  
      */
 347  
     public synchronized Extra getExtra() {
 348  9
         if (this.extra == null) {
 349  9
             GenericLogCommand cmd = new GenericLogCommand(getRepository()).style("extras");
 350  9
             cmd.rev(getNode());
 351  9
             this.extra = new Extra(cmd.stream());
 352  
         }
 353  9
         return this.extra;
 354  
     }
 355  
 
 356  
     @Override
 357  
     public boolean equals(Object that) {
 358  50
         if (that instanceof Changeset) {
 359  49
             return equals((Changeset) that);
 360  
         } else {
 361  1
             return false;
 362  
         }
 363  
     }
 364  
 
 365  
     public boolean equals(Changeset that) {
 366  53
         if (this.repository != that.repository) {
 367  1
             return false;
 368  
         }
 369  52
         return this.getNode().equals(that.getNode());
 370  
     }
 371  
 
 372  
     @Override
 373  
     public int hashCode() {
 374  34
         return getNode().hashCode();
 375  
     }
 376  
 
 377  
     @Override
 378  
     public String toString() {
 379  0
         StringBuilder builder = new StringBuilder("changeset[");
 380  0
         builder.append(this.data == null ? "?" : this.data.revision).append(':').append(this.node).append(']');
 381  0
         return builder.toString();
 382  
     }
 383  
 
 384  
     @VisibleForTesting
 385  
     ChangesetData getData() {
 386  3
         return this.data;
 387  
     }
 388  
     
 389  
     @VisibleForTesting
 390  
     ChangesetFileData getFileData(){
 391  2
       return this.fileData;
 392  
     }
 393  
 
 394  
     Repository getRepository() {
 395  38
         return repository;
 396  
     }
 397  
 
 398  
     private String decodeBytes(byte[] bytes) {
 399  6
         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  9
     public class Extra {
 411  
 
 412  
         private final Map<String, byte[]> map;
 413  
 
 414  9
         private Extra(HgInputStream stream) {
 415  
             try {
 416  9
                 this.map = Maps.newHashMap();
 417  
                 // The value is binary data, node is used as delimiter
 418  9
                 byte[] node = stream.upTo(0);
 419  22
                 while (!stream.isEof()) {
 420  13
                     String key = stream.textUpTo(0);
 421  13
                     byte[] value = stream.upTo(node);
 422  13
                     this.map.put(key, value);
 423  13
                 }
 424  0
             } catch (IOException e) {
 425  0
                 throw new RuntimeIOException(e);
 426  
             } finally {
 427  0
                     try {
 428  9
                                         stream.consumeAll();
 429  0
                                 } catch (IOException e) {
 430  0
                                         throw new RuntimeIOException(e);
 431  9
                                 }
 432  
             }
 433  9
         }
 434  
 
 435  
         /**
 436  
          * @param key
 437  
          * @return The extra data for the key as a String
 438  
          */
 439  
         public String getString(String key) {
 440  4
             byte[] bytes = getBytes(key);
 441  4
             if (bytes == null) {
 442  0
                 return null;
 443  
             } else {
 444  4
                 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  4
             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  1
             Function<byte[], String> f = new Function<byte[], String>() {
 462  
                 public String apply(byte[] input) {
 463  2
                     return decodeBytes(input);
 464  
                 }
 465  
             };
 466  1
             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  0
             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  5
   {
 492  5
     this.addedFiles = addedFiles;
 493  5
     this.modifiedFiles = modifiedFiles;
 494  5
     this.deletedFiles = deletedFiles;
 495  5
   }
 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  79
             Changeset parent2, String message) {
 509  79
         this.revision = revision;
 510  79
         this.user = user;
 511  79
         this.timestamp = timestamp;
 512  79
         this.branch = branch;
 513  79
         this.parent1 = parent1;
 514  79
         this.parent2 = parent2;
 515  79
         this.message = message;
 516  79
     }
 517  
 }