@Nonnull
	public WorkspacePatch createPatch(@Nonnull Workspace workspace) throws PatchGenerationException {
		List<RemovePath> removals = new ArrayList<>();
		List<JvmAssemblerPatch> jvmAssemblerPatches = new ArrayList<>();
		List<TextFilePatch> textFilePatches = new ArrayList<>();
		PatchConsumer<ClassPathNode, JvmClassInfo> classConsumer = (classPath, initial, current) -> {
			DirectoryPathNode parent = Objects.requireNonNull(classPath.getParent());
			ClassPathNode initialPath = parent.child(initial);
			ClassPathNode currentPath = parent.child(current);
			JvmAssemblerPipeline assembler = assemblerPipelineManager.newJvmAssemblerPipeline(workspace);
			Result<String> initialDisassembleRes = assembler.disassemble(initialPath);
			Result<String> currentDisassembleRes = assembler.disassemble(currentPath);
			if (!initialDisassembleRes.hasValue())
				throw new PatchGenerationException("Failed to disassemble initial state of '" + initial.getName() + "'");
			if (initialDisassembleRes.hasErr())
				throw new PatchGenerationException("Initial state of '" + initial.getName() + "' has assembler errors");
			if (!currentDisassembleRes.hasValue())
				throw new PatchGenerationException("Failed to disassemble current state of '" + initial.getName() + "'");
			if (currentDisassembleRes.hasErr())
				throw new PatchGenerationException("Current state of '" + initial.getName() + "' has assembler errors");
			String initialDisassemble = initialDisassembleRes.get();
			String currentDisassemble = currentDisassembleRes.get();
			List<StringDiff.Diff> assemblerDiffs = StringDiff.diff(initialDisassemble, currentDisassemble);
			if (!assemblerDiffs.isEmpty())
				jvmAssemblerPatches.add(new JvmAssemblerPatch(initialPath, assemblerDiffs));
		};
		PatchConsumer<FilePathNode, FileInfo> fileConsumer = (filePath, initial, current) -> {
			if (initial.isTextFile() && current.isTextFile()) {
				String initialText = initial.asTextFile().getText();
				String currentText = current.asTextFile().getText();
				List<StringDiff.Diff> textDiffs = StringDiff.diff(initialText, currentText);
				if (!textDiffs.isEmpty())
					textFilePatches.add(new TextFilePatch(filePath, textDiffs));
			} else {
				// TODO: Support binary patches of non-text files
				logger.debug("Skipping file diff for '{}' as it is not a text file", initial.getName());
			}
		};

		try {
			WorkspaceResource resource = workspace.getPrimaryResource();
			ResourcePathNode resourcePath = PathNodes.resourcePath(workspace, resource);
			resource.bundleStream().forEach(b -> {
				BundlePathNode bundlePath = resourcePath.child(b);
				Set<String> removedKeys = b.getRemovedKeys();
				for (String key : removedKeys) {
					if (b instanceof ClassBundle<?>) {
						ClassInfo stub = new StubClassInfo(key);
						ClassPathNode stubPath = bundlePath.child(stub.getPackageName()).child(stub);
						removals.add(new RemovePath(stubPath));
					} else {
						FileInfo stub = new StubFileInfo(key);
						FilePathNode stubPath = bundlePath.child(stub.getDirectoryName()).child(stub);
						removals.add(new RemovePath(stubPath));
					}
				}
			});
			visitDirtyItems(workspace, resource, resource.getJvmClassBundle(), classConsumer);
			for (var entry : resource.getVersionedJvmClassBundles().entrySet()) {
				visitDirtyItems(workspace, resource, entry.getValue(), classConsumer);
			}
			visitDirtyItems(workspace, resource, resource.getFileBundle(), fileConsumer);
		} catch (Throwable t) {
			throw new PatchGenerationException(t);
		}

		return new WorkspacePatch(workspace,
				Collections.unmodifiableList(removals),
				Collections.unmodifiableList(jvmAssemblerPatches),
				Collections.unmodifiableList(textFilePatches));
	}
Creates a patch which models all changes in the given workspace.

@param workspace
		Workspace to generate a patch for.

@return Patch modeling all changes made in the workspace.

@throws PatchGenerationException
		When the patch couldn't be made for any reason.
recaf-core/src/main/java/software/coley/recaf/services/workspace/patch/PatchProvider.java