Git Import Wizard
[egit.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / clone / GitProjectsImportPage.java
blobcf3ae8f1ad0691442628e6d4958d4601483cb80d
1 package org.eclipse.egit.ui.internal.clone;
3 /*******************************************************************************
4 * Copyright (c) 2004, 2008 IBM Corporation and others.
5 * Copyright (C) 2007, Martin Oberhuber (martin.oberhuber@windriver.com)
6 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
7 * Copyright (C) 2009, Mykola Nikishov <mn@mn.com.ua>
8 * Copyright (C) 2010, Wim Jongman <wim.jongman@remainsoftware.com>
10 * All rights reserved. This program and the accompanying materials
11 * are made available under the terms of the Eclipse Public License v1.0
12 * which accompanies this distribution, and is available at
13 * http://www.eclipse.org/legal/epl-v10.html
14 *******************************************************************************/
16 import java.io.File;
17 import java.io.IOException;
18 import java.lang.reflect.InvocationTargetException;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.HashSet;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Set;
26 import org.eclipse.core.resources.IProject;
27 import org.eclipse.core.resources.IProjectDescription;
28 import org.eclipse.core.resources.IResource;
29 import org.eclipse.core.resources.IWorkspace;
30 import org.eclipse.core.resources.ResourcesPlugin;
31 import org.eclipse.core.runtime.CoreException;
32 import org.eclipse.core.runtime.IPath;
33 import org.eclipse.core.runtime.IProgressMonitor;
34 import org.eclipse.core.runtime.IStatus;
35 import org.eclipse.core.runtime.OperationCanceledException;
36 import org.eclipse.core.runtime.Path;
37 import org.eclipse.core.runtime.Platform;
38 import org.eclipse.core.runtime.Status;
39 import org.eclipse.core.runtime.SubProgressMonitor;
40 import org.eclipse.egit.ui.Activator;
41 import org.eclipse.egit.ui.UIText;
42 import org.eclipse.jface.dialogs.Dialog;
43 import org.eclipse.jface.operation.IRunnableWithProgress;
44 import org.eclipse.jface.viewers.ITreeContentProvider;
45 import org.eclipse.jface.viewers.LabelProvider;
46 import org.eclipse.jface.viewers.TreeViewer;
47 import org.eclipse.jface.viewers.Viewer;
48 import org.eclipse.jface.viewers.ViewerComparator;
49 import org.eclipse.jface.wizard.WizardPage;
50 import org.eclipse.osgi.util.NLS;
51 import org.eclipse.swt.SWT;
52 import org.eclipse.swt.events.MouseAdapter;
53 import org.eclipse.swt.events.MouseEvent;
54 import org.eclipse.swt.events.SelectionAdapter;
55 import org.eclipse.swt.events.SelectionEvent;
56 import org.eclipse.swt.graphics.Point;
57 import org.eclipse.swt.layout.GridData;
58 import org.eclipse.swt.layout.GridLayout;
59 import org.eclipse.swt.widgets.Button;
60 import org.eclipse.swt.widgets.Composite;
61 import org.eclipse.swt.widgets.Display;
62 import org.eclipse.swt.widgets.Label;
63 import org.eclipse.swt.widgets.Tree;
64 import org.eclipse.swt.widgets.TreeItem;
65 import org.eclipse.ui.actions.WorkspaceModifyOperation;
66 import org.eclipse.ui.dialogs.FilteredTree;
67 import org.eclipse.ui.dialogs.PatternFilter;
68 import org.eclipse.ui.statushandlers.StatusManager;
69 import org.eclipse.ui.wizards.datatransfer.IImportStructureProvider;
71 /**
72 * The GitWizardProjectsImportPage is the page that allows the user to import
73 * projects from a particular location. This is a modified copy of the
74 * WizardProjectsImportPage class from the org.eclipse.ui.ide bundle.
76 public class GitProjectsImportPage extends WizardPage {
78 /**
79 * The name of the folder containing metadata information for the workspace.
81 public static final String METADATA_FOLDER = ".metadata"; //$NON-NLS-1$
83 private IImportStructureProvider structureProvider;
85 class ProjectRecord {
86 File projectSystemFile;
88 String projectName;
90 Object parent;
92 int level;
94 IProjectDescription description;
96 /**
97 * Create a record for a project based on the info in the file.
99 * @param file
101 ProjectRecord(File file) {
102 projectSystemFile = file;
103 setProjectName();
107 * @param parent
108 * The parent folder of the .project file
109 * @param level
110 * The number of levels deep in the provider the file is
112 ProjectRecord(Object parent, int level) {
113 this.parent = parent;
114 this.level = level;
115 setProjectName();
119 * Set the name of the project based on the projectFile.
121 private void setProjectName() {
122 try {
123 // If we don't have the project name try again
124 if (projectName == null) {
125 IPath path = new Path(projectSystemFile.getPath());
126 // if the file is in the default location, use the directory
127 // name as the project name
128 if (isDefaultLocation(path)) {
129 projectName = path.segment(path.segmentCount() - 2);
130 description = ResourcesPlugin.getWorkspace()
131 .newProjectDescription(projectName);
132 } else {
133 description = ResourcesPlugin.getWorkspace()
134 .loadProjectDescription(path);
135 projectName = description.getName();
139 } catch (CoreException e) {
140 // no good couldn't get the name
145 * Returns whether the given project description file path is in the
146 * default location for a project
148 * @param path
149 * The path to examine
150 * @return Whether the given path is the default location for a project
152 private boolean isDefaultLocation(IPath path) {
153 // The project description file must at least be within the project,
154 // which is within the workspace location
155 if (path.segmentCount() < 2)
156 return false;
157 return path.removeLastSegments(2).toFile().equals(
158 Platform.getLocation().toFile());
162 * Get the name of the project
164 * @return String
166 public String getProjectName() {
167 return projectName;
171 * Gets the label to be used when rendering this project record in the
172 * UI.
174 * @return String the label
175 * @since 3.4
177 public String getProjectLabel() {
178 if (description == null)
179 return projectName;
181 String path = projectSystemFile == null ? structureProvider
182 .getLabel(parent) : projectSystemFile.getParent();
184 return NLS.bind(UIText.WizardProjectsImportPage_projectLabel,
185 projectName, path);
189 private TreeViewer projectsList;
191 private ProjectRecord[] selectedProjects = new ProjectRecord[0];
193 final private HashSet<Object> checkedItems = new HashSet<Object>();
195 private IProject[] wsProjects;
197 // The last selected path to minimize searches
198 private String lastPath;
200 // The last time that the file or folder at the selected path was modified
201 // to minimize searches
202 private long lastModified;
204 private Button selectAll;
206 private Button deselectAll;
209 * Creates a new project creation wizard page.
211 public GitProjectsImportPage() {
212 super(GitProjectsImportPage.class.getName());
213 setPageComplete(false);
214 setTitle(UIText.WizardProjectsImportPage_ImportProjectsTitle);
215 setDescription(UIText.WizardProjectsImportPage_ImportProjectsDescription);
218 public void createControl(Composite parent) {
220 initializeDialogUnits(parent);
222 Composite workArea = new Composite(parent, SWT.NONE);
223 setControl(workArea);
225 workArea.setLayout(new GridLayout());
226 workArea.setLayoutData(new GridData(GridData.FILL_BOTH
227 | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL));
229 createProjectsRoot(workArea);
230 createProjectsList(workArea);
231 Dialog.applyDialogFont(workArea);
236 * Create the checkbox list for the found projects.
238 * @param workArea
240 private void createProjectsList(Composite workArea) {
242 checkedItems.clear();
244 Label title = new Label(workArea, SWT.NONE);
245 title.setText(UIText.WizardProjectsImportPage_ProjectsListTitle);
247 Composite listComposite = new Composite(workArea, SWT.NONE);
248 GridLayout layout = new GridLayout();
249 layout.numColumns = 2;
250 layout.marginWidth = 0;
251 layout.makeColumnsEqualWidth = false;
252 listComposite.setLayout(layout);
254 listComposite.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL
255 | GridData.GRAB_VERTICAL | GridData.FILL_BOTH));
257 PatternFilter filter = new PatternFilter() {
259 @Override
260 public boolean isElementVisible(Viewer viewer, Object element) {
262 if (checkedItems.contains(element)) {
263 return true;
266 return super.isElementVisible(viewer, element);
269 @Override
270 public void setPattern(String patternString) {
271 super.setPattern(patternString);
272 // TODO: is there a better way to react on changes in the tree?
273 // disable select all button when tree becomes empty due to
274 // filtering
275 Display.getDefault().asyncExec(new Runnable() {
276 public void run() {
277 enableSelectAllButtons();
284 // we have to use the old constructor in order to be 3.4 compatible
285 // TODO once we drop 3.4 support, we should change to the new constructor
286 FilteredTree filteredTree = new FilteredTree(listComposite, SWT.CHECK
287 | SWT.BORDER, filter);
288 filteredTree.setInitialText(UIText.WizardProjectsImportPage_filterText);
289 projectsList = filteredTree.getViewer();
290 GridData listData = new GridData(GridData.GRAB_HORIZONTAL
291 | GridData.GRAB_VERTICAL | GridData.FILL_BOTH);
292 projectsList.getControl().setLayoutData(listData);
294 projectsList.setContentProvider(new ITreeContentProvider() {
296 public Object[] getChildren(Object parentElement) {
297 return null;
300 public Object[] getElements(Object inputElement) {
301 return getValidProjects();
304 public boolean hasChildren(Object element) {
305 return false;
308 public Object getParent(Object element) {
309 return null;
312 public void dispose() {
313 // ignore
316 public void inputChanged(Viewer viewer, Object oldInput,
317 Object newInput) {
318 // ignore
323 projectsList.getTree().addMouseListener(new MouseAdapter() {
324 @Override
325 public void mouseUp(MouseEvent e) {
326 if (e.widget instanceof Tree) {
327 TreeItem item = ((Tree) e.widget).getItem(new Point(e.x,
328 e.y));
329 if (item != null) {
330 if (item.getChecked())
331 checkedItems.add(item.getData());
332 else
333 checkedItems.remove(item.getData());
334 setPageComplete(!checkedItems.isEmpty());
340 projectsList.setLabelProvider(new LabelProvider() {
341 public String getText(Object element) {
342 // Need to set the checked item state. FIXME This is clumsy.
343 for (final TreeItem item : projectsList.getTree().getItems()) {
344 if (checkedItems.contains(item.getData()))
345 item.setChecked(true);
346 else
347 item.setChecked(false);
349 return ((ProjectRecord) element).getProjectLabel();
353 projectsList.setInput(this);
354 projectsList.setComparator(new ViewerComparator());
355 createSelectionButtons(listComposite);
359 * Create the selection buttons in the listComposite.
361 * @param listComposite
363 private void createSelectionButtons(Composite listComposite) {
364 Composite buttonsComposite = new Composite(listComposite, SWT.NONE);
365 GridLayout layout = new GridLayout();
366 layout.marginWidth = 0;
367 layout.marginHeight = 0;
368 buttonsComposite.setLayout(layout);
370 buttonsComposite.setLayoutData(new GridData(
371 GridData.VERTICAL_ALIGN_BEGINNING));
373 selectAll = new Button(buttonsComposite, SWT.PUSH);
374 selectAll.setText(UIText.WizardProjectsImportPage_selectAll);
375 selectAll.addSelectionListener(new SelectionAdapter() {
376 public void widgetSelected(SelectionEvent e) {
377 checkedItems.clear();
378 // only the root has children
379 for (final TreeItem item : projectsList.getTree().getItems()) {
380 item.setChecked(true);
381 checkedItems.add(item.getData());
383 setPageComplete(true);
386 Dialog.applyDialogFont(selectAll);
387 setButtonLayoutData(selectAll);
389 deselectAll = new Button(buttonsComposite, SWT.PUSH);
390 deselectAll.setText(UIText.WizardProjectsImportPage_deselectAll);
391 deselectAll.addSelectionListener(new SelectionAdapter() {
392 public void widgetSelected(SelectionEvent e) {
393 checkedItems.clear();
394 // only the root has children
395 for (final TreeItem item : projectsList.getTree().getItems()) {
396 item.setChecked(false);
398 projectsList.setInput(this); // filter away selected projects
399 setPageComplete(false);
402 Dialog.applyDialogFont(deselectAll);
403 setButtonLayoutData(deselectAll);
408 * Create the area where you select the root directory for the projects.
410 * @param workArea
411 * Composite
413 private void createProjectsRoot(Composite workArea) {
415 // project specification group
416 Composite projectGroup = new Composite(workArea, SWT.NONE);
417 GridLayout layout = new GridLayout();
418 layout.numColumns = 3;
419 layout.makeColumnsEqualWidth = false;
420 layout.marginWidth = 0;
421 projectGroup.setLayout(layout);
422 projectGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
426 public void setVisible(boolean visible) {
427 super.setVisible(visible);
431 * Update the list of projects based on path. This will not check any
432 * projects.
434 * @param path
436 void setProjectsList(final String path) {
437 // on an empty path empty selectedProjects
438 if (path == null || path.length() == 0) {
439 selectedProjects = new ProjectRecord[0];
440 projectsList.refresh(true);
441 setPageComplete(checkedItems.size() > 0);
442 lastPath = path;
443 setErrorMessage(UIText.GitProjectsImportPage_NoProjectsMessage);
444 return;
447 final File directory = new File(path);
448 long modified = directory.lastModified();
449 if (path.equals(lastPath) && lastModified == modified) {
450 // since the file/folder was not modified and the path did not
451 // change, no refreshing is required
452 return;
455 setErrorMessage(null);
457 lastPath = path;
458 lastModified = modified;
460 try {
461 getContainer().run(true, true, new IRunnableWithProgress() {
463 public void run(IProgressMonitor monitor) {
465 monitor.beginTask(
466 UIText.WizardProjectsImportPage_SearchingMessage,
467 100);
468 selectedProjects = new ProjectRecord[0];
469 Collection<File> files = new ArrayList<File>();
470 monitor.worked(10);
471 if (directory.isDirectory()) {
473 if (!collectProjectFilesFromDirectory(files, directory,
474 null, monitor)) {
475 return;
477 Iterator<File> filesIterator = files.iterator();
478 selectedProjects = new ProjectRecord[files.size()];
479 int index = 0;
480 monitor.worked(50);
481 monitor
482 .subTask(UIText.WizardProjectsImportPage_ProcessingMessage);
483 while (filesIterator.hasNext()) {
484 File file = filesIterator.next();
485 selectedProjects[index] = new ProjectRecord(file);
486 checkedItems.add(selectedProjects[index]);
487 index++;
490 if (files.isEmpty())
491 setErrorMessage(UIText.GitProjectsImportPage_NoProjectsMessage);
492 } else {
493 monitor.worked(60);
495 monitor.done();
499 } catch (InvocationTargetException e) {
500 Activator.logError(e.getMessage(), e);
501 } catch (InterruptedException e) {
502 // Nothing to do if the user interrupts.
505 projectsList.refresh(true);
506 if (getValidProjects().length < selectedProjects.length) {
507 setMessage(UIText.WizardProjectsImportPage_projectsInWorkspace,
508 WARNING);
509 } else {
510 setMessage(UIText.WizardProjectsImportPage_ImportProjectsDescription);
512 enableSelectAllButtons();
513 setPageComplete(checkedItems.size() > 0);
516 private void enableSelectAllButtons() {
517 if (projectsList.getTree().getItemCount() > 0) {
518 selectAll.setEnabled(true);
519 deselectAll.setEnabled(true);
520 } else {
521 selectAll.setEnabled(false);
522 deselectAll.setEnabled(false);
527 * Collect the list of .project files that are under directory into files.
529 * @param files
530 * @param directory
531 * @param directoriesVisited
532 * Set of canonical paths of directories, used as recursion guard
533 * @param monitor
534 * The monitor to report to
535 * @return boolean <code>true</code> if the operation was completed.
537 private boolean collectProjectFilesFromDirectory(Collection<File> files,
538 File directory, Set<String> directoriesVisited,
539 IProgressMonitor monitor) {
541 if (monitor.isCanceled()) {
542 return false;
544 monitor.subTask(NLS.bind(
545 UIText.WizardProjectsImportPage_CheckingMessage, directory
546 .getPath()));
547 File[] contents = directory.listFiles();
548 if (contents == null)
549 return false;
551 // Initialize recursion guard for recursive symbolic links
552 if (directoriesVisited == null) {
553 directoriesVisited = new HashSet<String>();
554 try {
555 directoriesVisited.add(directory.getCanonicalPath());
556 } catch (IOException exception) {
557 StatusManager.getManager().handle(
558 new Status(IStatus.ERROR, Activator.getPluginId(),
559 exception.getLocalizedMessage(), exception));
563 // first look for project description files
564 final String dotProject = IProjectDescription.DESCRIPTION_FILE_NAME;
565 for (int i = 0; i < contents.length; i++) {
566 File file = contents[i];
567 if (file.isFile() && file.getName().equals(dotProject)) {
568 files.add(file);
569 // don't search sub-directories since we can't have nested
570 // projects
571 return true;
574 // no project description found, so recurse into sub-directories
575 for (int i = 0; i < contents.length; i++) {
576 if (contents[i].isDirectory()) {
577 if (!contents[i].getName().equals(METADATA_FOLDER)) {
578 try {
579 String canonicalPath = contents[i].getCanonicalPath();
580 if (!directoriesVisited.add(canonicalPath)) {
581 // already been here --> do not recurse
582 continue;
584 } catch (IOException exception) {
585 StatusManager.getManager().handle(
586 new Status(IStatus.ERROR, Activator
587 .getPluginId(), exception
588 .getLocalizedMessage(), exception));
591 collectProjectFilesFromDirectory(files, contents[i],
592 directoriesVisited, monitor);
596 return true;
600 * Create the selected projects
602 * @return boolean <code>true</code> if all project creations were
603 * successful.
605 boolean createProjects() {
606 final Object[] selected = checkedItems.toArray();
607 WorkspaceModifyOperation op = new WorkspaceModifyOperation() {
608 protected void execute(IProgressMonitor monitor)
609 throws InvocationTargetException, InterruptedException {
610 try {
611 monitor.beginTask("", selected.length); //$NON-NLS-1$
612 if (monitor.isCanceled()) {
613 throw new OperationCanceledException();
615 for (int i = 0; i < selected.length; i++) {
616 createExistingProject((ProjectRecord) selected[i],
617 new SubProgressMonitor(monitor, 1));
619 } finally {
620 monitor.done();
624 // run the new project creation operation
625 try {
626 getContainer().run(true, true, op);
627 } catch (InterruptedException e) {
628 return false;
629 } catch (InvocationTargetException e) {
630 // one of the steps resulted in a core exception
631 Throwable t = e.getTargetException();
632 Activator.handleError(UIText.WizardProjectImportPage_errorMessage,
633 t, true);
634 return false;
636 return true;
640 * Create the project described in record. If it is successful return true.
642 * @param record
643 * @param monitor
644 * @return boolean <code>true</code> if successful
645 * @throws InvocationTargetException
646 * @throws InterruptedException
648 private boolean createExistingProject(final ProjectRecord record,
649 IProgressMonitor monitor) throws InvocationTargetException,
650 InterruptedException {
651 String projectName = record.getProjectName();
652 final IWorkspace workspace = ResourcesPlugin.getWorkspace();
653 final IProject project = workspace.getRoot().getProject(projectName);
654 if (record.description == null) {
655 // error case
656 record.description = workspace.newProjectDescription(projectName);
657 IPath locationPath = new Path(record.projectSystemFile
658 .getAbsolutePath());
660 // If it is under the root use the default location
661 if (Platform.getLocation().isPrefixOf(locationPath)) {
662 record.description.setLocation(null);
663 } else {
664 record.description.setLocation(locationPath);
666 } else {
667 record.description.setName(projectName);
670 try {
671 monitor.beginTask(
672 UIText.WizardProjectsImportPage_CreateProjectsTask, 100);
673 project.create(record.description, new SubProgressMonitor(monitor,
674 30));
675 project.open(IResource.BACKGROUND_REFRESH, new SubProgressMonitor(
676 monitor, 50));
677 } catch (CoreException e) {
678 throw new InvocationTargetException(e);
679 } finally {
680 monitor.done();
683 return true;
687 * Method used for test suite.
689 * @return CheckboxTreeViewer the viewer containing all the projects found
691 public TreeViewer getProjectsList() {
692 return projectsList;
696 * Retrieve all the projects in the current workspace.
698 * @return IProject[] array of IProject in the current workspace
700 private IProject[] getProjectsInWorkspace() {
701 if (wsProjects == null) {
702 wsProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
704 return wsProjects;
708 * Get the array of valid project records that can be imported from the
709 * source workspace or archive, selected by the user. If a project with the
710 * same name exists in both the source workspace and the current workspace,
711 * it will not appear in the list of projects to import and thus cannot be
712 * selected for import.
714 * Method declared public for test suite.
716 * @return ProjectRecord[] array of projects that can be imported into the
717 * workspace
719 public ProjectRecord[] getValidProjects() {
720 List<ProjectRecord> validProjects = new ArrayList<ProjectRecord>();
721 for (int i = 0; i < selectedProjects.length; i++) {
722 if (!isProjectInWorkspace(selectedProjects[i].getProjectName())) {
723 validProjects.add(selectedProjects[i]);
726 return validProjects.toArray(new ProjectRecord[validProjects.size()]);
730 * Determine if the project with the given name is in the current workspace.
732 * @param projectName
733 * String the project name to check
734 * @return boolean true if the project with the given name is in this
735 * workspace
737 private boolean isProjectInWorkspace(String projectName) {
738 if (projectName == null) {
739 return false;
741 IProject[] workspaceProjects = getProjectsInWorkspace();
742 for (int i = 0; i < workspaceProjects.length; i++) {
743 if (projectName.equals(workspaceProjects[i].getName())) {
744 return true;
747 return false;