Fix bug in Import Git repository wizard
[egit.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / clone / GitProjectsImportPage.java
blobe6134a41d361ff6a43d25d6329d17a82fa45b533
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.core.op.ConnectProviderOperation;
41 import org.eclipse.egit.ui.Activator;
42 import org.eclipse.egit.ui.UIText;
43 import org.eclipse.jface.dialogs.Dialog;
44 import org.eclipse.jface.dialogs.ErrorDialog;
45 import org.eclipse.jface.operation.IRunnableWithProgress;
46 import org.eclipse.jface.viewers.ITreeContentProvider;
47 import org.eclipse.jface.viewers.LabelProvider;
48 import org.eclipse.jface.viewers.TreeViewer;
49 import org.eclipse.jface.viewers.Viewer;
50 import org.eclipse.jface.viewers.ViewerComparator;
51 import org.eclipse.jface.wizard.WizardPage;
52 import org.eclipse.osgi.util.NLS;
53 import org.eclipse.swt.SWT;
54 import org.eclipse.swt.events.MouseAdapter;
55 import org.eclipse.swt.events.MouseEvent;
56 import org.eclipse.swt.events.SelectionAdapter;
57 import org.eclipse.swt.events.SelectionEvent;
58 import org.eclipse.swt.graphics.Point;
59 import org.eclipse.swt.layout.GridData;
60 import org.eclipse.swt.layout.GridLayout;
61 import org.eclipse.swt.widgets.Button;
62 import org.eclipse.swt.widgets.Composite;
63 import org.eclipse.swt.widgets.Display;
64 import org.eclipse.swt.widgets.Label;
65 import org.eclipse.swt.widgets.Tree;
66 import org.eclipse.swt.widgets.TreeItem;
67 import org.eclipse.ui.actions.WorkspaceModifyOperation;
68 import org.eclipse.ui.dialogs.FilteredTree;
69 import org.eclipse.ui.dialogs.PatternFilter;
70 import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
71 import org.eclipse.ui.internal.ide.StatusUtil;
72 import org.eclipse.ui.internal.wizards.datatransfer.WizardProjectsImportPage;
73 import org.eclipse.ui.statushandlers.StatusManager;
74 import org.eclipse.ui.wizards.datatransfer.IImportStructureProvider;
76 /**
77 * The GitWizardProjectsImportPage is the page that allows the user to import
78 * projects from a particular location. This is a modified copy of
79 * {@link WizardProjectsImportPage}
81 public class GitProjectsImportPage extends WizardPage {
83 /**
84 * The name of the folder containing metadata information for the workspace.
86 public static final String METADATA_FOLDER = ".metadata"; //$NON-NLS-1$
88 private IImportStructureProvider structureProvider;
90 private File gitRepositoryDir;
92 class ProjectRecord {
93 File projectSystemFile;
95 String projectName;
97 Object parent;
99 int level;
101 IProjectDescription description;
104 * Create a record for a project based on the info in the file.
106 * @param file
108 ProjectRecord(File file) {
109 projectSystemFile = file;
110 setProjectName();
114 * @param parent
115 * The parent folder of the .project file
116 * @param level
117 * The number of levels deep in the provider the file is
119 ProjectRecord(Object parent, int level) {
120 this.parent = parent;
121 this.level = level;
122 setProjectName();
126 * Set the name of the project based on the projectFile.
128 private void setProjectName() {
129 try {
130 // If we don't have the project name try again
131 if (projectName == null) {
132 IPath path = new Path(projectSystemFile.getPath());
133 // if the file is in the default location, use the directory
134 // name as the project name
135 if (isDefaultLocation(path)) {
136 projectName = path.segment(path.segmentCount() - 2);
137 description = ResourcesPlugin.getWorkspace()
138 .newProjectDescription(projectName);
139 } else {
140 description = ResourcesPlugin.getWorkspace()
141 .loadProjectDescription(path);
142 projectName = description.getName();
146 } catch (CoreException e) {
147 // no good couldn't get the name
152 * Returns whether the given project description file path is in the
153 * default location for a project
155 * @param path
156 * The path to examine
157 * @return Whether the given path is the default location for a project
159 private boolean isDefaultLocation(IPath path) {
160 // The project description file must at least be within the project,
161 // which is within the workspace location
162 if (path.segmentCount() < 2)
163 return false;
164 return path.removeLastSegments(2).toFile().equals(
165 Platform.getLocation().toFile());
169 * Get the name of the project
171 * @return String
173 public String getProjectName() {
174 return projectName;
178 * Gets the label to be used when rendering this project record in the
179 * UI.
181 * @return String the label
182 * @since 3.4
184 public String getProjectLabel() {
185 if (description == null)
186 return projectName;
188 String path = projectSystemFile == null ? structureProvider
189 .getLabel(parent) : projectSystemFile.getParent();
191 return NLS.bind(UIText.WizardProjectsImportPage_projectLabel,
192 projectName, path);
196 private TreeViewer projectsList;
198 private ProjectRecord[] selectedProjects = new ProjectRecord[0];
200 final private HashSet<Object> checkedItems = new HashSet<Object>();
202 private IProject[] wsProjects;
204 // The last selected path to minimize searches
205 private String lastPath;
207 // The last time that the file or folder at the selected path was modified
208 // to minimize searches
209 private long lastModified;
211 private Button shareCheckBox;
213 private Button selectAll;
215 private Button deselectAll;
217 private boolean share;
220 * Creates a new project creation wizard page.
222 public GitProjectsImportPage() {
223 this("gitWizardExternalProjectsPage"); //$NON-NLS-1$
227 * Create a new instance of the receiver.
229 * @param pageName
231 public GitProjectsImportPage(String pageName) {
232 super(pageName);
233 setPageComplete(false);
234 setTitle(UIText.WizardProjectsImportPage_ImportProjectsTitle);
235 setDescription(UIText.WizardProjectsImportPage_ImportProjectsDescription);
238 public void createControl(Composite parent) {
240 initializeDialogUnits(parent);
242 Composite workArea = new Composite(parent, SWT.NONE);
243 setControl(workArea);
245 workArea.setLayout(new GridLayout());
246 workArea.setLayoutData(new GridData(GridData.FILL_BOTH
247 | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL));
249 createProjectsRoot(workArea);
250 createProjectsList(workArea);
251 createOptionsArea(workArea);
252 Dialog.applyDialogFont(workArea);
257 * Create the area with the extra options.
259 * @param workArea
261 private void createOptionsArea(Composite workArea) {
262 Composite optionsGroup = new Composite(workArea, SWT.NONE);
263 optionsGroup.setLayout(new GridLayout());
264 optionsGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
266 shareCheckBox = new Button(optionsGroup, SWT.CHECK);
267 shareCheckBox.setText(UIText.WizardProjectsImportPage_enableGit);
268 shareCheckBox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
269 shareCheckBox.setSelection(share = true);
270 shareCheckBox.addSelectionListener(new SelectionAdapter() {
271 public void widgetSelected(SelectionEvent e) {
272 share = shareCheckBox.getSelection();
278 * Create the checkbox list for the found projects.
280 * @param workArea
282 private void createProjectsList(Composite workArea) {
284 checkedItems.clear();
286 Label title = new Label(workArea, SWT.NONE);
287 title.setText(UIText.WizardProjectsImportPage_ProjectsListTitle);
289 Composite listComposite = new Composite(workArea, SWT.NONE);
290 GridLayout layout = new GridLayout();
291 layout.numColumns = 2;
292 layout.marginWidth = 0;
293 layout.makeColumnsEqualWidth = false;
294 listComposite.setLayout(layout);
296 listComposite.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL
297 | GridData.GRAB_VERTICAL | GridData.FILL_BOTH));
299 PatternFilter filter = new PatternFilter() {
301 @Override
302 public boolean isElementVisible(Viewer viewer, Object element) {
304 if (checkedItems.contains(element)) {
305 return true;
308 return super.isElementVisible(viewer, element);
311 @Override
312 public void setPattern(String patternString) {
313 super.setPattern(patternString);
314 // TODO: is there a better way to react on changes in the tree?
315 // disable select all button when tree becomes empty due to
316 // filtering
317 Display.getDefault().asyncExec(new Runnable() {
318 public void run() {
319 enableSelectAllButtons();
327 FilteredTree filteredTree = new FilteredTree(listComposite, SWT.CHECK
328 | SWT.BORDER, filter);
329 filteredTree.setInitialText(UIText.WizardProjectsImportPage_filterText);
330 projectsList = filteredTree.getViewer();
331 GridData listData = new GridData(GridData.GRAB_HORIZONTAL
332 | GridData.GRAB_VERTICAL | GridData.FILL_BOTH);
333 projectsList.getControl().setLayoutData(listData);
335 projectsList.setContentProvider(new ITreeContentProvider() {
337 public Object[] getChildren(Object parentElement) {
338 return null;
341 public Object[] getElements(Object inputElement) {
342 return getValidProjects();
345 public boolean hasChildren(Object element) {
346 return false;
349 public Object getParent(Object element) {
350 return null;
353 public void dispose() {
357 public void inputChanged(Viewer viewer, Object oldInput,
358 Object newInput) {
363 projectsList.getTree().addMouseListener(new MouseAdapter() {
364 @Override
365 public void mouseUp(MouseEvent e) {
366 if (e.widget instanceof Tree) {
367 TreeItem item = ((Tree) e.widget).getItem(new Point(e.x,
368 e.y));
369 if (item != null) {
370 if (item.getChecked())
371 checkedItems.add(item.getData());
372 else
373 checkedItems.remove(item.getData());
374 setPageComplete(!checkedItems.isEmpty());
380 projectsList.setLabelProvider(new LabelProvider() {
381 public String getText(Object element) {
382 // Need to set the checked item state. FIXME This is clumsy.
383 for (final TreeItem item : projectsList.getTree().getItems()) {
384 if (checkedItems.contains(item.getData()))
385 item.setChecked(true);
386 else
387 item.setChecked(false);
389 return ((ProjectRecord) element).getProjectLabel();
393 projectsList.setInput(this);
394 projectsList.setComparator(new ViewerComparator());
395 createSelectionButtons(listComposite);
399 * Create the selection buttons in the listComposite.
401 * @param listComposite
403 private void createSelectionButtons(Composite listComposite) {
404 Composite buttonsComposite = new Composite(listComposite, SWT.NONE);
405 GridLayout layout = new GridLayout();
406 layout.marginWidth = 0;
407 layout.marginHeight = 0;
408 buttonsComposite.setLayout(layout);
410 buttonsComposite.setLayoutData(new GridData(
411 GridData.VERTICAL_ALIGN_BEGINNING));
413 selectAll = new Button(buttonsComposite, SWT.PUSH);
414 selectAll.setText(UIText.WizardProjectsImportPage_selectAll);
415 selectAll.addSelectionListener(new SelectionAdapter() {
416 public void widgetSelected(SelectionEvent e) {
417 checkedItems.clear();
418 // only the root has children
419 for (final TreeItem item : projectsList.getTree().getItems()) {
420 item.setChecked(true);
421 checkedItems.add(item.getData());
423 setPageComplete(true);
426 Dialog.applyDialogFont(selectAll);
427 setButtonLayoutData(selectAll);
429 deselectAll = new Button(buttonsComposite, SWT.PUSH);
430 deselectAll.setText(UIText.WizardProjectsImportPage_deselectAll);
431 deselectAll.addSelectionListener(new SelectionAdapter() {
432 public void widgetSelected(SelectionEvent e) {
433 checkedItems.clear();
434 // only the root has children
435 for (final TreeItem item : projectsList.getTree().getItems()) {
436 item.setChecked(false);
438 projectsList.setInput(this); // filter away selected projects
439 setPageComplete(false);
442 Dialog.applyDialogFont(deselectAll);
443 setButtonLayoutData(deselectAll);
448 * Create the area where you select the root directory for the projects.
450 * @param workArea
451 * Composite
453 private void createProjectsRoot(Composite workArea) {
455 // project specification group
456 Composite projectGroup = new Composite(workArea, SWT.NONE);
457 GridLayout layout = new GridLayout();
458 layout.numColumns = 3;
459 layout.makeColumnsEqualWidth = false;
460 layout.marginWidth = 0;
461 projectGroup.setLayout(layout);
462 projectGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
466 public void setVisible(boolean visible) {
467 super.setVisible(visible);
471 * Set the git directory which will contain the repository
473 * @param gitDir
475 public void setGitDir(File gitDir) {
476 this.gitRepositoryDir = gitDir;
480 * Update the list of projects based on path. This will not check any
481 * projects.
483 * @param path
485 void setProjectsList(final String path) {
486 // on an empty path empty selectedProjects
487 if (path == null || path.length() == 0) {
488 setMessage(UIText.WizardProjectsImportPage_ImportProjectsDescription);
489 selectedProjects = new ProjectRecord[0];
490 projectsList.refresh(true);
491 setPageComplete(checkedItems.size() > 0);
492 lastPath = path;
493 return;
496 final File directory = new File(path);
497 long modified = directory.lastModified();
498 if (path.equals(lastPath) && lastModified == modified) {
499 // since the file/folder was not modified and the path did not
500 // change, no refreshing is required
501 return;
504 lastPath = path;
505 lastModified = modified;
507 try {
508 getContainer().run(true, true, new IRunnableWithProgress() {
510 public void run(IProgressMonitor monitor) {
512 monitor.beginTask(
513 UIText.WizardProjectsImportPage_SearchingMessage,
514 100);
515 selectedProjects = new ProjectRecord[0];
516 Collection<File> files = new ArrayList<File>();
517 monitor.worked(10);
518 if (directory.isDirectory()) {
520 if (!collectProjectFilesFromDirectory(files, directory,
521 null, monitor)) {
522 return;
524 Iterator<File> filesIterator = files.iterator();
525 selectedProjects = new ProjectRecord[files.size()];
526 int index = 0;
527 monitor.worked(50);
528 monitor
529 .subTask(UIText.WizardProjectsImportPage_ProcessingMessage);
530 while (filesIterator.hasNext()) {
531 File file = filesIterator.next();
532 selectedProjects[index] = new ProjectRecord(file);
533 checkedItems.add(selectedProjects[index]);
534 index++;
536 } else {
537 monitor.worked(60);
539 monitor.done();
543 } catch (InvocationTargetException e) {
544 IDEWorkbenchPlugin.log(e.getMessage(), e);
545 } catch (InterruptedException e) {
546 // Nothing to do if the user interrupts.
549 projectsList.refresh(true);
550 if (getValidProjects().length < selectedProjects.length) {
551 setMessage(UIText.WizardProjectsImportPage_projectsInWorkspace,
552 WARNING);
553 } else {
554 setMessage(UIText.WizardProjectsImportPage_ImportProjectsDescription);
556 enableSelectAllButtons();
557 setPageComplete(checkedItems.size() > 0);
560 private void enableSelectAllButtons() {
561 if (projectsList.getTree().getItemCount()>0){
562 selectAll.setEnabled(true);
563 deselectAll.setEnabled(true);
564 } else {
565 selectAll.setEnabled(false);
566 deselectAll.setEnabled(false);
571 * Collect the list of .project files that are under directory into files.
573 * @param files
574 * @param directory
575 * @param directoriesVisited
576 * Set of canonical paths of directories, used as recursion guard
577 * @param monitor
578 * The monitor to report to
579 * @return boolean <code>true</code> if the operation was completed.
581 private boolean collectProjectFilesFromDirectory(Collection<File> files,
582 File directory, Set<String> directoriesVisited,
583 IProgressMonitor monitor) {
585 if (monitor.isCanceled()) {
586 return false;
588 monitor.subTask(NLS.bind(
589 UIText.WizardProjectsImportPage_CheckingMessage, directory
590 .getPath()));
591 File[] contents = directory.listFiles();
592 if (contents == null)
593 return false;
595 // Initialize recursion guard for recursive symbolic links
596 if (directoriesVisited == null) {
597 directoriesVisited = new HashSet<String>();
598 try {
599 directoriesVisited.add(directory.getCanonicalPath());
600 } catch (IOException exception) {
601 StatusManager.getManager().handle(
602 StatusUtil.newStatus(IStatus.ERROR, exception
603 .getLocalizedMessage(), exception));
607 // first look for project description files
608 final String dotProject = IProjectDescription.DESCRIPTION_FILE_NAME;
609 for (int i = 0; i < contents.length; i++) {
610 File file = contents[i];
611 if (file.isFile() && file.getName().equals(dotProject)) {
612 files.add(file);
613 // don't search sub-directories since we can't have nested
614 // projects
615 return true;
618 // no project description found, so recurse into sub-directories
619 for (int i = 0; i < contents.length; i++) {
620 if (contents[i].isDirectory()) {
621 if (!contents[i].getName().equals(METADATA_FOLDER)) {
622 try {
623 String canonicalPath = contents[i].getCanonicalPath();
624 if (!directoriesVisited.add(canonicalPath)) {
625 // already been here --> do not recurse
626 continue;
628 } catch (IOException exception) {
629 StatusManager.getManager().handle(
630 StatusUtil.newStatus(IStatus.ERROR, exception
631 .getLocalizedMessage(), exception));
634 collectProjectFilesFromDirectory(files, contents[i],
635 directoriesVisited, monitor);
639 return true;
643 * Create the selected projects
645 * @return boolean <code>true</code> if all project creations were
646 * successful.
648 boolean createProjects() {
649 final Object[] selected = checkedItems.toArray();
650 WorkspaceModifyOperation op = new WorkspaceModifyOperation() {
651 protected void execute(IProgressMonitor monitor)
652 throws InvocationTargetException, InterruptedException {
653 try {
654 monitor.beginTask("", selected.length); //$NON-NLS-1$
655 if (monitor.isCanceled()) {
656 throw new OperationCanceledException();
658 for (int i = 0; i < selected.length; i++) {
659 createExistingProject((ProjectRecord) selected[i],
660 new SubProgressMonitor(monitor, 1));
662 } finally {
663 monitor.done();
667 // run the new project creation operation
668 try {
669 getContainer().run(true, true, op);
670 } catch (InterruptedException e) {
671 return false;
672 } catch (InvocationTargetException e) {
673 // one of the steps resulted in a core exception
674 Throwable t = e.getTargetException();
675 String message = UIText.WizardProjectImportPage_errorMessage;
676 IStatus status;
677 if (t instanceof CoreException) {
678 status = ((CoreException) t).getStatus();
679 } else {
680 status = new Status(IStatus.ERROR,
681 IDEWorkbenchPlugin.IDE_WORKBENCH, 1, message, t);
683 Activator.logError(message, t);
684 ErrorDialog.openError(getShell(), message, null, status);
685 return false;
687 return true;
691 * Create the project described in record. If it is successful return true.
693 * @param record
694 * @param monitor
695 * @return boolean <code>true</code> if successful
696 * @throws InvocationTargetException
697 * @throws InterruptedException
699 private boolean createExistingProject(final ProjectRecord record,
700 IProgressMonitor monitor) throws InvocationTargetException,
701 InterruptedException {
702 String projectName = record.getProjectName();
703 final IWorkspace workspace = ResourcesPlugin.getWorkspace();
704 final IProject project = workspace.getRoot().getProject(projectName);
705 if (record.description == null) {
706 // error case
707 record.description = workspace.newProjectDescription(projectName);
708 IPath locationPath = new Path(record.projectSystemFile
709 .getAbsolutePath());
711 // If it is under the root use the default location
712 if (Platform.getLocation().isPrefixOf(locationPath)) {
713 record.description.setLocation(null);
714 } else {
715 record.description.setLocation(locationPath);
717 } else {
718 record.description.setName(projectName);
721 try {
722 monitor.beginTask(
723 UIText.WizardProjectsImportPage_CreateProjectsTask, 100);
724 project.create(record.description, new SubProgressMonitor(monitor,
725 30));
726 int openTicks = share ? 50 : 70;
727 project.open(IResource.BACKGROUND_REFRESH, new SubProgressMonitor(
728 monitor, openTicks));
729 if (share) {
730 ConnectProviderOperation connectProviderOperation = new ConnectProviderOperation(
731 project, gitRepositoryDir);
732 connectProviderOperation
733 .run(new SubProgressMonitor(monitor, 20));
735 } catch (CoreException e) {
736 throw new InvocationTargetException(e);
737 } finally {
738 monitor.done();
741 return true;
745 * Method used for test suite.
747 * @return CheckboxTreeViewer the viewer containing all the projects found
749 public TreeViewer getProjectsList() {
750 return projectsList;
754 * Retrieve all the projects in the current workspace.
756 * @return IProject[] array of IProject in the current workspace
758 private IProject[] getProjectsInWorkspace() {
759 if (wsProjects == null) {
760 wsProjects = IDEWorkbenchPlugin.getPluginWorkspace().getRoot()
761 .getProjects();
763 return wsProjects;
767 * Get the array of valid project records that can be imported from the
768 * source workspace or archive, selected by the user. If a project with the
769 * same name exists in both the source workspace and the current workspace,
770 * it will not appear in the list of projects to import and thus cannot be
771 * selected for import.
773 * Method declared public for test suite.
775 * @return ProjectRecord[] array of projects that can be imported into the
776 * workspace
778 public ProjectRecord[] getValidProjects() {
779 List<ProjectRecord> validProjects = new ArrayList<ProjectRecord>();
780 for (int i = 0; i < selectedProjects.length; i++) {
781 if (!isProjectInWorkspace(selectedProjects[i].getProjectName())) {
782 validProjects.add(selectedProjects[i]);
785 return validProjects.toArray(new ProjectRecord[validProjects.size()]);
789 * Determine if the project with the given name is in the current workspace.
791 * @param projectName
792 * String the project name to check
793 * @return boolean true if the project with the given name is in this
794 * workspace
796 private boolean isProjectInWorkspace(String projectName) {
797 if (projectName == null) {
798 return false;
800 IProject[] workspaceProjects = getProjectsInWorkspace();
801 for (int i = 0; i < workspaceProjects.length; i++) {
802 if (projectName.equals(workspaceProjects[i].getName())) {
803 return true;
806 return false;