View Javadoc

1   /**
2    * Copyright (C) 2008 Atlassian
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    http://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,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.atlassian.theplugin.idea;
18  
19  import com.atlassian.theplugin.commons.crucible.api.model.CommitType;
20  import com.atlassian.theplugin.commons.util.MiscUtil;
21  import com.intellij.openapi.actionSystem.AnActionEvent;
22  import com.intellij.openapi.actionSystem.DataContext;
23  import com.intellij.openapi.application.ApplicationManager;
24  import com.intellij.openapi.fileEditor.OpenFileDescriptor;
25  import com.intellij.openapi.progress.ProgressIndicator;
26  import com.intellij.openapi.progress.Task;
27  import com.intellij.openapi.project.Project;
28  import com.intellij.openapi.ui.Messages;
29  import com.intellij.openapi.vcs.AbstractVcs;
30  import com.intellij.openapi.vcs.CommittedChangesProvider;
31  import com.intellij.openapi.vcs.ProjectLevelVcsManager;
32  import com.intellij.openapi.vcs.RepositoryLocation;
33  import com.intellij.openapi.vcs.VcsException;
34  import com.intellij.openapi.vcs.changes.ContentRevision;
35  import com.intellij.openapi.vcs.diff.DiffProvider;
36  import com.intellij.openapi.vcs.history.VcsFileRevision;
37  import com.intellij.openapi.vcs.history.VcsRevisionNumber;
38  import com.intellij.openapi.vcs.vfs.VcsVirtualFile;
39  import com.intellij.openapi.vfs.VfsUtil;
40  import com.intellij.openapi.vfs.VirtualFile;
41  import com.intellij.vcsUtil.VcsUtil;
42  import org.jetbrains.annotations.NotNull;
43  import org.jetbrains.annotations.Nullable;
44  
45  import java.util.List;
46  import java.util.Map;
47  
48  public final class VcsIdeaHelper {
49  
50  	private static final int INITIAL_CAPACITY = 20;
51  	private static Map<String, VirtualFile> fetchedFiles = MiscUtil.buildConcurrentHashMap(INITIAL_CAPACITY);
52  
53  	private VcsIdeaHelper() {
54  	}
55  
56  	public static boolean isUnderVcsControl(DataContext context) {
57  		return isUnderVcsControl(IdeaHelper.getCurrentProject(context));
58  	}
59  
60  	public static boolean isUnderVcsControl(AnActionEvent action) {
61  		return isUnderVcsControl(action.getDataContext());
62  	}
63  
64  	public static boolean isUnderVcsControl(Project project) {
65  		ProjectLevelVcsManager plm = ProjectLevelVcsManager.getInstance(project);
66  
67  		return (plm != null && plm.getAllActiveVcss().length > 0);
68  	}
69  
70  
71  	@Nullable
72  	public static String getRepositoryUrlForFile(Project project, VirtualFile vFile) {
73  		ProjectLevelVcsManager plm = ProjectLevelVcsManager.getInstance(project);
74  		if (plm == null) {
75  			return null;
76  		}
77  		AbstractVcs vcs = plm.getVcsFor(vFile);
78  		if (vcs == null) {
79  			return null;
80  		}
81  		CommittedChangesProvider<?, ?> provider = vcs.getCommittedChangesProvider();
82  		if (provider == null) {
83  			return null;
84  		}
85  		RepositoryLocation repositoryLocation
86  				= provider.getLocationFor(VcsUtil.getFilePath(vFile.getPath()));
87  		if (repositoryLocation == null) {
88  			return null;
89  		}
90  		return repositoryLocation.toPresentableString();
91  	}
92  
93  	public static List<VcsFileRevision> getFileHistory(Project project, VirtualFile vFile) throws VcsException {
94  		ProjectLevelVcsManager vcsPLM = ProjectLevelVcsManager.getInstance(project);
95  
96  		if (vcsPLM != null) {
97  			return vcsPLM.getVcsFor(vFile).getVcsHistoryProvider().createSessionFor(
98  					VcsUtil.getFilePath(vFile.getPath())).getRevisionList();
99  		} else {
100 			throw new VcsException("File: " + vFile.getPath() + " is not under VCS.");
101 		}
102 	}
103 
104 	private static String getFileCacheKey(VirtualFile file, String revision) {
105 		return revision + ":" + file.getPath();
106 	}
107 
108 	private static VirtualFile getFileFromCache(VirtualFile virtualFile, String revision) {
109 		String key = getFileCacheKey(virtualFile, revision);
110 		if (fetchedFiles.containsKey(key)) {
111 			return fetchedFiles.get(key);
112 		} else {
113 			return null;
114 		}
115 	}
116 
117 	private static void putFileInfoCache(VirtualFile file, VirtualFile virtualFile, String revision) {
118 		String key = getFileCacheKey(virtualFile, revision);
119 		fetchedFiles.put(key, file);
120 	}
121 
122 	@Nullable
123 	private static VirtualFile getVcsVirtualFile(Project project, VirtualFile virtualFile,
124 			String revision) throws VcsException {
125 		VirtualFile vcvf = getFileFromCache(virtualFile, revision);
126 		if (vcvf != null) {
127 			return vcvf;
128 		} else {
129 			AbstractVcs vcs = VcsUtil.getVcsFor(project, virtualFile);
130 			if (vcs == null) {
131 				return null;
132 			}
133 			VcsRevisionNumber vcsRevisionNumber = vcs.parseRevisionNumber(revision);
134 			vcvf = getVcsVirtualFileImpl2(virtualFile, vcs, vcsRevisionNumber);
135 			putFileInfoCache(vcvf, virtualFile, revision);
136 			return vcvf;
137 		}
138 	}
139 
140 	@Nullable
141 	private static VirtualFile getVcsVirtualFileImpl2(VirtualFile virtualFile, AbstractVcs vcs,
142 			VcsRevisionNumber vcsRevisionNumber) throws VcsException {
143 
144 		DiffProvider diffProvider = vcs.getDiffProvider();
145 		if (diffProvider == null) {
146 			return null;
147 		}
148 		ContentRevision contentRevision = diffProvider.createFileContent(vcsRevisionNumber, virtualFile);
149 		if (contentRevision == null) {
150 			return null;
151 		}
152 
153 		// this operation is typically quite costly
154 		final String content = contentRevision.getContent();
155 		if (content == null) {
156 			return null;
157 		}
158 // we will restore it one day and the the world will be great again :)
159 //		try {
160 //			byte[] currentContent = virtualFile.contentsToByteArray();
161 //			if (Arrays.equals(currentContent, content.getBytes())) {
162 //				return virtualFile;
163 //			}
164 //
165 //		} catch (IOException e) {
166 //			// just resign and try fetch as normally
167 //		}
168 		return new VcsVirtualFile(contentRevision.getFile().getPath(), content.getBytes(),
169 				vcsRevisionNumber.asString(), virtualFile.getFileSystem());
170 	}
171 
172 	/**
173 	 * Must be run from UI thread!
174 	 *
175 	 * @param project	 project
176 	 * @param revision	VCS revision of the file
177 	 * @param virtualFile file to fetch from VCS
178 	 * @param line		line to go to
179 	 * @param column	  column to go to
180 	 * @param action
181 	 */
182 	private static void fetchAndOpenFile(final Project project, final String revision, @NotNull final VirtualFile virtualFile,
183 			final int line, final int column, @Nullable final OpenFileDescriptorAction action) {
184 		VirtualFile file = getFileFromCache(virtualFile, revision);
185 		if (file != null && action != null) {
186 			OpenFileDescriptor fileDescriptor = new OpenFileDescriptor(project, file, line, column);
187 			action.run(fileDescriptor);
188 			return;
189 		}
190 
191 		final String niceFileMessage = virtualFile.getName() + " (rev: " + revision + ") from VCS";
192 		new FetchingFileTask(project, niceFileMessage, virtualFile, revision, line, column, action).queue();
193 	}
194 
195 	/**
196 	 * Must be run from UI thread!
197 	 *
198 	 * @param project	  project
199 	 * @param fromRevision start VCS revision of the file
200 	 * @param toRevision   to VCS revision of the file
201 	 * @param commitType
202 	 * @param virtualFile  file to fetch from VCS
203 	 * @param line		 line to go to
204 	 * @param column	   column to go to
205 	 * @param action action to execute upon sucsessful completion of the fetching 
206 	 */
207 	// CHECKSTYLE:OFF
208 	private static void fetchAndOpenFileWithDiffs(final Project project, final String fromRevision, final String toRevision,
209 			@NotNull final CommitType commitType, @NotNull final VirtualFile virtualFile,
210 			final int line, final int column, @Nullable final OpenDiffAction action) {
211 	// CHECKSTYLE:ON
212 		VirtualFile referenceVirtualFile = getFileFromCache(virtualFile, fromRevision);
213 		VirtualFile displayVirtualFile = getFileFromCache(virtualFile, toRevision);
214 
215 		if (referenceVirtualFile != null
216 				&& displayVirtualFile != null) {
217 			OpenFileDescriptor displayDescriptor = new OpenFileDescriptor(project, displayVirtualFile, line, column);
218 			action.run(displayDescriptor, referenceVirtualFile, commitType);
219 			return;
220 		}
221 
222 		final String niceFileMessage;
223 		switch (commitType) {
224 			case Added:
225 				niceFileMessage = " " + virtualFile.getName() + " (rev: " + toRevision + ") from VCS";
226 				break;
227 			case Deleted:
228 				niceFileMessage = " " + virtualFile.getName() + " (rev: " + fromRevision + ") from VCS";
229 				break;
230 			case Modified:
231 			case Moved:
232 			case Copied:
233 				niceFileMessage = "s" + virtualFile.getName() + " (rev: " + fromRevision + ", " + toRevision + ") from VCS";
234 				break;
235 			case Unknown:
236 				niceFileMessage = "s" + virtualFile.getName() + " (rev: " + fromRevision + ", " + toRevision + ") from VCS";
237 				break;
238 			default:
239 				niceFileMessage = "s" + virtualFile.getName() + " (rev: " + fromRevision + ", " + toRevision + ") from VCS";
240 		}
241 
242 		new FetchingTwoFilesTask(project, niceFileMessage, commitType, virtualFile, fromRevision,
243 				toRevision, line, column, action).queue();
244 	}
245 
246 	/**
247 	 * Is file is currently open it does not try to refetch it
248 	 *
249 	 * @param project
250 	 * @param filePath
251 	 * @param fileRevision
252 	 * @param line
253 	 * @param col
254 	 * @param action
255 	 */
256 	public static void openFile(final Project project, String filePath, @NotNull final String fileRevision,
257 			final int line, final int col, @Nullable final OpenFileDescriptorAction action) {
258 
259 		VirtualFile baseDir = project.getBaseDir();
260 		String baseUrl = getRepositoryUrlForFile(project, baseDir);
261 
262 		if (baseUrl != null && filePath.startsWith(baseUrl)) {
263 			String relUrl = filePath.substring(baseUrl.length());
264 			final VirtualFile vfl = VfsUtil.findRelativeFile(relUrl, baseDir);
265 			ApplicationManager.getApplication().invokeLater(new Runnable() {
266 				public void run() {
267 					fetchAndOpenFile(project, fileRevision, vfl, line, col, action);
268 				}
269 			});
270 		}
271 
272 	}
273 
274 	/**
275 	 * Is file is currently open it does not try to refetch it
276 	 *
277 	 * @param project
278 	 * @param filePath
279 	 * @param fileRevision
280 	 * @param toRevision
281 	 * @param commitType
282 	 * @param line
283 	 * @param col
284 	 * @param action
285 	 */
286 	// CHECKSTYLE:OFF
287 	public static void openFileWithDiffs(final Project project, String filePath, @NotNull final String fileRevision,
288 			final String toRevision, @NotNull final CommitType commitType,
289 			final int line, final int col, @Nullable final OpenDiffAction action) {
290 		// CHECKSTYLE:ON
291 		VirtualFile baseDir = project.getBaseDir();
292 		String baseUrl = getRepositoryUrlForFile(project, baseDir);
293 
294 		if (baseUrl != null && filePath.startsWith(baseUrl)) {
295 			String relUrl = filePath.substring(baseUrl.length());
296 			final VirtualFile vfl = VfsUtil.findRelativeFile(relUrl, baseDir);
297 			if (vfl != null) {
298 
299 				ApplicationManager.getApplication().invokeLater(new Runnable() {
300 					public void run() {
301 						fetchAndOpenFileWithDiffs(project, fileRevision, toRevision, commitType, vfl, line, col, action);
302 					}
303 				});
304 			} else {
305 				ApplicationManager.getApplication().invokeLater(new Runnable() {
306 					public void run() {
307 						switch (commitType) {
308 							case Deleted:
309 								Messages.showErrorDialog(project,
310 										"Your project does not contain requested file. Please update to revision "
311 												+ fileRevision + " before review",
312 										"File removed form repository");
313 
314 								break;
315 							case Added:
316 								Messages.showErrorDialog(project,
317 										"Your project does not contain requested file. Please update to revision "
318 												+ toRevision + " before review",
319 										"Project out of date");
320 								break;
321 							default:
322 								Messages.showErrorDialog(project,
323 										"Your project does not contain requested file. Please update before review",
324 										"Project out of date");
325 								break;
326 						}
327 					}
328 				});
329 
330 			}
331 		} else {
332 			ApplicationManager.getApplication().invokeLater(new Runnable() {
333 				public void run() {
334 					Messages.showErrorDialog(project,
335 							"Your project does not contain requested file.",
336 							"File not found");
337 				}
338 			});
339 		}
340 	}
341 
342 	public static void openFile(final Project project, @NotNull final VirtualFile virtualFile,
343 			@NotNull final String fileRevision, final int line, final int col,
344 			@Nullable final OpenFileDescriptorAction action) {
345 
346 		// do we have the same file revision opened in our project?
347 		ApplicationManager.getApplication().invokeLater(new Runnable() {
348 			public void run() {
349 				fetchAndOpenFile(project, fileRevision, virtualFile, line, col, action);
350 			}
351 		});
352 	}
353 
354 	public interface OpenFileDescriptorAction {
355 		/**
356 		 * Will be always invoked in UI thread
357 		 *
358 		 * @param ofd description which will be passed to this action
359 		 */
360 		void run(OpenFileDescriptor ofd);
361 	}
362 
363 	public interface OpenDiffAction {
364 		/**
365 		 * Open file view based on two file revisions
366 		 * Will be always invoked in UI thread
367 		 *
368 		 * @param displayFile
369 		 * @param referenceDocument
370 		 */
371 		void run(OpenFileDescriptor displayFile, VirtualFile referenceDocument, CommitType commitType);
372 	}
373 
374 	private static class FetchingTwoFilesTask extends Task.Backgroundable {
375 		private OpenFileDescriptor displayDescriptor;
376 		private VirtualFile referenceVirtualFile;
377 
378 		private VcsException exception;
379 		private final Project project;
380 		private final String niceFileMessage;
381 		private final CommitType commitType;
382 		private final VirtualFile virtualFile;
383 		private final String fromRevision;
384 		private final String toRevision;
385 		private final int line;
386 		private final int column;
387 		private final OpenDiffAction action;
388 
389 		public FetchingTwoFilesTask(final Project project, final String niceFileMessage, final CommitType commitType,
390 				final VirtualFile virtualFile, final String fromRevision, final String toRevision, final int line,
391 				final int column,
392 				final OpenDiffAction action) {
393 			super(project, "Fetching file" + niceFileMessage, false);
394 			this.project = project;
395 			this.niceFileMessage = niceFileMessage;
396 			this.commitType = commitType;
397 			this.virtualFile = virtualFile;
398 			this.fromRevision = fromRevision;
399 			this.toRevision = toRevision;
400 			this.line = line;
401 			this.column = column;
402 			this.action = action;
403 			displayDescriptor = null;
404 			referenceVirtualFile = null;
405 		}
406 
407 		@Override
408 		public boolean shouldStartInBackground() {
409 			return false;
410 		}
411 
412 		@Override
413 		public void run(ProgressIndicator indicator) {
414 			indicator.setIndeterminate(false);
415 			VirtualFile displayVirtualFile = null;
416 			try {
417 				switch (commitType) {
418 					case Modified:
419 					case Moved:
420 					case Copied:
421 						referenceVirtualFile = getVcsVirtualFile(project, virtualFile, fromRevision);
422 						displayVirtualFile = getVcsVirtualFile(project, virtualFile, toRevision);
423 						break;
424 					case Added:
425 						displayVirtualFile = getVcsVirtualFile(project, virtualFile, toRevision);
426 						break;
427 					case Deleted:
428 						referenceVirtualFile = getVcsVirtualFile(project, virtualFile, fromRevision);
429 						displayVirtualFile = getVcsVirtualFile(project, virtualFile, toRevision);
430 						break;
431 					default:
432 						break;
433 				}
434 				if (displayVirtualFile != null) {
435 					displayDescriptor = new OpenFileDescriptor(project, displayVirtualFile, line, column);
436 				} else {
437 					// we cannot open such file (just for clarity as by default displayDescriptor = null)
438 					displayDescriptor = null;
439 				}
440 			} catch (VcsException e) {
441 				exception = e;
442 			}
443 		}
444 
445 		@Override
446 		public void onSuccess() {
447 			if (exception != null) {
448 				Messages.showErrorDialog(project, "The following error has occured while fetching "
449 						+ niceFileMessage + ":\n" + exception.getMessage(), "Error fetching file");
450 				return;
451 			}
452 			if (displayDescriptor != null) {
453 				if (action != null) {
454 					action.run(displayDescriptor, referenceVirtualFile, commitType);
455 				}
456 				displayDescriptor.navigate(true);
457 			}
458 
459 		}
460 	}
461 
462 	private static class FetchingFileTask extends Task.Backgroundable {
463 
464 		private OpenFileDescriptor ofd;
465 
466 		private VcsException exception;
467 		private final Project project;
468 		private final String niceFileMessage;
469 		private final VirtualFile virtualFile;
470 		private final String revision;
471 		private final int line;
472 		private final int column;
473 		private final OpenFileDescriptorAction action;
474 
475 		public FetchingFileTask(final Project project, final String niceFileMessage, final VirtualFile virtualFile,
476 				final String revision,
477 				final int line, final int column, final OpenFileDescriptorAction action) {
478 			super(project, "Fetching file " + niceFileMessage, false);
479 			this.project = project;
480 			this.niceFileMessage = niceFileMessage;
481 			this.virtualFile = virtualFile;
482 			this.revision = revision;
483 			this.line = line;
484 			this.column = column;
485 			this.action = action;
486 		}
487 
488 		@Override
489 		public boolean shouldStartInBackground() {
490 			return false;
491 		}
492 
493 		@Override
494 		public void run(ProgressIndicator indicator) {
495 
496 			indicator.setIndeterminate(true);
497 			final VirtualFile myVirtualFile;
498 			try {
499 				myVirtualFile = getVcsVirtualFile(project, virtualFile, revision);
500 			} catch (VcsException e) {
501 				exception = e;
502 				return;
503 			}
504 			if (myVirtualFile != null) {
505 				ofd = new OpenFileDescriptor(project, myVirtualFile, line, column);
506 			}
507 		}
508 
509 		@Override
510 		public void onSuccess() {
511 			if (exception != null) {
512 				Messages.showErrorDialog(project, "The following error has occured while fetching "
513 						+ niceFileMessage + ":\n" + exception.getMessage(), "Error fetching file");
514 				return;
515 			}
516 			if (ofd != null) {
517 				if (action != null) {
518 					action.run(ofd);
519 				}
520 				ofd.navigate(true);
521 			}
522 
523 		}
524 	}
525 }