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.File;
29  import java.io.IOException;
30  import java.nio.charset.CharsetDecoder;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.concurrent.ExecutionException;
34  
35  import com.aragost.javahg.RepositoryConfiguration.CachePolicy;
36  import com.aragost.javahg.commands.HeadsCommand;
37  import com.aragost.javahg.commands.LogCommand;
38  import com.aragost.javahg.internals.GenericCommand;
39  import com.aragost.javahg.internals.GenericLogCommand;
40  import com.aragost.javahg.internals.HgInputStream;
41  import com.aragost.javahg.internals.RuntimeIOException;
42  import com.aragost.javahg.internals.ServerPool;
43  import com.aragost.javahg.internals.Utils;
44  import com.google.common.cache.CacheBuilder;
45  import com.google.common.cache.CacheLoader;
46  import com.google.common.cache.CacheStats;
47  import com.google.common.cache.LoadingCache;
48  import com.google.common.collect.Maps;
49  
50  /**
51   * A Mercurial repository.
52   */
53  public abstract class Repository {
54  
55      private final LoadingCache<String, Changeset> changesetCache;
56  
57      protected Repository(CachePolicy cachePolicy) {
58          this.changesetCache = createCache(cachePolicy);
59      }
60  
61      /**
62       * Open an existing Mercurial repository.
63       * 
64       * @param mercurialRepository
65       *            path to the local repository.
66       * @return the repository.
67       */
68      public static BaseRepository open(RepositoryConfiguration conf, File mercurialRepository) {
69          return new BaseRepository(conf, mercurialRepository, false, null);
70      }
71  
72      /**
73       * Create a new Mercurial repository and open a javahg Repository
74       * on it.
75       * 
76       * @param directory
77       *            the local destination path of the clone
78       * @return the newly created repository.
79       */
80      public static BaseRepository create(RepositoryConfiguration conf, File directory) {
81          return new BaseRepository(conf, directory, true, null);
82      }
83  
84      /**
85       * Clone an existing Mercurial repository.
86       * 
87       * @param directory
88       *            the local destination path of the clone
89       * @param otherRepoUrl
90       *            the (possible remote) repository to clone.
91       * @return the newly cloned repository.
92       */
93      public static BaseRepository clone(RepositoryConfiguration conf, File directory, String otherRepoUrl) {
94          return new BaseRepository(conf, directory, false, otherRepoUrl);
95      }
96  
97      /**
98       * Open an existing Mercurial repository. This uses the default
99       * Mercurial binary from {@link RepositoryConfiguration#DEFAULT}.
100      * 
101      * @param mercurialRepository
102      *            the local path to the repository.
103      * @return the repository.
104      */
105     public static BaseRepository open(File mercurialRepository) {
106         return new BaseRepository(RepositoryConfiguration.DEFAULT, mercurialRepository, false, null);
107     }
108 
109     /**
110      * Create a new Mercurial repository. This uses the default
111      * Mercurial binary from {@link RepositoryConfiguration#DEFAULT}.
112      * 
113      * @param directory
114      *            the local destination path of the clone
115      * @return the newly created repository.
116      */
117     public static BaseRepository create(File directory) {
118         return new BaseRepository(RepositoryConfiguration.DEFAULT, directory, true, null);
119     }
120 
121     /**
122      * Clone an existing Mercurial repository. This uses the default
123      * Mercurial binary from {@link RepositoryConfiguration#DEFAULT}.
124      * 
125      * @param directory
126      *            the local destination path of the clone
127      * @param otherRepoUrl
128      *            the (possible remote) repository to clone.
129      * @return the newly cloned repository.
130      */
131     public static BaseRepository clone(File directory, String otherRepoUrl) {
132         return new BaseRepository(RepositoryConfiguration.DEFAULT, directory, false, otherRepoUrl);
133     }
134 
135     /**
136      * @deprecated Use changeset instead.
137      * 
138      * @param node
139      *            a changeset ID, must be 40 hexadecimal characters.
140      * @return the changeset
141      */
142     @Deprecated
143     public final Changeset changeSet(String node) {
144         return changeset(node);
145     }
146 
147     /**
148      * Return a {@link Changeset} object for the specified node id.
149      * <p>
150      * The changeset is not actually loaded, but will be loaded on
151      * demand when a getter is called.
152      * <p>
153      * {@code null} is used to represent the null changeset.
154      * 
155      * @param node
156      *            a changeset ID, must be 40 hexadecimal characters.
157      * @return the changeset
158      */
159     public final Changeset changeset(String node) {
160         if (node.equals(Changeset.NULL_ID)) {
161             return null;
162         } else {
163             try {
164                 return basicChangeset(node);
165             } catch (ExecutionException e) {
166                 throw Utils.asRuntime(e);
167             }
168         }
169     }
170 
171     /**
172      * 
173      * @param node
174      *            a changeset node id, that is assume not to be the
175      *            null id
176      * @return Changeset object with the specified id
177      */
178     protected Changeset basicChangeset(String node) throws ExecutionException {
179         return this.changesetCache.get(node);
180     }
181 
182     /**
183      * @return the command server pool associated with this repository.
184      */
185     public abstract ServerPool getServerPool();
186 
187     /**
188      * @return Get a new decoder for data in this repository
189      */
190     public abstract CharsetDecoder newDecoder();
191 
192     /**
193      * Close the repository. This also stops the command server
194      * associated with the repository.
195      */
196     public void close() {
197         getServerPool().decrementRefCount();
198     }
199 
200     /**
201      * @return the root directory of this repository.
202      */
203     public abstract File getDirectory();
204 
205     public abstract BaseRepository getBaseRepository();
206 
207     @Override
208     public String toString() {
209         return "repo@" + getDirectory();
210     }
211 
212     /**
213      * Add repository specific arguments to the hg command line for
214      * command execution
215      * 
216      * @param commandLine
217      */
218     public void addToCommandLine(List<String> commandLine) {
219         String sshBin = getBaseRepository().getConfiguration().getSshBin();
220         if (sshBin != null) {
221             commandLine.add("--config");
222             commandLine.add("ui.ssh=" + sshBin);
223         }
224     }
225 
226     /**
227      * @return The version string for the Mercurial server
228      */
229     public HgVersion getHgVersion() {
230         return getServerPool().getHgVersion(this);
231     }
232 
233     /**
234      * Create a new WorkingCopy object for this repository.
235      * 
236      * @return a working copy object for this repository
237      */
238     public WorkingCopy workingCopy() {
239         return new WorkingCopy(this);
240     }
241 
242     /**
243      * 
244      * @return heads for the repository
245      */
246     public List<Changeset> heads() {
247         return HeadsCommand.on(this).execute();
248     }
249 
250     /**
251      * @deprecated use phases instead
252      * 
253      * @param revs
254      * @return mapping from Changeset to Phase
255      */
256     @Deprecated
257     public Map<Changeset, Phase> readPhases(String... revs) {
258         return phases(revs);
259     }
260 
261     /**
262      * Return the phases of the specified revisions
263      * 
264      * @param revs
265      * @return Map mapping a {@link Changeset} to a {@link Phase}
266      */
267     public Map<Changeset, Phase> phases(String... revs) {
268         GenericLogCommand cmd = new GenericLogCommand(this).template("{node} {phase}\\0");
269         cmd.rev(revs);
270         HgInputStream stream = cmd.stream();
271         Map<Changeset, Phase> result = Maps.newHashMap();
272         try {
273             while (!stream.isEof()) {
274             	// $ hg log --debug --template "{node} {phase}" --rev 5b80e11a7c32121b5fd926b06056bb773eff050f
275             	// removing unknown node dd8c766936b9 from 1-phase boundary
276             	// 5b80e11a7c32121b5fd926b06056bb773eff050f draft
277             	// Observed with at least Mercurial Distributed SCM (version 2.3+10-9d9d15928521)
278             	String node = stream.textUpTo(' ');
279 
280             	while ("removing".equals(node) && stream.find('\n')) {
281             		node = stream.textUpTo(' ');
282             	}
283             	
284                 String phaseName = stream.textUpTo('\0');
285                 Phase phase = Phase.fromText(phaseName);
286                 result.put(changeset(node), phase);
287             }
288         } catch (IOException e) {
289             throw new RuntimeIOException(e);
290         } finally {
291         	try {
292 				stream.consumeAll();
293 			} catch (IOException e) {
294 				throw new RuntimeIOException(e);
295 			}
296         }
297 
298         return result;
299     }
300 
301     /**
302      * 
303      * @return the tip Changeset for the repository
304      */
305     public Changeset tip() {
306         List<Changeset> changesets = LogCommand.on(this).rev("tip").execute();
307         return Utils.single(changesets);
308     }
309 
310     /**
311      * Convert the specified file object to a file object that is
312      * relative to the root of this repository.
313      * <p>
314      * If the file argument is not absolute it is returned as it is.
315      * 
316      * @param file
317      * @return
318      */
319     public File relativeFile(File file) {
320         if (file.isAbsolute()) {
321             String filePath = Utils.resolveSymlinks(file).getPath();
322             String repoPath = getDirectory().getPath() + File.separator;
323             if (filePath.startsWith(repoPath)) {
324                 return new File(filePath.substring(repoPath.length()));
325             } else {
326                 throw new IllegalArgumentException("" + file + " is not under root of repository");
327             }
328         } else {
329             return file;
330         }
331     }
332 
333     /**
334      * Return a File object for the file name specified in this
335      * repository.
336      * 
337      * @param name
338      * @return the File
339      */
340     public File file(String name) {
341         return new File(getDirectory(), name);
342     }
343 
344     public void lock() {
345         GenericCommand lock = new GenericCommand(this, "javahg-lock");
346         lock.execute();
347     }
348 
349     public void unlock() {
350         GenericCommand unlock = new GenericCommand(this, "javahg-unlock");
351         unlock.execute();
352     }
353 
354     private LoadingCache<String, Changeset> createCache(CachePolicy cachePolicy) {
355         CacheLoader<String, Changeset> cacheLoader = new CacheLoader<String, Changeset>() {
356             @Override
357             public Changeset load(String key) throws Exception {
358                 return new Changeset(Repository.this, key);
359             }
360         };
361         CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder();
362         switch (cachePolicy) {
363         case STRONG:
364             break;
365         case SOFT:
366             cacheBuilder.softValues();
367             break;
368         case WEAK:
369             cacheBuilder.weakValues();
370             break;
371         case NONE:
372             // Consider not to have any cache at all
373             cacheBuilder.maximumSize(0);
374             break;
375         }
376         return cacheBuilder.build(cacheLoader);
377     }
378 
379     protected LoadingCache<String, Changeset> getChangesetCache() {
380         return changesetCache;
381     }
382 
383     protected Changeset getChangesetIfInCache(String node) {
384         return this.changesetCache.getIfPresent(node);
385     }
386 
387     public CacheStats getCacheStats() {
388         return this.changesetCache.stats();
389     }
390 
391 }