GP-2496 edit shared project info improvements

This commit is contained in:
ghidra1 2022-09-08 17:57:09 -04:00
parent 09d326ddbb
commit 52d1097c5b
17 changed files with 416 additions and 107 deletions

View file

@ -335,6 +335,7 @@ src/main/help/help/topics/FrontEndPlugin/images/ConnectTools.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/DeleteProject.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/EditPluginPath.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/EditProjectAccessList.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/EditProjectAccessPanel.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/MemoryUsage.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/NonSharedProjectInfo.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/OpenProject.png||GHIDRA||||END|
@ -361,6 +362,7 @@ src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOnoServer.png||GHI
src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOwithServer.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/VersionedFileIcon.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/ViewOtherProjects.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/ViewProjectAccessPanel.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/closedBookBlue.png||GHIDRA||reviewed||END|
src/main/help/help/topics/FrontEndPlugin/images/connected.gif||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/disconnected.gif||GHIDRA||||END|

View file

@ -119,11 +119,20 @@
<H2><A name="Change_Shared_Project_Info"></A>Changing Shared Project Information</H2>
<P>Changing shared project details may become neccessary when a server's IP address or name
has changed. While other cases are supported, these may cause some issues with private and
checked-out project files. Any checked-out file which does not match-up properly will be
renamed to a private <I>.keep</I> file within the project and a checkin will no longer be
possible. In addition, when switching to and a different repository private files may conflict
with those in the repository resulting in
<A href="Ghidra_Front_end.htm#HijackedFile">hijacked files</A>.
<BLOCKQUOTE>
<P>To update repository information:</P>
<OL>
<LI>Close all open files and make sure that all files are checked in.</LI>
<LI>Close all open files. Closing all of your active tools (e.g., CodeBrowser) may be
the simplest way to accomplish this.</LI>
<LI>In <I>Project Information</I>, click on the <B>Change Shared Project Info...</B> button
to start the&nbsp;<I>Change Shared Project Information</I> wizard.&nbsp;</LI>
@ -201,23 +210,27 @@
and modify user privileges by choosing the <B>Project</B><IMG border="0" src=
"../../shared/arrow.gif"><B><A href=
"Ghidra_Front_end.htm#Edit_Project_Access_List">Edit Project Access List...</A></B> <A
href="Ghidra_Front_end.htm">option</A>.&nbsp;</P>
href="Ghidra_Front_end.htm"></A> option.&nbsp;</P>
</BLOCKQUOTE>
<OL start="8">
<LI>Select the <B>Finish</B> button.&nbsp; &nbsp;</LI>
<LI><A name="Step9"></A>A confirmation dialog is displayed; select the <B>Update</B> button
to complete the <I>Change Shared Project Information</I> process.</LI>
to start the <I>Change Shared Project Information</I> process.</LI>
</OL>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG border="0" src="../../shared/note.png"> After you have updated
your project information, you may end up with <A href=
"Ghidra_Front_end.htm#HijackedFile">hijacked files</A> if a file of the same name exists in
the repository.&nbsp;&nbsp;</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG border="0" src="../../shared/warning.png">
If one or more checked-out files do not match-up properly with the new repository you will
be prompted to allow these checkouts to be terminated and converted to private <I>.keep</I>
files. Such file conversion will prevent such files from ever being checked-in and
should be avoided when possible. Click <B>Terminate Checkouts and Continue</B> to proceed
with change or <B>Cancel</B> to abort change. The conversion of these files to private .keep
files can not be undone.
</P>
</BLOCKQUOTE>
<H2>&nbsp;</H2>
@ -225,8 +238,12 @@
<BLOCKQUOTE>
<P>The image below shows project information for a project that is not shared. Note that the
repository information is disabled. Before you can convert your project, you must first close
any files that you have opened, and check in any files that you have checked out.</P>
repository information will not be displayed for a private project. If repository information
is displayed this is already a shared project.</P>
<P>Before you can convert your project, you must first close
any files that you have opened. Closing all of your active tools (e.g., CodeBrowser) may be
the simplest way to accomplish this.</P>
<P><IMG border="0" src="../../shared/warning.png"> You will lose all
version history for files under local <A href=
@ -241,7 +258,7 @@
<BLOCKQUOTE>
<P>The steps to converting your project are the same as those described for <A href=
"#Change_Shared_Project_Info">changing your repository information</A>. However, at <A href=
"#Change_Shared_Project_Info">Change Shared Project Information</A>. However, at <A href=
"#Step9">Step 9</A> above you will get a dialog that warns you about losing version history
on versioned files. From the warning dialog, click on the <B>Convert</B> button to complete
the conversion process.&nbsp;</P>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -256,6 +256,7 @@ public interface RepositoryHandle {
* @param parentPath parent folder path
* @param itemName name of item
* @return checkout data list
* @throws FileNotFoundException if folder item not found
* @throws IOException if an IO error occurs
*/
ItemCheckoutStatus[] getCheckouts(String parentPath, String itemName) throws IOException;

View file

@ -379,6 +379,11 @@ public class DomainFileProxy implements DomainFile {
throw new UnsupportedOperationException("undoCheckout() unsupported for DomainFileProxy");
}
@Override
public void undoCheckout(boolean keep, boolean force) throws IOException {
throw new UnsupportedOperationException("undoCheckout() unsupported for DomainFileProxy");
}
@Override
public ChangeSet getChangesByOthersSinceCheckout() throws IOException {
return null;

View file

@ -28,7 +28,6 @@ import ghidra.util.InvalidNameException;
import ghidra.util.ReadOnlyException;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
import ghidra.util.task.TaskMonitorAdapter;
public class GhidraFile implements DomainFile {
@ -90,6 +89,7 @@ public class GhidraFile implements DomainFile {
/**
* Reassign a new file-ID to resolve file-ID conflict.
* Conflicts can occur as a result of a cancelled check-out.
* @throws IOException if an IO error occurs
*/
void resetFileID() throws IOException {
getFileData().resetFileID();
@ -176,21 +176,21 @@ public class GhidraFile implements DomainFile {
public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
TaskMonitor monitor) throws VersionException, IOException, CancelledException {
return getFileData().getDomainObject(consumer, okToUpgrade, okToRecover,
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor)
throws VersionException, IOException, CancelledException {
return getFileData().getReadOnlyDomainObject(consumer, version,
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor)
throws VersionException, IOException, CancelledException {
return getFileData().getImmutableDomainObject(consumer, version,
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
@ -205,7 +205,7 @@ public class GhidraFile implements DomainFile {
if (isReadOnly()) {
throw new ReadOnlyException("Cannot save to read-only file");
}
dobj.save(null, monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
dobj.save(null, monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
@ -428,21 +428,21 @@ public class GhidraFile implements DomainFile {
public boolean checkout(boolean exclusive, TaskMonitor monitor) throws IOException,
CancelledException {
return getFileData().checkout(exclusive,
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
public void checkin(CheckinHandler checkinHandler, boolean okToUpgrade, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
getFileData().checkin(checkinHandler, okToUpgrade,
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
public void merge(boolean okToUpgrade, TaskMonitor monitor) throws IOException,
VersionException, CancelledException {
getFileData().merge(okToUpgrade,
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
@ -450,6 +450,11 @@ public class GhidraFile implements DomainFile {
getFileData().undoCheckout(keep, false);
}
@Override
public void undoCheckout(boolean keep, boolean force) throws IOException {
getFileData().undoCheckout(keep, force, false);
}
@Override
public void terminateCheckout(long checkoutId) throws IOException {
getFileData().terminateCheckout(checkoutId);
@ -486,7 +491,7 @@ public class GhidraFile implements DomainFile {
CancelledException {
GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation
return getFileData().copyTo(newGhidraParent.getFolderData(),
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
@ -494,17 +499,19 @@ public class GhidraFile implements DomainFile {
throws IOException, CancelledException {
GhidraFolder destGhidraFolder = (GhidraFolder) destFolder; // assumes single implementation
return getFileData().copyVersionTo(version, destGhidraFolder.getFolderData(),
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
monitor != null ? monitor : TaskMonitor.DUMMY);
}
/**
* Copy this file to make a private file if it is versioned. This method should be called
* only when a non shared project is being converted to a shared project.
* @throws IOException
* @param monitor task monitor
* @throws IOException if an IO error occurs
* @throws CancelledException if task cancelled
*/
void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException {
getFileData().convertToPrivateFile(
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
@ -542,7 +549,7 @@ public class GhidraFile implements DomainFile {
@Override
public void packFile(File file, TaskMonitor monitor) throws IOException, CancelledException {
getFileData().packFile(file, monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
getFileData().packFile(file, monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override

View file

@ -1217,6 +1217,10 @@ public class GhidraFileData {
}
void undoCheckout(boolean keep, boolean inUseOK) throws IOException {
undoCheckout(keep, false, inUseOK);
}
void undoCheckout(boolean keep, boolean force, boolean inUseOK) throws IOException {
synchronized (fileSystem) {
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException("undoCheckout permitted within writeable project only");
@ -1224,16 +1228,23 @@ public class GhidraFileData {
if (!inUseOK) {
checkInUse();
}
if (!versionedFileSystem.isOnline()) {
throw new NotConnectedException("Not connected to repository server");
boolean doForce = false;
boolean isOnline = versionedFileSystem.isOnline();
if (!isOnline) {
if (!force) {
throw new NotConnectedException("Not connected to repository server");
}
doForce = true;
}
if (!isCheckedOut()) {
throw new IOException("File not checked out");
}
verifyRepoUser("undo-checkout");
long checkoutId = folderItem.getCheckoutId();
if (!doForce) {
verifyRepoUser("undo-checkout");
long checkoutId = folderItem.getCheckoutId();
versionedFolderItem.terminateCheckout(checkoutId, true);
}
String keepName = getKeepName();
versionedFolderItem.terminateCheckout(checkoutId, true);
if (keep) {
folderItem.clearCheckout();
try {

View file

@ -27,6 +27,7 @@ import ghidra.framework.remote.User;
import ghidra.framework.store.*;
import ghidra.framework.store.FileSystem;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.framework.store.local.LocalFolderItem;
import ghidra.framework.store.remote.RemoteFileSystem;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
@ -384,7 +385,7 @@ public class ProjectFileManager implements ProjectData {
}
@Override
public DomainFolder getFolder(String path) {
public GhidraFolder getFolder(String path) {
int len = path.length();
if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
throw new IllegalArgumentException(
@ -565,33 +566,158 @@ public class ProjectFileManager implements ProjectData {
}
@Override
public void updateRepositoryInfo(RepositoryAdapter newRepository, TaskMonitor monitor)
public void updateRepositoryInfo(RepositoryAdapter newRepository, boolean force,
TaskMonitor monitor)
throws IOException, CancelledException {
// 1) check for checked out files
findCheckedOutFiles(getRootFolder(), monitor);
newRepository.connect();
if (!newRepository.isConnected()) {
throw new IOException("new respository not connected");
}
// 2) Update the properties with server info
// Terminate any local checkouts which are not valid with newRepository
List<DomainFile> checkoutFiles = findCheckedOutFiles(monitor);
List<DomainFile> invalidCheckoutFiles =
findInvalidCheckouts(checkoutFiles, newRepository, monitor);
undoCheckouts(invalidCheckoutFiles, true, force, monitor);
// Update the properties with server info
updatePropertiesFile(newRepository);
}
private void findCheckedOutFiles(DomainFolder folder, TaskMonitor monitor)
throws IOException, CancelledException {
DomainFile[] files = folder.getFiles();
for (DomainFile file : files) {
if (monitor.isCancelled()) {
throw new CancelledException();
private boolean hasInvalidCheckout(DomainFile df, RepositoryAdapter newRepository)
throws IOException {
try {
LocalFolderItem item = fileSystem.getItem(df.getParent().getPathname(), df.getName());
if (item == null) {
return false;
}
if (file.isCheckedOut()) {
throw new IOException("File " + file.getPathname() + " is checked out.");
// TODO: this is not bulletproof since we have limited data to validate checkout.
long checkoutId = item.getCheckoutId();
int checkoutVersion = item.getCheckoutVersion();
ItemCheckoutStatus otherCheckoutStatus = newRepository.getCheckout(
df.getParent().getPathname(), df.getName(), checkoutId);
if (!newRepository.getUser().getName().equals(otherCheckoutStatus.getUser())) {
return true;
}
if (checkoutVersion != otherCheckoutStatus.getCheckoutVersion()) {
return true;
}
}
DomainFolder[] folders = folder.getFolders();
for (DomainFolder folder2 : folders) {
if (monitor.isCancelled()) {
throw new CancelledException();
catch (FileNotFoundException e) {
return true;
}
catch (NotConnectedException e) {
throw e;
}
catch (IOException e) {
// skip file
}
return false;
}
/**
* Determine if any domain files listed does not correspond to a checkout in the specified
* newRespository.
* @param checkoutList project domain files to check
* @param newRepository repository to check against before updating
* @param monitor task monitor
* @return true if one or more files are not valid checkouts in newRepository
* @throws IOException if IO error occurs
* @throws CancelledException if task cancelled
*/
public boolean hasInvalidCheckouts(List<DomainFile> checkoutList,
RepositoryAdapter newRepository, TaskMonitor monitor)
throws IOException, CancelledException {
for (DomainFile df : checkoutList) {
monitor.checkCanceled();
if (hasInvalidCheckout(df, newRepository)) {
return true;
}
findCheckedOutFiles(folder2, monitor);
}
return false;
}
/**
* Find those domain files listed which do not correspond to checkouts in the specified
* newRespository.
* @param checkoutList project domain files to check
* @param newRepository repository to check against before updating
* @param monitor task monitor
* @return list of domain files not checked-out in repo
* @throws IOException if IO error occurs
* @throws CancelledException if task cancelled
*/
private List<DomainFile> findInvalidCheckouts(List<DomainFile> checkoutList,
RepositoryAdapter newRepository, TaskMonitor monitor)
throws IOException, CancelledException {
List<DomainFile> list = new ArrayList<>();
for (DomainFile df : checkoutList) {
monitor.checkCanceled();
if (hasInvalidCheckout(df, newRepository)) {
list.add(df);
}
}
return list;
}
/**
* Undo checkouts for all domain files listed.
* @param files list of files to undo checkout
* @param keep if a .keep copy of any checked-out file should be retained in the local file.
* @param force if not connected to the repository the local checkout file will be removed.
* Warning: forcing undo checkout will leave a stale checkout in place for the associated
* repository if not connected.
* @param monitor task monitor
* @throws IOException if an IO error occurs
* @throws CancelledException if task cancelled
*/
private void undoCheckouts(List<DomainFile> files, boolean keep, boolean force,
TaskMonitor monitor) throws IOException, CancelledException {
for (DomainFile df : files) {
monitor.checkCanceled();
if (df.isCheckedOut()) {
df.undoCheckout(keep, force);
}
}
}
/**
* Find all project files which are currently checked-out
* @param monitor task monitor (no progress updates)
* @return list of current checkout files
* @throws IOException if IO error occurs
* @throws CancelledException if task cancelled
*/
public List<DomainFile> findCheckedOutFiles(TaskMonitor monitor)
throws IOException, CancelledException {
List<DomainFile> list = new ArrayList<>();
findCheckedOutFiles("/", list, monitor);
return list;
}
private void findCheckedOutFiles(String folderPath, List<DomainFile> checkoutList,
TaskMonitor monitor)
throws IOException, CancelledException {
for (String name : fileSystem.getItemNames(folderPath)) {
monitor.checkCanceled();
LocalFolderItem item = fileSystem.getItem(folderPath, name);
if (item.getCheckoutId() != FolderItem.DEFAULT_CHECKOUT_ID) {
checkoutList.add(new GhidraFile(getFolder(folderPath), name));
}
}
if (!folderPath.endsWith(FileSystem.SEPARATOR)) {
folderPath += FileSystem.SEPARATOR;
}
for (String subfolder : fileSystem.getFolderNames(folderPath)) {
monitor.checkCanceled();
findCheckedOutFiles(folderPath + subfolder, checkoutList, monitor);
}
}

View file

@ -19,6 +19,8 @@ import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
import javax.swing.border.BevelBorder;
@ -33,6 +35,7 @@ import docking.wizard.WizardManager;
import ghidra.app.util.GenericHelpTopics;
import ghidra.framework.client.*;
import ghidra.framework.data.ConvertFileSystem;
import ghidra.framework.data.TransientDataManager;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.remote.User;
@ -337,10 +340,17 @@ public class ProjectInfoDialog extends DialogComponentProvider {
}
private void updateSharedProjectInfo() {
if (filesAreOpen()) {
Msg.showInfo(getClass(), getComponent(), "Cannot Change Project Info with Open Files",
"Before your project info can be updated, you must close\n" +
"files in running tools and make sure you have no files\n" + "checked out.");
int openCount = getOpenFileCount();
if (openCount != 0) {
Msg.showInfo(getClass(), getComponent(),
"Cannot Change Project Info with Open Files",
"Found " + openCount + " open project file(s).\n" +
"Before your project info can be updated, you must\n" +
"close all open project files and tools.");
return;
}
if (!checkToolsClose()) {
return;
}
@ -356,8 +366,10 @@ public class ProjectInfoDialog extends DialogComponentProvider {
currentRepository.getName().equals(rep.getName())) {
Msg.showInfo(getClass(), getComponent(), "No Changes Made",
"No changes were made to the shared project information.");
return;
}
else if (OptionDialog.showOptionDialog(getComponent(), "Update Shared Project Info",
if (OptionDialog.showOptionDialog(getComponent(), "Update Shared Project Info",
"Are you sure you want to update your shared project information?", "Update",
OptionDialog.QUESTION_MESSAGE) == OptionDialog.OPTION_ONE) {
@ -376,12 +388,29 @@ public class ProjectInfoDialog extends DialogComponentProvider {
}
private boolean checkToolsClose() {
PluginTool[] runningTools = project.getToolManager().getRunningTools();
for (PluginTool runningTool : runningTools) {
if (!runningTool.canClose(false)) {
return false;
}
runningTool.close();
}
return true;
}
private void convertToIndexedFilesystem() {
if (filesAreOpen()) {
int openCount = getOpenFileCount();
if (openCount != 0) {
Msg.showInfo(getClass(), getComponent(),
"Cannot Convert/Upgrade Project Storage with Open Files",
"Found " + openCount + " open project file(s).\n" +
"Before your project can be converted, you must close\n" +
"files in running tools.");
"all open project files and tools.");
return;
}
if (!checkToolsClose()) {
return;
}
@ -415,10 +444,18 @@ public class ProjectInfoDialog extends DialogComponentProvider {
}
private void convertToShared() {
if (filesAreOpen()) {
Msg.showInfo(getClass(), getComponent(), "Cannot Convert Project with Open Files",
int openCount = getOpenFileCount();
if (openCount != 0) {
Msg.showInfo(getClass(), getComponent(),
"Cannot Convert Project with Open Files",
"Found " + openCount + " open project file(s).\n" +
"Before your project can be converted, you must close\n" +
"files in running tools and make sure you have no files\n" + "checked out.");
"all open project files and tools.");
return;
}
if (!checkToolsClose()) {
return;
}
@ -430,7 +467,7 @@ public class ProjectInfoDialog extends DialogComponentProvider {
if (rep != null) {
StringBuffer confirmMsg = new StringBuffer();
confirmMsg.append("All version history on your files will be\n" +
"lost after your project is converted.\n" +
"lost after your project is converted and checkouts terminated.\n" +
"Do you want to convert your project?\n");
confirmMsg.append(" \n");
confirmMsg.append("WARNING: Convert CANNOT be undone!");
@ -442,11 +479,12 @@ public class ProjectInfoDialog extends DialogComponentProvider {
ConvertProjectTask task = new ConvertProjectTask(rep);
new TaskLauncher(task, getComponent(), 500);
// block until task completes
ProjectLocator projectLocator = project.getProjectLocator();
if (task.getStatus()) {
close();
FileActionManager actionMgr = plugin.getFileActionManager();
actionMgr.closeProject(false);
actionMgr.openProject(project.getProjectLocator());
actionMgr.openProject(projectLocator);
plugin.getProjectActionManager().showProjectInfo();
}
else {
@ -456,36 +494,27 @@ public class ProjectInfoDialog extends DialogComponentProvider {
}
}
private boolean filesAreOpen() {
PluginTool[] tools = project.getToolManager().getRunningTools();
if (tools.length > 0) {
for (PluginTool tool : tools) {
if (tool.getDomainFiles().length > 0) {
return true;
}
}
}
return false;
private int getOpenFileCount() {
List<DomainFile> openFiles = new ArrayList<>();
project.getProjectData().findOpenFiles(openFiles);
TransientDataManager.getTransients(openFiles);
return openFiles.size();
}
private class ConvertProjectTask extends Task {
private RepositoryAdapter taskRepository;
private RepositoryAdapter newRepository;
private boolean status;
ConvertProjectTask(RepositoryAdapter repository) {
super("Convert Project to Shared", true, false, true);
this.taskRepository = repository;
this.newRepository = repository;
}
/* (non-Javadoc)
* @see ghidra.util.task.Task#run(ghidra.util.task.TaskMonitor)
*/
@Override
public void run(TaskMonitor monitor) {
try {
project.getProjectData().convertProjectToShared(taskRepository, monitor);
newRepository.connect();
project.getProjectData().convertProjectToShared(newRepository, monitor);
status = true;
}
catch (IOException e) {
@ -515,9 +544,6 @@ public class ProjectInfoDialog extends DialogComponentProvider {
this.projectLocator = projectLocator;
}
/* (non-Javadoc)
* @see ghidra.util.task.Task#run(ghidra.util.task.TaskMonitor)
*/
@Override
public void run(TaskMonitor monitor) {
try {
@ -544,22 +570,20 @@ public class ProjectInfoDialog extends DialogComponentProvider {
}
private class UpdateInfoTask extends Task {
private RepositoryAdapter taskRepository;
private RepositoryAdapter newRepository;
private boolean status;
UpdateInfoTask(RepositoryAdapter repository) {
super("Update Shared Project Info", true, false, true);
this.taskRepository = repository;
this.newRepository = repository;
}
/* (non-Javadoc)
* @see ghidra.util.task.Task#run(ghidra.util.task.TaskMonitor)
*/
@Override
public void run(TaskMonitor monitor) {
try {
// NOTE: conversion of non-shared project will lose version history
project.getProjectData().updateRepositoryInfo(taskRepository, monitor);
newRepository.connect();
boolean force = useForcedCheckoutTransition(monitor);
project.getProjectData().updateRepositoryInfo(newRepository, force, monitor);
status = true;
}
catch (IOException e) {
@ -575,6 +599,36 @@ public class ProjectInfoDialog extends DialogComponentProvider {
}
}
private boolean useForcedCheckoutTransition(TaskMonitor monitor) throws CancelledException, IOException {
if (repository == null) {
return false;
}
ProjectData projectData = project.getProjectData();
List<DomainFile> checkoutFiles = projectData.findCheckedOutFiles(monitor);
if (checkoutFiles.isEmpty() ||
!projectData.hasInvalidCheckouts(checkoutFiles, newRepository, monitor)) {
return false;
}
if (OptionDialog.showOptionDialog(getComponent(), "Terminate Unrecognized Checkouts",
"One or more project file checkouts are not recognized by the selected repository.\n" +
"These checkouts will be terminated and a local .keep file created." +
(repository.isConnected() ? ""
: " Doing this\n" +
"will abandon such checkouts on the old repository since you are not connected.") +
"\n\n" +
"Are you sure you want to continue changing your shared project information?",
"Terminate Checkouts and Continue",
OptionDialog.QUESTION_MESSAGE) != OptionDialog.OPTION_ONE) {
throw new CancelledException();
}
// Must force termination if not connected to current repository
return !repository.isConnected();
}
boolean getStatus() {
return status;
}

View file

@ -21,6 +21,7 @@ import java.util.Map;
import javax.swing.Icon;
import ghidra.framework.client.NotConnectedException;
import ghidra.framework.data.CheckinHandler;
import ghidra.framework.store.*;
import ghidra.util.InvalidNameException;
@ -403,11 +404,26 @@ public interface DomainFile extends Comparable<DomainFile> {
* Undo "checked-out" file. The original repository file is restored.
* @param keep if true, the private database will be renamed with a .keep
* extension.
* @throws NotConnectedException if shared project and not connected to repository
* @throws FileInUseException if this file is in-use / checked-out.
* @throws IOException thrown if file is not checked-out or an IO / access error occurs.
*/
public void undoCheckout(boolean keep) throws IOException;
/**
* Undo "checked-out" file. The original repository file is restored.
* @param keep if true, the private database will be renamed with a .keep
* extension.
* @param force if not connected to the repository the local checkout file will be removed.
* Warning: forcing undo checkout will leave a stale checkout in place for the associated
* repository if not connected.
* @throws NotConnectedException if shared project and not connected to repository and
* force is false
* @throws FileInUseException if this file is in-use / checked-out.
* @throws IOException thrown if file is not checked-out or an IO / access error occurs.
*/
public void undoCheckout(boolean keep, boolean force) throws IOException;
/**
* Forcefully terminate a checkout for the associated versioned file.
* The user must be the owner of the checkout or have administrator privilege

View file

@ -15,6 +15,10 @@
*/
package ghidra.framework.model;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.remote.User;
import ghidra.framework.store.local.LocalFileSystem;
@ -22,10 +26,6 @@ import ghidra.util.InvalidNameException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import java.io.IOException;
import java.net.URL;
import java.util.List;
/**
* The ProjectData interface provides access to all the data files and folders
* in a project.
@ -79,6 +79,30 @@ public interface ProjectData {
*/
public void findOpenFiles(List<DomainFile> list);
/**
* Find all project files which are currently checked-out to this project
* @param monitor task monitor (no progress updates)
* @return list of current checkout files
* @throws IOException if IO error occurs
* @throws CancelledException if task cancelled
*/
public List<DomainFile> findCheckedOutFiles(TaskMonitor monitor)
throws IOException, CancelledException;
/**
* Determine if any domain files listed do not correspond to a checkout in the specified
* newRespository prior to invoking {@link #updateRepositoryInfo(RepositoryAdapter, boolean, TaskMonitor)}.
* @param checkoutList project domain files to check
* @param newRepository repository to check against before updating
* @param monitor task monitor
* @return true if one or more files are not valid checkouts in newRepository
* @throws IOException if IO error occurs
* @throws CancelledException if task cancelled
*/
public boolean hasInvalidCheckouts(List<DomainFile> checkoutList,
RepositoryAdapter newRepository, TaskMonitor monitor)
throws IOException, CancelledException;
/**
* Get domain file specified by its unique fileID.
* @param fileID domain file ID
@ -157,16 +181,19 @@ public interface ProjectData {
/**
* Update the repository for this project; the server may have changed or a different
* repository is being used. NOTE: The project should be closed and then reopened after this
* method is called.
* @param repository new repository to use
* repository is being used. Any existing checkout which is not recognized/valid by
* newRepository will be terminated and a local .keep file created.
* NOTE: The project should be closed and then reopened after this method is called.
* @param newRepository new repository to use
* @param force if true any existing local checkout which is not recognized/valid
* for newRepository will be forceably terminated if offline with old repository.
* @param monitor task monitor
* @throws IOException thrown if files are still checked out, or if there was a problem accessing
* the filesystem
* @throws CancelledException if the user canceled the update
*/
public void updateRepositoryInfo(RepositoryAdapter repository, TaskMonitor monitor)
throws IOException, CancelledException;
public void updateRepositoryInfo(RepositoryAdapter newRepository, boolean force,
TaskMonitor monitor) throws IOException, CancelledException;
/**
* Close the project storage associated with this project data object.

View file

@ -1207,7 +1207,7 @@ public abstract class PluginTool extends AbstractDockingTool {
else {
beep();
Msg.showInfo(getClass(), getToolFrame(), "Tool Busy",
"You must stop all background tasks before exiting.");
"You must stop all background tasks before tool may close.");
return false;
}
}

View file

@ -287,6 +287,11 @@ public class TestDummyDomainFile implements DomainFile {
throw new UnsupportedOperationException();
}
@Override
public void undoCheckout(boolean keep, boolean force) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void terminateCheckout(long checkoutId) throws IOException {
throw new UnsupportedOperationException();

View file

@ -63,6 +63,21 @@ public class TestDummyProjectData implements ProjectData {
// stub
}
@Override
public List<DomainFile> findCheckedOutFiles(TaskMonitor monitor)
throws IOException, CancelledException {
// stub
return List.of();
}
@Override
public boolean hasInvalidCheckouts(List<DomainFile> checkoutList,
RepositoryAdapter newRepository, TaskMonitor monitor)
throws IOException, CancelledException {
// stub
return false;
}
@Override
public DomainFile getFileByID(String fileID) {
// stub
@ -121,8 +136,8 @@ public class TestDummyProjectData implements ProjectData {
}
@Override
public void updateRepositoryInfo(RepositoryAdapter repository, TaskMonitor monitor)
throws IOException, CancelledException {
public void updateRepositoryInfo(RepositoryAdapter repository, boolean force,
TaskMonitor monitor) throws IOException, CancelledException {
// stub
}

View file

@ -329,7 +329,7 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
FrontEndPlugin plugin = getPlugin(tool, FrontEndPlugin.class);
JComponent projectDataPanel = (JComponent) getInstanceField("projectDataPanel", plugin);
JTabbedPane tabbedPane =
(JTabbedPane) getInstanceField("projectTabPanel", projectDataPanel);
(JTabbedPane) getInstanceField("projectTab", projectDataPanel);
tabbedPane.setSelectedIndex(1);
setToolSize(800, 600);
captureComponent(projectDataPanel);

View file

@ -25,7 +25,6 @@ import javax.swing.*;
import org.junit.*;
import org.junit.experimental.categories.Category;
import docking.AbstractErrDialog;
import docking.action.DockingActionIf;
import docking.widgets.OptionDialog;
import docking.wizard.WizardManager;
@ -278,7 +277,8 @@ public class ProjectInfoDialogTest extends AbstractGhidraHeadedIntegrationTest {
waitForTasks();
// check out file from shared project
rootFolder = getProject().getProjectData().getRootFolder();
Project oldProject = getProject();
rootFolder = oldProject.getProjectData().getRootFolder();
DomainFile df = rootFolder.getFile("testA");
df.addToVersionControl("test", true, TaskMonitor.DUMMY);
assertTrue(df.isCheckedOut());
@ -299,11 +299,34 @@ public class ProjectInfoDialogTest extends AbstractGhidraHeadedIntegrationTest {
assertNotNull(opt);
assertEquals("Update Shared Project Info", opt.getTitle());
pressButtonByText(opt, "Update");
opt = waitForDialogComponent(OptionDialog.class);
assertNotNull(opt);
assertEquals("Terminate Unrecognized Checkouts", opt.getTitle());
pressButtonByText(opt, "Terminate Checkouts and Continue");
waitForTasks();
AbstractErrDialog errorDialog = waitForErrorDialog();
assertEquals("Failed to Update Shared Project Info", errorDialog.getTitle());
close(errorDialog);
dialog = waitForDialogComponent(ProjectInfoDialog.class);
assertNotNull(dialog);
pressButtonByText(dialog, "Dismiss");
Project updatedProject = getProject();
assertNotNull(updatedProject);
assertTrue(updatedProject != oldProject);
RepositoryAdapter rep = updatedProject.getRepository();
assertNotNull(rep);
assertEquals("AnotherRepository", rep.getName());
ProjectData updatedProjectData = updatedProject.getProjectData();
rootFolder = updatedProjectData.getRootFolder();
assertNull(rootFolder.getFile("testA"));
df = rootFolder.getFile("testA.keep");
assertNotNull(df);
assertFalse(df.isVersioned());
assertFalse(df.isCheckedOut());
}
private void checkProjectInfo(String expectedRepName) {