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/DeleteProject.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/EditPluginPath.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/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/MemoryUsage.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/NonSharedProjectInfo.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| 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/VersionedFileCOwithServer.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/VersionedFileIcon.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/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/closedBookBlue.png||GHIDRA||reviewed||END|
src/main/help/help/topics/FrontEndPlugin/images/connected.gif||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/connected.gif||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/disconnected.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> <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> <BLOCKQUOTE>
<P>To update repository information:</P> <P>To update repository information:</P>
<OL> <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 <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> 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= and modify user privileges by choosing the <B>Project</B><IMG border="0" src=
"../../shared/arrow.gif"><B><A href= "../../shared/arrow.gif"><B><A href=
"Ghidra_Front_end.htm#Edit_Project_Access_List">Edit Project Access List...</A></B> <A "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> </BLOCKQUOTE>
<OL start="8"> <OL start="8">
<LI>Select the <B>Finish</B> button.&nbsp; &nbsp;</LI> <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 <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> </OL>
</BLOCKQUOTE> </BLOCKQUOTE>
<BLOCKQUOTE> <BLOCKQUOTE>
<P><IMG border="0" src="../../shared/note.png"> After you have updated <P><IMG border="0" src="../../shared/warning.png">
your project information, you may end up with <A href= If one or more checked-out files do not match-up properly with the new repository you will
"Ghidra_Front_end.htm#HijackedFile">hijacked files</A> if a file of the same name exists in be prompted to allow these checkouts to be terminated and converted to private <I>.keep</I>
the repository.&nbsp;&nbsp;</P> files. Such file conversion will prevent such files from ever being checked-in and
</BLOCKQUOTE> 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> <H2>&nbsp;</H2>
@ -225,8 +238,12 @@
<BLOCKQUOTE> <BLOCKQUOTE>
<P>The image below shows project information for a project that is not shared. Note that the <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 repository information will not be displayed for a private project. If repository information
any files that you have opened, and check in any files that you have checked out.</P> 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 <P><IMG border="0" src="../../shared/warning.png"> You will lose all
version history for files under local <A href= version history for files under local <A href=
@ -241,7 +258,7 @@
<BLOCKQUOTE> <BLOCKQUOTE>
<P>The steps to converting your project are the same as those described for <A href= <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 "#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 on versioned files. From the warning dialog, click on the <B>Convert</B> button to complete
the conversion process.&nbsp;</P> 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 parentPath parent folder path
* @param itemName name of item * @param itemName name of item
* @return checkout data list * @return checkout data list
* @throws FileNotFoundException if folder item not found
* @throws IOException if an IO error occurs * @throws IOException if an IO error occurs
*/ */
ItemCheckoutStatus[] getCheckouts(String parentPath, String itemName) throws IOException; 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"); 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 @Override
public ChangeSet getChangesByOthersSinceCheckout() throws IOException { public ChangeSet getChangesByOthersSinceCheckout() throws IOException {
return null; return null;

View file

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

View file

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

View file

@ -27,6 +27,7 @@ import ghidra.framework.remote.User;
import ghidra.framework.store.*; import ghidra.framework.store.*;
import ghidra.framework.store.FileSystem; import ghidra.framework.store.FileSystem;
import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFileSystem;
import ghidra.framework.store.local.LocalFolderItem;
import ghidra.framework.store.remote.RemoteFileSystem; import ghidra.framework.store.remote.RemoteFileSystem;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
@ -384,7 +385,7 @@ public class ProjectFileManager implements ProjectData {
} }
@Override @Override
public DomainFolder getFolder(String path) { public GhidraFolder getFolder(String path) {
int len = path.length(); int len = path.length();
if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
@ -565,33 +566,158 @@ public class ProjectFileManager implements ProjectData {
} }
@Override @Override
public void updateRepositoryInfo(RepositoryAdapter newRepository, TaskMonitor monitor) public void updateRepositoryInfo(RepositoryAdapter newRepository, boolean force,
TaskMonitor monitor)
throws IOException, CancelledException { 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); updatePropertiesFile(newRepository);
} }
private void findCheckedOutFiles(DomainFolder folder, TaskMonitor monitor) private boolean hasInvalidCheckout(DomainFile df, RepositoryAdapter newRepository)
throws IOException, CancelledException { throws IOException {
try {
DomainFile[] files = folder.getFiles(); LocalFolderItem item = fileSystem.getItem(df.getParent().getPathname(), df.getName());
for (DomainFile file : files) { if (item == null) {
if (monitor.isCancelled()) { return false;
throw new CancelledException();
} }
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(); catch (FileNotFoundException e) {
for (DomainFolder folder2 : folders) { return true;
if (monitor.isCancelled()) { }
throw new CancelledException(); 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.awt.FlowLayout;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.BevelBorder; import javax.swing.border.BevelBorder;
@ -33,6 +35,7 @@ import docking.wizard.WizardManager;
import ghidra.app.util.GenericHelpTopics; import ghidra.app.util.GenericHelpTopics;
import ghidra.framework.client.*; import ghidra.framework.client.*;
import ghidra.framework.data.ConvertFileSystem; import ghidra.framework.data.ConvertFileSystem;
import ghidra.framework.data.TransientDataManager;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.remote.User; import ghidra.framework.remote.User;
@ -337,10 +340,17 @@ public class ProjectInfoDialog extends DialogComponentProvider {
} }
private void updateSharedProjectInfo() { private void updateSharedProjectInfo() {
if (filesAreOpen()) { int openCount = getOpenFileCount();
Msg.showInfo(getClass(), getComponent(), "Cannot Change Project Info with Open Files", if (openCount != 0) {
"Before your project info can be updated, you must close\n" + Msg.showInfo(getClass(), getComponent(),
"files in running tools and make sure you have no files\n" + "checked out."); "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; return;
} }
@ -356,8 +366,10 @@ public class ProjectInfoDialog extends DialogComponentProvider {
currentRepository.getName().equals(rep.getName())) { currentRepository.getName().equals(rep.getName())) {
Msg.showInfo(getClass(), getComponent(), "No Changes Made", Msg.showInfo(getClass(), getComponent(), "No Changes Made",
"No changes were made to the shared project information."); "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", "Are you sure you want to update your shared project information?", "Update",
OptionDialog.QUESTION_MESSAGE) == OptionDialog.OPTION_ONE) { 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() { private void convertToIndexedFilesystem() {
if (filesAreOpen()) { int openCount = getOpenFileCount();
if (openCount != 0) {
Msg.showInfo(getClass(), getComponent(), Msg.showInfo(getClass(), getComponent(),
"Cannot Convert/Upgrade Project Storage with Open Files", "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" + "Before your project can be converted, you must close\n" +
"files in running tools."); "all open project files and tools.");
return;
}
if (!checkToolsClose()) {
return; return;
} }
@ -415,10 +444,18 @@ public class ProjectInfoDialog extends DialogComponentProvider {
} }
private void convertToShared() { 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" + "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; return;
} }
@ -430,7 +467,7 @@ public class ProjectInfoDialog extends DialogComponentProvider {
if (rep != null) { if (rep != null) {
StringBuffer confirmMsg = new StringBuffer(); StringBuffer confirmMsg = new StringBuffer();
confirmMsg.append("All version history on your files will be\n" + 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"); "Do you want to convert your project?\n");
confirmMsg.append(" \n"); confirmMsg.append(" \n");
confirmMsg.append("WARNING: Convert CANNOT be undone!"); confirmMsg.append("WARNING: Convert CANNOT be undone!");
@ -442,11 +479,12 @@ public class ProjectInfoDialog extends DialogComponentProvider {
ConvertProjectTask task = new ConvertProjectTask(rep); ConvertProjectTask task = new ConvertProjectTask(rep);
new TaskLauncher(task, getComponent(), 500); new TaskLauncher(task, getComponent(), 500);
// block until task completes // block until task completes
ProjectLocator projectLocator = project.getProjectLocator();
if (task.getStatus()) { if (task.getStatus()) {
close(); close();
FileActionManager actionMgr = plugin.getFileActionManager(); FileActionManager actionMgr = plugin.getFileActionManager();
actionMgr.closeProject(false); actionMgr.closeProject(false);
actionMgr.openProject(project.getProjectLocator()); actionMgr.openProject(projectLocator);
plugin.getProjectActionManager().showProjectInfo(); plugin.getProjectActionManager().showProjectInfo();
} }
else { else {
@ -456,36 +494,27 @@ public class ProjectInfoDialog extends DialogComponentProvider {
} }
} }
private boolean filesAreOpen() { private int getOpenFileCount() {
PluginTool[] tools = project.getToolManager().getRunningTools(); List<DomainFile> openFiles = new ArrayList<>();
project.getProjectData().findOpenFiles(openFiles);
if (tools.length > 0) { TransientDataManager.getTransients(openFiles);
for (PluginTool tool : tools) { return openFiles.size();
if (tool.getDomainFiles().length > 0) {
return true;
}
}
}
return false;
} }
private class ConvertProjectTask extends Task { private class ConvertProjectTask extends Task {
private RepositoryAdapter taskRepository; private RepositoryAdapter newRepository;
private boolean status; private boolean status;
ConvertProjectTask(RepositoryAdapter repository) { ConvertProjectTask(RepositoryAdapter repository) {
super("Convert Project to Shared", true, false, true); 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 @Override
public void run(TaskMonitor monitor) { public void run(TaskMonitor monitor) {
try { try {
project.getProjectData().convertProjectToShared(taskRepository, monitor); newRepository.connect();
project.getProjectData().convertProjectToShared(newRepository, monitor);
status = true; status = true;
} }
catch (IOException e) { catch (IOException e) {
@ -515,9 +544,6 @@ public class ProjectInfoDialog extends DialogComponentProvider {
this.projectLocator = projectLocator; this.projectLocator = projectLocator;
} }
/* (non-Javadoc)
* @see ghidra.util.task.Task#run(ghidra.util.task.TaskMonitor)
*/
@Override @Override
public void run(TaskMonitor monitor) { public void run(TaskMonitor monitor) {
try { try {
@ -544,22 +570,20 @@ public class ProjectInfoDialog extends DialogComponentProvider {
} }
private class UpdateInfoTask extends Task { private class UpdateInfoTask extends Task {
private RepositoryAdapter taskRepository; private RepositoryAdapter newRepository;
private boolean status; private boolean status;
UpdateInfoTask(RepositoryAdapter repository) { UpdateInfoTask(RepositoryAdapter repository) {
super("Update Shared Project Info", true, false, true); 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 @Override
public void run(TaskMonitor monitor) { public void run(TaskMonitor monitor) {
try { try {
// NOTE: conversion of non-shared project will lose version history newRepository.connect();
project.getProjectData().updateRepositoryInfo(taskRepository, monitor); boolean force = useForcedCheckoutTransition(monitor);
project.getProjectData().updateRepositoryInfo(newRepository, force, monitor);
status = true; status = true;
} }
catch (IOException e) { 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() { boolean getStatus() {
return status; return status;
} }

View file

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

View file

@ -15,6 +15,10 @@
*/ */
package ghidra.framework.model; package ghidra.framework.model;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.remote.User; import ghidra.framework.remote.User;
import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFileSystem;
@ -22,10 +26,6 @@ import ghidra.util.InvalidNameException;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; 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 * The ProjectData interface provides access to all the data files and folders
* in a project. * in a project.
@ -79,6 +79,30 @@ public interface ProjectData {
*/ */
public void findOpenFiles(List<DomainFile> list); 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. * Get domain file specified by its unique fileID.
* @param fileID domain file ID * @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 * 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 * repository is being used. Any existing checkout which is not recognized/valid by
* method is called. * newRepository will be terminated and a local .keep file created.
* @param repository new repository to use * 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 * @param monitor task monitor
* @throws IOException thrown if files are still checked out, or if there was a problem accessing * @throws IOException thrown if files are still checked out, or if there was a problem accessing
* the filesystem * the filesystem
* @throws CancelledException if the user canceled the update * @throws CancelledException if the user canceled the update
*/ */
public void updateRepositoryInfo(RepositoryAdapter repository, TaskMonitor monitor) public void updateRepositoryInfo(RepositoryAdapter newRepository, boolean force,
throws IOException, CancelledException; TaskMonitor monitor) throws IOException, CancelledException;
/** /**
* Close the project storage associated with this project data object. * Close the project storage associated with this project data object.

View file

@ -1207,7 +1207,7 @@ public abstract class PluginTool extends AbstractDockingTool {
else { else {
beep(); beep();
Msg.showInfo(getClass(), getToolFrame(), "Tool Busy", 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; return false;
} }
} }

View file

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

View file

@ -63,6 +63,21 @@ public class TestDummyProjectData implements ProjectData {
// stub // 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 @Override
public DomainFile getFileByID(String fileID) { public DomainFile getFileByID(String fileID) {
// stub // stub
@ -121,8 +136,8 @@ public class TestDummyProjectData implements ProjectData {
} }
@Override @Override
public void updateRepositoryInfo(RepositoryAdapter repository, TaskMonitor monitor) public void updateRepositoryInfo(RepositoryAdapter repository, boolean force,
throws IOException, CancelledException { TaskMonitor monitor) throws IOException, CancelledException {
// stub // stub
} }

View file

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

View file

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