// Copyright (c) 2018 Artur Shepilko // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "commiteditor.h" #include "configuredialog.h" #include "constants.h" #include "fossilclient.h" #include "fossilcommitwidget.h" #include "fossileditor.h" #include "fossiltr.h" #include "pullorpushdialog.h" #include "wizard/fossiljsextension.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_TESTS #include #endif using namespace Core; using namespace Utils; using namespace VcsBase; using namespace std::placeholders; namespace Fossil::Internal { class FossilPluginPrivate final : public VersionControlBase { public: enum SyncMode { SyncPull, SyncPush }; FossilPluginPrivate(); // IVersionControl QString displayName() const final { return "Fossil"; } Id id() const final; bool isVcsFileOrDirectory(const FilePath &filePath) const final; bool managesDirectory(const FilePath &directory, FilePath *topLevel) const final; bool managesFile(const FilePath &workingDirectory, const QString &fileName) const final; bool isConfigured() const final; bool supportsOperation(Operation operation) const final; bool vcsOpen(const FilePath &fileName) final; bool vcsAdd(const FilePath &fileName) final; bool vcsDelete(const FilePath &filename) final; bool vcsMove(const FilePath &from, const FilePath &to) final; bool vcsCreateRepository(const FilePath &directory) final; void vcsAnnotate(const FilePath &file, int line) final; void vcsDescribe(const FilePath &source, const QString &id) final; void vcsLog(const Utils::FilePath &topLevel, const Utils::FilePath &relativeDirectory) final { FossilClient::SupportedFeatures features = fossilClient().supportedFeatures(); QStringList options = {"-n", QString::number(fossilClient().settings().logCount())}; if (features.testFlag(FossilClient::TimelineWidthFeature)) options << "-W" << QString::number(fossilClient().settings().timelineWidth()); fossilClient().log(topLevel, {relativeDirectory.path()}, options); } VcsCommand *createInitialCheckoutCommand(const QString &url, const FilePath &baseDirectory, const QString &localName, const QStringList &extraArgs) final; void updateActions(VersionControlBase::ActionState) override; bool activateCommit() override; // File menu action slots void addCurrentFile(); void deleteCurrentFile(); void annotateCurrentFile(); void diffCurrentFile(); void logCurrentFile(); void revertCurrentFile(); void statusCurrentFile(); // Directory menu action slots void diffRepository(); void logRepository(); void revertAll(); void statusMulti(); // Repository menu action slots void pull() { pullOrPush(SyncPull); } void push() { pullOrPush(SyncPush); } void update(); void configureRepository(); void commit(); void showCommitWidget(const QList &status); void diffFromEditorSelected(const QStringList &files); void createRepository(); // Methods void createMenu(const Context &context); void createFileActions(const Context &context); void createDirectoryActions(const Context &context); void createRepositoryActions(const Context &context); bool pullOrPush(SyncMode mode); // Variables VcsEditorFactory fileLogFactory {{ LogOutput, Constants::FILELOG_ID, VcsBase::Tr::tr("Fossil File Log Editor"), Constants::LOGAPP, &createFossilEditorWidget, std::bind(&FossilPluginPrivate::vcsDescribe, this, _1, _2) }}; VcsEditorFactory annotateLogFactory {{ AnnotateOutput, Constants::ANNOTATELOG_ID, VcsBase::Tr::tr("Fossil Annotation Editor"), Constants::ANNOTATEAPP, &createFossilEditorWidget, std::bind(&FossilPluginPrivate::vcsDescribe, this, _1, _2) }}; VcsEditorFactory diffFactory {{ DiffOutput, Constants::DIFFLOG_ID, VcsBase::Tr::tr("Fossil Diff Editor"), Constants::DIFFAPP, &createFossilEditorWidget, std::bind(&FossilPluginPrivate::vcsDescribe, this, _1, _2) }}; CommandLocator *m_commandLocator = nullptr; ActionContainer *m_fossilContainer = nullptr; QList m_repositoryActionList; // Menu Items (file actions) Action *m_addAction = nullptr; Action *m_deleteAction = nullptr; Action *m_annotateFile = nullptr; Action *m_diffFile = nullptr; Action *m_logFile = nullptr; Action *m_revertFile = nullptr; Action *m_statusFile = nullptr; QAction *m_createRepositoryAction = nullptr; // Submit editor actions QAction *m_menuAction = nullptr; FilePath m_submitRepository; // To be connected to the VcsTask's success signal to emit the repository/ // files changed signals according to the variant's type: // String -> repository, StringList -> files void changed(const QVariant &); }; static FossilPluginPrivate *dd = nullptr; class RevertDialog : public QDialog { public: RevertDialog(const QString &title, QWidget *parent = nullptr); QString revision() const { return m_revisionLineEdit->text(); } private: QLineEdit *m_revisionLineEdit = nullptr; }; FossilPluginPrivate::FossilPluginPrivate() : VersionControlBase(Context(Constants::FOSSIL_CONTEXT)) { Context context(Constants::FOSSIL_CONTEXT); setTopicFileTracker([](const FilePath &repository) { return repository.pathAppended(Constants::FOSSILREPO); }); setTopicRefresher([](const FilePath &repository) { return fossilClient().synchronousTopic(repository); }); connect(&fossilClient(), &VcsBaseClient::changed, this, &FossilPluginPrivate::changed); m_commandLocator = new CommandLocator("Fossil", "fossil", "fossil", this); m_commandLocator->setDescription(Tr::tr("Triggers a Fossil version control operation.")); JsExpander::registerGlobalObject("Fossil", [] { return new FossilJsExtension; }); connect(&settings(), &AspectContainer::changed, this, &IVersionControl::configurationChanged); createMenu(context); setupVcsSubmitEditor(this, { Constants::COMMITMIMETYPE, Constants::COMMIT_ID, VcsBase::Tr::tr("Fossil Commit Log Editor"), VcsBaseSubmitEditorParameters::DiffFiles, [] { return new CommitEditor; } }); } void FossilPluginPrivate::createMenu(const Context &context) { // Create menu item for Fossil m_fossilContainer = ActionManager::createMenu("Fossil.FossilMenu"); QMenu *menu = m_fossilContainer->menu(); menu->setTitle(Tr::tr("&Fossil")); createFileActions(context); m_fossilContainer->addSeparator(context); createDirectoryActions(context); m_fossilContainer->addSeparator(context); createRepositoryActions(context); m_fossilContainer->addSeparator(context); // Request the Tools menu and add the Fossil menu to it ActionContainer *toolsMenu = ActionManager::actionContainer(Core::Constants::M_TOOLS); toolsMenu->addMenu(m_fossilContainer); m_menuAction = m_fossilContainer->menu()->menuAction(); } void FossilPluginPrivate::createFileActions(const Context &context) { Command *command; m_annotateFile = new Action(Tr::tr("Annotate Current File"), Tr::tr("Annotate \"%1\""), Action::EnabledWithParameter, this); command = ActionManager::registerAction(m_annotateFile, Constants::ANNOTATE, context); command->setAttribute(Command::CA_UpdateText); connect(m_annotateFile, &QAction::triggered, this, &FossilPluginPrivate::annotateCurrentFile); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); m_diffFile = new Action(Tr::tr("Diff Current File"), Tr::tr("Diff \"%1\""), Action::EnabledWithParameter, this); command = ActionManager::registerAction(m_diffFile, Constants::DIFF, context); command->setAttribute(Command::CA_UpdateText); command->setDefaultKeySequence(QKeySequence(Core::useMacShortcuts ? Tr::tr("Meta+I,Meta+D") : Tr::tr("Alt+I,Alt+D"))); connect(m_diffFile, &QAction::triggered, this, &FossilPluginPrivate::diffCurrentFile); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); m_logFile = new Action(Tr::tr("Timeline Current File"), Tr::tr("Timeline \"%1\""), Action::EnabledWithParameter, this); command = ActionManager::registerAction(m_logFile, Constants::LOG, context); command->setAttribute(Command::CA_UpdateText); command->setDefaultKeySequence(QKeySequence(Core::useMacShortcuts ? Tr::tr("Meta+I,Meta+L") : Tr::tr("Alt+I,Alt+L"))); connect(m_logFile, &QAction::triggered, this, &FossilPluginPrivate::logCurrentFile); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); m_statusFile = new Action(Tr::tr("Status Current File"), Tr::tr("Status \"%1\""), Action::EnabledWithParameter, this); command = ActionManager::registerAction(m_statusFile, Constants::STATUS, context); command->setAttribute(Command::CA_UpdateText); command->setDefaultKeySequence(QKeySequence(Core::useMacShortcuts ? Tr::tr("Meta+I,Meta+S") : Tr::tr("Alt+I,Alt+S"))); connect(m_statusFile, &QAction::triggered, this, &FossilPluginPrivate::statusCurrentFile); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); m_fossilContainer->addSeparator(context); m_addAction = new Action(Tr::tr("Add Current File"), Tr::tr("Add \"%1\""), Action::EnabledWithParameter, this); command = ActionManager::registerAction(m_addAction, Constants::ADD, context); command->setAttribute(Command::CA_UpdateText); connect(m_addAction, &QAction::triggered, this, &FossilPluginPrivate::addCurrentFile); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); m_deleteAction = new Action(Tr::tr("Delete Current File..."), Tr::tr("Delete \"%1\"..."), Action::EnabledWithParameter, this); command = ActionManager::registerAction(m_deleteAction, Constants::DELETE, context); command->setAttribute(Command::CA_UpdateText); connect(m_deleteAction, &QAction::triggered, this, &FossilPluginPrivate::deleteCurrentFile); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); m_revertFile = new Action(Tr::tr("Revert Current File..."), Tr::tr("Revert \"%1\"..."), Action::EnabledWithParameter, this); command = ActionManager::registerAction(m_revertFile, Constants::REVERT, context); command->setAttribute(Command::CA_UpdateText); connect(m_revertFile, &QAction::triggered, this, &FossilPluginPrivate::revertCurrentFile); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); } void FossilPluginPrivate::addCurrentFile() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasFile(), return); fossilClient().synchronousAdd(state.currentFileTopLevel(), state.relativeCurrentFile()); } void FossilPluginPrivate::deleteCurrentFile() { promptToDeleteCurrentFile(); } void FossilPluginPrivate::annotateCurrentFile() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasFile(), return); const int lineNumber = VcsBaseEditor::lineNumberOfCurrentEditor(state.currentFile()); fossilClient().annotate(state.currentFileTopLevel(), state.relativeCurrentFile(), lineNumber); } void FossilPluginPrivate::diffCurrentFile() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasFile(), return); fossilClient().diff(state.currentFileTopLevel(), QStringList(state.relativeCurrentFile())); } void FossilPluginPrivate::logCurrentFile() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasFile(), return); FossilClient::SupportedFeatures features = fossilClient().supportedFeatures(); QStringList extraOptions; extraOptions << "-n" << QString::number(fossilClient().settings().logCount()); if (features.testFlag(FossilClient::TimelineWidthFeature)) extraOptions << "-W" << QString::number(fossilClient().settings().timelineWidth()); // disable annotate context menu for older client versions, used to be supported for current revision only bool enableAnnotationContextMenu = features.testFlag(FossilClient::AnnotateRevisionFeature); fossilClient().logCurrentFile(state.currentFileTopLevel(), QStringList(state.relativeCurrentFile()), extraOptions, enableAnnotationContextMenu); } void FossilPluginPrivate::revertCurrentFile() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasFile(), return); RevertDialog dialog(Tr::tr("Revert"), ICore::dialogParent()); if (dialog.exec() == QDialog::Accepted) { fossilClient().revertFile(state.currentFileTopLevel(), state.relativeCurrentFile(), dialog.revision()); } } void FossilPluginPrivate::statusCurrentFile() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasFile(), return); fossilClient().status(state.currentFileTopLevel(), state.relativeCurrentFile()); } void FossilPluginPrivate::createDirectoryActions(const Context &context) { QAction *action; Command *command; action = new QAction(Tr::tr("Diff"), this); m_repositoryActionList.append(action); command = ActionManager::registerAction(action, Constants::DIFFMULTI, context); connect(action, &QAction::triggered, this, &FossilPluginPrivate::diffRepository); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); action = new QAction(Tr::tr("Timeline"), this); m_repositoryActionList.append(action); command = ActionManager::registerAction(action, Constants::LOGMULTI, context); command->setDefaultKeySequence(QKeySequence(Core::useMacShortcuts ? Tr::tr("Meta+I,Meta+T") : Tr::tr("Alt+I,Alt+T"))); connect(action, &QAction::triggered, this, &FossilPluginPrivate::logRepository); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); action = new QAction(Tr::tr("Revert..."), this); m_repositoryActionList.append(action); command = ActionManager::registerAction(action, Constants::REVERTMULTI, context); connect(action, &QAction::triggered, this, &FossilPluginPrivate::revertAll); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); action = new QAction(Tr::tr("Status"), this); m_repositoryActionList.append(action); command = ActionManager::registerAction(action, Constants::STATUSMULTI, context); connect(action, &QAction::triggered, this, &FossilPluginPrivate::statusMulti); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); } void FossilPluginPrivate::diffRepository() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasTopLevel(), return); fossilClient().diff(state.topLevel()); } void FossilPluginPrivate::logRepository() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasTopLevel(), return); FossilClient::SupportedFeatures features = fossilClient().supportedFeatures(); QStringList extraOptions; extraOptions << "-n" << QString::number(fossilClient().settings().logCount()); if (features.testFlag(FossilClient::TimelineWidthFeature)) extraOptions << "-W" << QString::number(fossilClient().settings().timelineWidth()); fossilClient().log(state.topLevel(), {}, extraOptions); } void FossilPluginPrivate::revertAll() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasTopLevel(), return); RevertDialog dialog(Tr::tr("Revert"), ICore::dialogParent()); if (dialog.exec() == QDialog::Accepted) fossilClient().revertAll(state.topLevel(), dialog.revision()); } void FossilPluginPrivate::statusMulti() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasTopLevel(), return); fossilClient().status(state.topLevel()); } void FossilPluginPrivate::createRepositoryActions(const Context &context) { QAction *action = 0; Command *command = 0; action = new QAction(Tr::tr("Pull..."), this); m_repositoryActionList.append(action); command = ActionManager::registerAction(action, Constants::PULL, context); connect(action, &QAction::triggered, this, &FossilPluginPrivate::pull); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); action = new QAction(Tr::tr("Push..."), this); m_repositoryActionList.append(action); command = ActionManager::registerAction(action, Constants::PUSH, context); connect(action, &QAction::triggered, this, &FossilPluginPrivate::push); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); action = new QAction(Tr::tr("Update..."), this); m_repositoryActionList.append(action); command = ActionManager::registerAction(action, Constants::UPDATE, context); command->setDefaultKeySequence(QKeySequence(Core::useMacShortcuts ? Tr::tr("Meta+I,Meta+U") : Tr::tr("Alt+I,Alt+U"))); connect(action, &QAction::triggered, this, &FossilPluginPrivate::update); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); action = new QAction(Tr::tr("Commit..."), this); m_repositoryActionList.append(action); command = ActionManager::registerAction(action, Constants::COMMIT, context); command->setDefaultKeySequence(QKeySequence(Core::useMacShortcuts ? Tr::tr("Meta+I,Meta+C") : Tr::tr("Alt+I,Alt+C"))); connect(action, &QAction::triggered, this, &FossilPluginPrivate::commit); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); action = new QAction(Tr::tr("Settings..."), this); m_repositoryActionList.append(action); command = ActionManager::registerAction(action, Constants::CONFIGURE_REPOSITORY, context); connect(action, &QAction::triggered, this, &FossilPluginPrivate::configureRepository); m_fossilContainer->addAction(command); m_commandLocator->appendCommand(command); // Register "Create Repository..." action in global context, so that it's visible // without active repository to allow creating a new one. m_createRepositoryAction = new QAction(Tr::tr("Create Repository..."), this); command = ActionManager::registerAction(m_createRepositoryAction, Constants::CREATE_REPOSITORY); connect(m_createRepositoryAction, &QAction::triggered, this, &FossilPluginPrivate::createRepository); m_fossilContainer->addAction(command); } bool FossilPluginPrivate::pullOrPush(FossilPluginPrivate::SyncMode mode) { PullOrPushDialog::Mode pullOrPushMode; switch (mode) { case SyncPull: pullOrPushMode = PullOrPushDialog::PullMode; break; case SyncPush: pullOrPushMode = PullOrPushDialog::PushMode; break; default: return false; } const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasTopLevel(), return false); PullOrPushDialog dialog(pullOrPushMode, ICore::dialogParent()); dialog.setLocalBaseDirectory(fossilClient().settings().defaultRepoPath()); const QString defaultURL(fossilClient().synchronousGetRepositoryURL(state.topLevel())); dialog.setDefaultRemoteLocation(defaultURL); if (dialog.exec() != QDialog::Accepted) return true; QString remoteLocation(dialog.remoteLocation()); if (remoteLocation.isEmpty() && defaultURL.isEmpty()) { VcsOutputWindow::appendError(Tr::tr("Remote repository is not defined.")); return false; } else if (remoteLocation == defaultURL) { remoteLocation.clear(); } QStringList extraOptions; if (!remoteLocation.isEmpty() && !dialog.isRememberOptionEnabled()) extraOptions << "--once"; if (dialog.isPrivateOptionEnabled()) extraOptions << "--private"; switch (mode) { case SyncPull: return fossilClient().synchronousPull(state.topLevel(), remoteLocation, extraOptions); case SyncPush: return fossilClient().synchronousPush(state.topLevel(), remoteLocation, extraOptions); default: return false; } } void FossilPluginPrivate::update() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasTopLevel(), return); RevertDialog dialog(Tr::tr("Update"), ICore::dialogParent()); if (dialog.exec() == QDialog::Accepted) fossilClient().update(state.topLevel(), dialog.revision()); } void FossilPluginPrivate::configureRepository() { const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasTopLevel(), return); ConfigureDialog dialog; // retrieve current settings from the repository RepositorySettings currentSettings = fossilClient().synchronousSettingsQuery(state.topLevel()); dialog.setSettings(currentSettings); if (dialog.exec() != QDialog::Accepted) return; const RepositorySettings newSettings = dialog.settings(); fossilClient().synchronousConfigureRepository(state.topLevel(), newSettings, currentSettings); } void FossilPluginPrivate::commit() { if (!promptBeforeCommit()) return; if (raiseSubmitEditor()) return; const VcsBasePluginState state = currentState(); QTC_ASSERT(state.hasTopLevel(), return); m_submitRepository = state.topLevel(); connect(&fossilClient(), &VcsBaseClient::parsedStatus, this, &FossilPluginPrivate::showCommitWidget); fossilClient().emitParsedStatus(m_submitRepository, {}); } void FossilPluginPrivate::showCommitWidget(const QList &status) { //Once we receive our data release the connection so it can be reused elsewhere disconnect(&fossilClient(), &VcsBaseClient::parsedStatus, this, &FossilPluginPrivate::showCommitWidget); if (status.isEmpty()) { VcsOutputWindow::appendError(Tr::tr("There are no changes to commit.")); return; } // Start new temp file for commit message TempFileSaver saver; // Keep the file alive, else it removes self and forgets its name saver.setAutoRemove(false); if (!saver.finalize()) { VcsOutputWindow::appendError(saver.errorString()); return; } IEditor *editor = EditorManager::openEditor(saver.filePath(), Constants::COMMIT_ID); if (!editor) { VcsOutputWindow::appendError(Tr::tr("Unable to create an editor for the commit.")); return; } CommitEditor *commitEditor = qobject_cast(editor); if (!commitEditor) { VcsOutputWindow::appendError(Tr::tr("Unable to create a commit editor.")); return; } setSubmitEditor(commitEditor); const QString msg = Tr::tr("Commit changes for \"%1\".").arg(m_submitRepository.toUserOutput()); commitEditor->document()->setPreferredDisplayName(msg); const RevisionInfo currentRevision = fossilClient().synchronousRevisionQuery(m_submitRepository); const BranchInfo currentBranch = fossilClient().synchronousCurrentBranch(m_submitRepository); const QString currentUser = fossilClient().synchronousUserDefaultQuery(m_submitRepository); QStringList tags = fossilClient().synchronousTagQuery(m_submitRepository, currentRevision.id); // Fossil includes branch name in tag list -- remove. tags.removeAll(currentBranch.name); commitEditor->setFields(m_submitRepository, currentBranch, tags, currentUser, status); connect(commitEditor, &VcsBaseSubmitEditor::diffSelectedFiles, this, &FossilPluginPrivate::diffFromEditorSelected); commitEditor->setCheckScriptWorkingDirectory(m_submitRepository); } void FossilPluginPrivate::diffFromEditorSelected(const QStringList &files) { fossilClient().diff(m_submitRepository, files); } static inline bool ask(QWidget *parent, const QString &title, const QString &question, bool defaultValue = true) { const QMessageBox::StandardButton defaultButton = defaultValue ? QMessageBox::Yes : QMessageBox::No; return QMessageBox::question(parent, title, question, QMessageBox::Yes|QMessageBox::No, defaultButton) == QMessageBox::Yes; } void FossilPluginPrivate::createRepository() { // re-implemented from void VcsBasePlugin::createRepository() // Find current starting directory FilePath directory; if (const ProjectExplorer::Project *currentProject = ProjectExplorer::ProjectTree::currentProject()) directory = currentProject->projectDirectory(); // Prompt for a directory that is not under version control yet QWidget *mw = ICore::dialogParent(); do { directory = FileUtils::getExistingDirectory(Tr::tr("Choose Checkout Directory"), directory); if (directory.isEmpty()) return; const IVersionControl *managingControl = VcsManager::findVersionControlForDirectory(directory); if (managingControl == 0) break; const QString question = Tr::tr("The directory \"%1\" is already managed by a version control system (%2)." " Would you like to specify another directory?").arg(directory.toUserOutput(), managingControl->displayName()); if (!ask(mw, Tr::tr("Repository already under version control"), question)) return; } while (true); // Create const bool rc = vcsCreateRepository(directory); const QString nativeDir = directory.toUserOutput(); if (rc) { QMessageBox::information(mw, Tr::tr("Repository Created"), Tr::tr("A version control repository has been created in %1."). arg(nativeDir)); } else { QMessageBox::warning(mw, Tr::tr("Repository Creation Failed"), Tr::tr("A version control repository could not be created in %1."). arg(nativeDir)); } } bool FossilPluginPrivate::activateCommit() { CommitEditor *commitEditor = qobject_cast(submitEditor()); QTC_ASSERT(commitEditor, return true); IDocument *editorDocument = commitEditor->document(); QTC_ASSERT(editorDocument, return true); QStringList files = commitEditor->checkedFiles(); if (!files.empty()) { //save the commit message if (!DocumentManager::saveDocument(editorDocument)) return false; //rewrite entries of the form 'file => newfile' to 'newfile' because //this would mess the commit command for (QStringList::iterator iFile = files.begin(); iFile != files.end(); ++iFile) { const QStringList parts = iFile->split(" => ", Qt::SkipEmptyParts); if (!parts.isEmpty()) *iFile = parts.last(); } FossilCommitWidget *commitWidget = commitEditor->commitWidget(); QStringList extraOptions; // Author -- override the repository-default user if (!commitWidget->committer().isEmpty()) extraOptions << "--user" << commitWidget->committer(); // Branch QString branch = commitWidget->newBranch(); if (!branch.isEmpty()) { // @TODO: make enquote utility function QString enquotedBranch = branch; static const QRegularExpression regexp("\\s"); if (branch.contains(regexp)) enquotedBranch = QString("\"") + branch + "\""; extraOptions << "--branch" << enquotedBranch; } // Tags const QStringList tags = commitWidget->tags(); for (const QString &tag : tags) { extraOptions << "--tag" << tag; } // Whether local commit or not if (commitWidget->isPrivateOptionEnabled()) extraOptions += "--private"; fossilClient().commit(m_submitRepository, files, editorDocument->filePath().toUrlishString(), extraOptions); } return true; } void FossilPluginPrivate::updateActions(VersionControlBase::ActionState as) { m_createRepositoryAction->setEnabled(true); if (!enableMenuAction(as, m_menuAction)) { m_commandLocator->setEnabled(false); return; } const QString filename = currentState().currentFileName(); const bool repoEnabled = currentState().hasTopLevel(); m_commandLocator->setEnabled(repoEnabled); m_annotateFile->setParameter(filename); m_diffFile->setParameter(filename); m_logFile->setParameter(filename); m_addAction->setParameter(filename); m_deleteAction->setParameter(filename); m_revertFile->setParameter(filename); m_statusFile->setParameter(filename); for (QAction *repoAction : std::as_const(m_repositoryActionList)) repoAction->setEnabled(repoEnabled); } Id FossilPluginPrivate::id() const { return Id(Constants::VCS_ID_FOSSIL); } bool FossilPluginPrivate::isVcsFileOrDirectory(const FilePath &filePath) const { return fossilClient().isVcsFileOrDirectory(filePath); } bool FossilPluginPrivate::managesDirectory(const FilePath &directory, FilePath *topLevel) const { const FilePath topLevelFound = VcsManager::findRepositoryForFiles(directory, {Constants::FOSSILREPO}); if (topLevel) *topLevel = topLevelFound; return !topLevelFound.isEmpty(); } bool FossilPluginPrivate::managesFile(const FilePath &workingDirectory, const QString &fileName) const { return fossilClient().managesFile(workingDirectory, fileName); } bool FossilPluginPrivate::isConfigured() const { const FilePath binary = fossilClient().vcsBinary({}); if (binary.isEmpty()) return false; if (!binary.isExecutableFile()) return false; // Local repositories default path must be set and exist const FilePath repoPath = fossilClient().settings().defaultRepoPath(); if (repoPath.isEmpty()) return false; return repoPath.isReadableDir(); } bool FossilPluginPrivate::supportsOperation(Operation operation) const { bool supported = isConfigured(); switch (operation) { case IVersionControl::AddOperation: case IVersionControl::DeleteOperation: case IVersionControl::MoveOperation: case IVersionControl::CreateRepositoryOperation: case IVersionControl::AnnotateOperation: case IVersionControl::InitialCheckoutOperation: break; case IVersionControl::SnapshotOperations: supported = false; break; } return supported; } bool FossilPluginPrivate::vcsOpen(const FilePath &filePath) { Q_UNUSED(filePath) return true; } bool FossilPluginPrivate::vcsAdd(const FilePath &filePath) { return fossilClient().synchronousAdd(filePath.absolutePath(), filePath.fileName()); } bool FossilPluginPrivate::vcsDelete(const FilePath &filePath) { return fossilClient().synchronousRemove(filePath.absolutePath(), filePath.fileName()); } bool FossilPluginPrivate::vcsMove(const FilePath &from, const FilePath &to) { const QFileInfo fromInfo = from.toFileInfo(); const QFileInfo toInfo = to.toFileInfo(); return fossilClient().synchronousMove(from.absolutePath(), fromInfo.absoluteFilePath(), toInfo.absoluteFilePath()); } bool FossilPluginPrivate::vcsCreateRepository(const FilePath &directory) { return fossilClient().synchronousCreateRepository(directory); } void FossilPluginPrivate::vcsAnnotate(const FilePath &filePath, int line) { fossilClient().annotate(filePath.absolutePath(), filePath.fileName(), line); } void FossilPluginPrivate::vcsDescribe(const FilePath &source, const QString &id) { fossilClient().view(source, id); } VcsCommand *FossilPluginPrivate::createInitialCheckoutCommand(const QString &sourceUrl, const FilePath &baseDirectory, const QString &localName, const QStringList &extraArgs) { const QMap options = FossilJsExtension::parseArgOptions(extraArgs); // Two operating modes: // 1) CloneCheckout: // -- clone from remote-URL or a local-fossil a repository into a local-clone fossil. // -- open/checkout the local-clone fossil // The local-clone fossil must not point to an existing repository. // Clone URL may be either schema-based (http, ssh, file) or an absolute local path. // // 2) LocalCheckout: // -- open/checkout an existing local fossil // Clone URL is an absolute local path and is the same as the local fossil. const FilePath checkoutPath = baseDirectory.pathAppended(localName); const QString fossilFile = options.value("fossil-file"); const FilePath fossilFilePath = FilePath::fromUserInput(QDir::fromNativeSeparators(fossilFile)); const QString fossilFileNative = fossilFilePath.toUserOutput(); const QFileInfo cloneRepository(fossilFilePath.toUrlishString()); // Check when requested to clone a local repository and clone-into repository file is the same // or not specified. // In this case handle it as local fossil checkout request. const QUrl url(sourceUrl); bool isLocalRepository = (options.value("repository-type") == "localRepo"); if (url.isLocalFile() || url.isRelative()) { const QFileInfo sourcePath(url.path()); isLocalRepository = (sourcePath.canonicalFilePath() == cloneRepository.canonicalFilePath()); } // set clone repository admin user to configured user name // OR override it with the specified user from clone panel const QString adminUser = options.value("admin-user"); const bool disableAutosync = (options.value("settings-autosync") == "off"); const QString checkoutBranch = options.value("branch-tag"); // first create the checkout directory, // as it needs to become a working directory for wizard command jobs checkoutPath.createDir(); // Setup the wizard page command job auto command = VcsBaseClient::createVcsCommand(checkoutPath, fossilClient().processEnvironment(checkoutPath)); if (!isLocalRepository && !cloneRepository.exists()) { const QString sslIdentityFile = options.value("ssl-identity"); const FilePath sslIdentityFilePath = FilePath::fromUserInput(QDir::fromNativeSeparators(sslIdentityFile)); const bool includePrivate = (options.value("include-private") == "true"); QStringList extraOptions; if (includePrivate) extraOptions << "--private"; if (!sslIdentityFile.isEmpty()) extraOptions << "--ssl-identity" << sslIdentityFilePath.toUserOutput(); if (!adminUser.isEmpty()) extraOptions << "--admin-user" << adminUser; // Fossil allows saving the remote address and login. This is used to // facilitate autosync (commit/update) functionality. // When no password is given, it prompts for that. // When both username and password are specified, it prompts whether to // save them. // NOTE: In non-interactive context, these prompts won't work. // Fossil currently does not support SSH_ASKPASS way for login query. // // Alternatively, "--once" option does not save the remote details. // In such case remote details must be provided on the command-line every // time. This also precludes autosync. // // So here we want Fossil to save the remote details when specified. command->addJob({fossilClient().vcsBinary(checkoutPath), {fossilClient().vcsCommandString(FossilClient::CloneCommand), extraOptions, sourceUrl, fossilFileNative}}, -1); } // check out the cloned repository file into the working copy directory; // by default the latest revision is checked out QStringList args({"open", fossilFileNative}); if (!checkoutBranch.isEmpty()) args << checkoutBranch; command->addJob({fossilClient().vcsBinary(checkoutPath), args}, -1); // set user default to admin user if specified if (!isLocalRepository && !adminUser.isEmpty()) { command->addJob({fossilClient().vcsBinary(checkoutPath), {"user", "default", adminUser, "--user", adminUser}}, -1); } // turn-off autosync if requested if (!isLocalRepository && disableAutosync) { command->addJob({fossilClient().vcsBinary(checkoutPath), {"settings", "autosync", "off"}}, -1); } return command; } void FossilPluginPrivate::changed(const QVariant &v) { switch (v.typeId()) { case QMetaType::QString: emit repositoryChanged(FilePath::fromVariant(v)); break; case QMetaType::QStringList: emit filesChanged(v.toStringList()); break; default: break; } } RevertDialog::RevertDialog(const QString &title, QWidget *parent) : QDialog(parent) { resize(600, 0); setWindowTitle(title); auto *groupBox = new QGroupBox(Tr::tr("Specify a revision other than the default?")); groupBox->setCheckable(true); groupBox->setChecked(false); groupBox->setToolTip(Tr::tr("Checkout revision, can also be a branch or a tag name.")); m_revisionLineEdit = new QLineEdit; auto buttonBox = new QDialogButtonBox; buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); using namespace Layouting; Form { Tr::tr("Revision"), m_revisionLineEdit, br, }.attachTo(groupBox); Column { groupBox, buttonBox, }.attachTo(this); } #ifdef WITH_TESTS class FossilTests final : public QObject { Q_OBJECT private slots: void testDiffFileResolving_data(); void testDiffFileResolving(); void testLogResolving(); }; void FossilTests::testDiffFileResolving_data() { QTest::addColumn("header"); QTest::addColumn("fileName"); QTest::newRow("New") << QByteArray( "ADDED src/plugins/fossil/fossilclient.cpp\n" "Index: src/plugins/fossil/fossilclient.cpp\n" "==================================================================\n" "--- src/plugins/fossil/fossilclient.cpp\n" "+++ src/plugins/fossil/fossilclient.cpp\n" "@@ -0,0 +1,295 @@\n" ) << QByteArray("src/plugins/fossil/fossilclient.cpp"); QTest::newRow("Deleted") << QByteArray( "DELETED src/plugins/fossil/fossilclient.cpp\n" "Index: src/plugins/fossil/fossilclient.cpp\n" "==================================================================\n" "--- src/plugins/fossil/fossilclient.cpp\n" "+++ src/plugins/fossil/fossilclient.cpp\n" "@@ -1,266 +0,0 @@\n" ) << QByteArray("src/plugins/fossil/fossilclient.cpp"); QTest::newRow("Modified") << QByteArray( "Index: src/plugins/fossil/fossilclient.cpp\n" "==================================================================\n" "--- src/plugins/fossil/fossilclient.cpp\n" "+++ src/plugins/fossil/fossilclient.cpp\n" "@@ -112,22 +112,37 @@\n" ) << QByteArray("src/plugins/fossil/fossilclient.cpp"); } void FossilTests::testDiffFileResolving() { VcsBaseEditorWidget::testDiffFileResolving(dd->diffFactory); } void FossilTests::testLogResolving() { QByteArray data( "=== 2014-03-08 ===\n" "22:14:02 [ac6d1129b8] Change scaling algorithm. (user: ninja tags: ninja-fixes-5.1)\n" " EDITED src/core/scaler.cpp\n" "20:23:51 [56d6917c3b] *BRANCH* Add width option (conditional). (user: ninja tags: ninja-fixes-5.1)\n" " EDITED src/core/scaler.cpp\n" " EDITED src/core/scaler.h\n" ); VcsBaseEditorWidget::testLogResolving(dd->fileLogFactory, data, "ac6d1129b8", "56d6917c3b"); } #endif class FossilPlugin final : public ExtensionSystem::IPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Fossil.json") ~FossilPlugin() final { delete dd; dd = nullptr; } void initialize() final { dd = new FossilPluginPrivate; #ifdef WITH_TESTS addTest(); #endif } void extensionsInitialized() final { dd->extensionsInitialized(); } }; } // namespace Fossil::Internal #include "fossilplugin.moc"