From e7fcc6eddde1976f4d5053458fbd87277c751ce4 Mon Sep 17 00:00:00 2001 From: ShadelessFox Date: Tue, 3 Nov 2020 13:53:09 +0300 Subject: [PATCH] #9911 Refactor local client selection for tasks --- .../ui/nativetool/NativeToolConfigPanel.java | 11 ++++--- .../ui/nativetool/NativeToolWizardDialog.java | 8 +++-- .../internal/TaskNativeUIMessages.java | 7 +++++ .../internal/TaskNativeUIMessages.properties | 7 +++++ .../TaskNativeUIMessages_ru.properties | 7 +++++ .../AbstractImportExportSettings.java | 7 +++-- .../dialogs/connection/ClientHomesPanel.java | 7 ++++- .../connection/ClientHomesSelector.java | 31 ++++++++----------- .../ui/internal/UIConnectionMessages.java | 2 ++ .../internal/UIConnectionMessages.properties | 2 ++ .../UIConnectionMessages_ru.properties | 2 ++ 11 files changed, 62 insertions(+), 29 deletions(-) diff --git a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/NativeToolConfigPanel.java b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/NativeToolConfigPanel.java index 3976989a72..94b9e75134 100644 --- a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/NativeToolConfigPanel.java +++ b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/NativeToolConfigPanel.java @@ -32,6 +32,7 @@ import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.tasks.nativetool.AbstractImportExportSettings; import org.jkiss.dbeaver.tasks.nativetool.AbstractNativeToolSettings; import org.jkiss.dbeaver.tasks.ui.DBTTaskConfigPanel; +import org.jkiss.dbeaver.tasks.ui.nativetool.internal.TaskNativeUIMessages; import org.jkiss.dbeaver.tasks.ui.wizard.TaskConfigurationWizard; import org.jkiss.dbeaver.ui.UIUtils; import org.jkiss.dbeaver.ui.dialogs.connection.ClientHomesSelector; @@ -73,7 +74,7 @@ public abstract class NativeToolConfigPanel imple public void createControl(Composite parent, TaskConfigurationWizard wizard, Runnable propertyChangeListener) { ieWizard = (AbstractNativeToolWizard) wizard; { - Group databasesGroup = UIUtils.createControlGroup(parent, "Select target database", 1, GridData.FILL_BOTH, 0); + Group databasesGroup = UIUtils.createControlGroup(parent, TaskNativeUIMessages.tools_wizard_database_group_title, 1, GridData.FILL_BOTH, 0); selectorPanel = new DatabaseObjectsSelectorPanel( databasesGroup, @@ -130,8 +131,8 @@ public abstract class NativeToolConfigPanel imple } { - Composite clientGroup = UIUtils.createControlGroup((Composite) parent, "Client files", 1, GridData.FILL_HORIZONTAL, 0); - homesSelector = new ClientHomesSelector(clientGroup, "Native client"); + Composite clientGroup = UIUtils.createControlGroup((Composite) parent, TaskNativeUIMessages.tools_wizard_client_group_title, 1, GridData.FILL_HORIZONTAL, 0); + homesSelector = new ClientHomesSelector(clientGroup, TaskNativeUIMessages.tools_wizard_client_group_client); homesSelector.addSelectionChangedListener(event -> propertyChangeListener.run()); homesSelector.getPanel().setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); } @@ -194,10 +195,10 @@ public abstract class NativeToolConfigPanel imple @Override public String getErrorMessage() { if (selectedObject == null) { - return "No database object selected"; + return TaskNativeUIMessages.tools_wizard_error_no_database_object_selected; } if (CommonUtils.isEmpty(homesSelector.getSelectedHome())) { - return "No native client selected"; + return TaskNativeUIMessages.tools_wizard_error_no_native_client_selected; } return null; } diff --git a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/NativeToolWizardDialog.java b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/NativeToolWizardDialog.java index 0373cc805d..fc09bbb0e1 100644 --- a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/NativeToolWizardDialog.java +++ b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/NativeToolWizardDialog.java @@ -20,6 +20,7 @@ package org.jkiss.dbeaver.tasks.ui.nativetool; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; @@ -29,6 +30,7 @@ import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IWorkbenchWindow; import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.tasks.ui.nativetool.internal.TaskNativeUIMessages; import org.jkiss.dbeaver.tasks.ui.wizard.TaskConfigurationWizard; import org.jkiss.dbeaver.tasks.ui.wizard.TaskConfigurationWizardDialog; import org.jkiss.dbeaver.ui.dialogs.BaseDialog; @@ -61,7 +63,7 @@ public class NativeToolWizardDialog extends TaskConfigurationWizardDialog { if (nativeClientRequired) { parent.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); - Button configButton = createButton(parent, CLIENT_CONFIG_ID, "Client ...", false); + Button configButton = createButton(parent, CLIENT_CONFIG_ID, TaskNativeUIMessages.tools_wizard_client_button, false); //configButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING)); Label spacer = new Label(parent, SWT.NONE); @@ -100,7 +102,7 @@ public class NativeToolWizardDialog extends TaskConfigurationWizardDialog { private ClientHomesSelector homesSelector; public NativeClientConfigDialog(Shell parentShell, DBPDataSourceContainer dataSource) { - super(parentShell, "Configure local client for " + dataSource.getName(), dataSource.getDriver().getIcon()); + super(parentShell, NLS.bind(TaskNativeUIMessages.tools_wizard_client_dialog_title, dataSource.getName()), dataSource.getDriver().getIcon()); this.dataSource = dataSource; } @@ -108,7 +110,7 @@ public class NativeToolWizardDialog extends TaskConfigurationWizardDialog { protected Composite createDialogArea(Composite parent) { Composite dialogArea = super.createDialogArea(parent); - homesSelector = new ClientHomesSelector(dialogArea, "Native client"); + homesSelector = new ClientHomesSelector(dialogArea, TaskNativeUIMessages.tools_wizard_client_group_client); homesSelector.populateHomes(dataSource.getDriver(), dataSource.getConnectionConfiguration().getClientHomeId(), true); homesSelector.getPanel().setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); diff --git a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages.java b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages.java index 7d77e89410..34305af0d3 100644 --- a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages.java +++ b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages.java @@ -25,6 +25,13 @@ public class TaskNativeUIMessages extends NLS { public static String tools_script_execute_wizard_task_completed; public static String tools_wizard_error_task_error_message; public static String tools_wizard_error_task_error_title; + public static String tools_wizard_error_no_database_object_selected; + public static String tools_wizard_error_no_native_client_selected; + public static String tools_wizard_database_group_title; + public static String tools_wizard_client_group_title; + public static String tools_wizard_client_group_client; + public static String tools_wizard_client_button; + public static String tools_wizard_client_dialog_title; public static String tools_wizard_error_task_canceled; public static String tools_wizard_log_process_exit_code; public static String tools_wizard_log_io_error; diff --git a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages.properties b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages.properties index 386536fc3e..a0af86e6f4 100644 --- a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages.properties +++ b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages.properties @@ -2,6 +2,13 @@ tools_script_execute_wizard_task_completed = {0} ({1}) completed tools_wizard_error_task_canceled = {0} "{1}" canceled tools_wizard_error_task_error_message = Cannot perform tools_wizard_error_task_error_title = {0} error +tools_wizard_error_no_database_object_selected = No database object selected +tools_wizard_error_no_native_client_selected = No local client selected +tools_wizard_database_group_title = Target database object +tools_wizard_client_group_title = Local client configuration +tools_wizard_client_group_client = Local client +tools_wizard_client_button = Local Client ... +tools_wizard_client_dialog_title = Local client configuration for ''{0}'' tools_wizard_log_io_error = IO Error: {0} tools_wizard_log_process_exit_code = Process exit code: {0} tools_wizard_message_client_home_not_found = Client home "{0}" not found diff --git a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages_ru.properties b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages_ru.properties index 920e6e1a7a..f7b00a93f0 100644 --- a/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages_ru.properties +++ b/plugins/org.jkiss.dbeaver.tasks.native.ui/src/org/jkiss/dbeaver/tasks/ui/nativetool/internal/TaskNativeUIMessages_ru.properties @@ -6,6 +6,13 @@ tools_wizard_log_io_error=\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u0432\u043 tools_wizard_message_client_home_not_found=\u041A\u043B\u0438\u0435\u043D\u0442 "{0}" \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D tools_wizard_message_no_client_home=\u041A\u043B\u0438\u0435\u043D\u0442 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F \u043D\u0435 \u0437\u0430\u0434\u0430\u043D tools_wizard_error_task_error_title={0} - \u043E\u0448\u0438\u0431\u043A\u0430. +tools_wizard_error_no_database_object_selected = \u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D \u043E\u0431\u044A\u0435\u043A\u0442 \u0411\u0414 +tools_wizard_error_no_native_client_selected = \u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u043B\u0438\u0435\u043D\u0442 +tools_wizard_database_group_title = \u041E\u0431\u044A\u0435\u043A\u0442 \u0411\u0414 +tools_wizard_client_group_title = \u041A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u0430 +tools_wizard_client_group_client = \u041B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u043B\u0438\u0435\u043D\u0442 +tools_wizard_client_button = \u041B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u041A\u043B\u0438\u0435\u043D\u0442 ... +tools_wizard_client_dialog_title = \u041A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u0430 \u0434\u043B\u044F ''{0}'' tools_wizard_page_log_task_finished={0} \u0437\u0430\u043A\u043E\u043D\u0447\u0435\u043D \u0432 {1} tools_wizard_page_log_task_log_reader={0}. \u0427\u0442\u0435\u043D\u0438\u0435 \u043B\u043E\u0433\u043E\u0432. tools_wizard_page_log_task_progress={0} \u0432 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0435 diff --git a/plugins/org.jkiss.dbeaver.tasks.native/src/org/jkiss/dbeaver/tasks/nativetool/AbstractImportExportSettings.java b/plugins/org.jkiss.dbeaver.tasks.native/src/org/jkiss/dbeaver/tasks/nativetool/AbstractImportExportSettings.java index 949090ace8..2214433e89 100644 --- a/plugins/org.jkiss.dbeaver.tasks.native/src/org/jkiss/dbeaver/tasks/nativetool/AbstractImportExportSettings.java +++ b/plugins/org.jkiss.dbeaver.tasks.native/src/org/jkiss/dbeaver/tasks/nativetool/AbstractImportExportSettings.java @@ -28,7 +28,7 @@ import java.io.File; public abstract class AbstractImportExportSettings extends AbstractNativeToolSettings { - private File outputFolder = new File(RuntimeUtils.getUserHomeDir().getAbsolutePath()); + private File outputFolder; private String outputFilePattern; public File getOutputFolder() { @@ -58,7 +58,10 @@ public abstract class AbstractImportExportSettings 0) { + selectHome((HomeInfo) homesTable.getItem(selIndex - 1).getData()); + homesTable.setSelection(selIndex - 1); + } else { + selectHome(null); + } } } } diff --git a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/dialogs/connection/ClientHomesSelector.java b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/dialogs/connection/ClientHomesSelector.java index 5f9807b0af..48b3aabdda 100644 --- a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/dialogs/connection/ClientHomesSelector.java +++ b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/dialogs/connection/ClientHomesSelector.java @@ -53,6 +53,7 @@ public class ClientHomesSelector implements ISelectionProvider { private DBPDriver driver; private List homeIds = new ArrayList<>(); private String currentHomeId; + private int currentHomeIndex; private final Map listeners = new IdentityHashMap<>(); @@ -70,7 +71,7 @@ public class ClientHomesSelector implements ISelectionProvider { selectorPanel = createComposite ? UIUtils.createComposite(parent, 2) : parent; Label controlLabel = UIUtils.createControlLabel(selectorPanel, title); - controlLabel.setToolTipText("Local client configuration is needed for some administrative tasks like database dump/restore."); + controlLabel.setToolTipText(UIConnectionMessages.controls_client_home_selector_tip); //label.setFont(UIUtils.makeBoldFont(label.getFont())); homesCombo = new Combo(selectorPanel, SWT.READ_ONLY); //directoryDialog = new DirectoryDialog(selectorContainer.getShell(), SWT.OPEN); @@ -83,9 +84,11 @@ public class ClientHomesSelector implements ISelectionProvider { public void widgetSelected(SelectionEvent e) { if (homesCombo.getSelectionIndex() == homesCombo.getItemCount() - 1) { + homesCombo.select(currentHomeIndex); manageHomes(); } else { currentHomeId = homeIds.get(homesCombo.getSelectionIndex()); + currentHomeIndex = homesCombo.getSelectionIndex(); } displayClientVersion(); handleHomeChange(); @@ -108,14 +111,11 @@ public class ClientHomesSelector implements ISelectionProvider { if (newHomeId != null) { currentHomeId = newHomeId; } - populateHomes(driver, currentHomeId, false); + populateHomes(driver, currentHomeId, true); } public void populateHomes(DBPDriver driver, String currentHome, boolean selectDefault) { - if (this.driver == driver) { - return; - } this.driver = driver; this.currentHomeId = currentHome; @@ -135,15 +135,9 @@ public class ClientHomesSelector implements ISelectionProvider { DBPNativeClientLocationManager clientManager = driver.getNativeClientManager(); if (clientManager != null) { for (DBPNativeClientLocation location : clientManager.findLocalClientLocations()) { - if (!homes.containsKey(location.getName())) { - homes.put(location.getName(), location); - } + homes.putIfAbsent(location.getName(), location); } } - if (!CommonUtils.isEmpty(currentHome) && !homes.containsKey(currentHome)) { - homes.put(currentHome, new LocalNativeClientLocation(currentHome, currentHome)); - } - return Status.OK_STATUS; } }; @@ -151,8 +145,6 @@ public class ClientHomesSelector implements ISelectionProvider { @Override public void done(IJobChangeEvent event) { UIUtils.syncExec(() -> { - homesCombo.add(""); - homeIds.add(null); for (DBPNativeClientLocation location : homes.values()) { homesCombo.add(location.getDisplayName()); homeIds.add(location.getName()); @@ -160,10 +152,13 @@ public class ClientHomesSelector implements ISelectionProvider { homesCombo.select(homesCombo.getItemCount() - 1); } } - if (selectDefault && homesCombo.getItemCount() > 1 && homesCombo.getSelectionIndex() == -1) { - // Select first - homesCombo.select(1); - currentHomeId = homesCombo.getItem(1); + if (homesCombo.getItemCount() == 0) { + homesCombo.add(UIConnectionMessages.controls_client_home_selector_missing); + homeIds.add(null); + } + if (selectDefault && homesCombo.getSelectionIndex() == -1) { + homesCombo.select(0); + currentHomeId = homeIds.get(0); } homesCombo.add(UIConnectionMessages.controls_client_home_selector_browse); diff --git a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.java b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.java index ce73d910cf..e5c1bee2bd 100644 --- a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.java +++ b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.java @@ -35,6 +35,8 @@ public class UIConnectionMessages extends NLS { public static String dialog_connection_wizard_final_checkbox_save_password_locally; public static String controls_client_home_selector_browse; + public static String controls_client_home_selector_missing; + public static String controls_client_home_selector_tip; public static String controls_client_homes_panel_button_add_home; public static String controls_client_homes_panel_button_remove_home; public static String controls_client_homes_panel_confirm_remove_home_text; diff --git a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.properties b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.properties index 7980101f18..a15a460cf5 100644 --- a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.properties +++ b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages.properties @@ -12,6 +12,8 @@ dialog_connection_edit_driver_button = Edit Driver Settings dialog_connection_wizard_final_checkbox_save_password_locally = Save password locally controls_client_home_selector_browse = Browse ... +controls_client_home_selector_missing = +controls_client_home_selector_tip = Local client is needed for some administrative tasks like database dump/restore. controls_client_homes_panel_button_add_home = Add Home controls_client_homes_panel_button_remove_home = Remove Home controls_client_homes_panel_confirm_remove_home_text = Are you sure you want to delete client home "{0}"? diff --git a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages_ru.properties b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages_ru.properties index bc727a09aa..9a99da5584 100644 --- a/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages_ru.properties +++ b/plugins/org.jkiss.dbeaver.ui.editors.connection/src/org/jkiss/dbeaver/ui/internal/UIConnectionMessages_ru.properties @@ -11,6 +11,8 @@ dialog_connection_edit_driver_button = \u041D\u0430\u0441\u0442\u0440\u043E\u043 dialog_connection_wizard_final_checkbox_save_password_locally=\u0421\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E controls_client_home_selector_browse=\u0412\u044B\u0431\u0440\u0430\u0442\u044C ... +controls_client_home_selector_missing=<\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D> +controls_client_home_selector_tip = \u041B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u043B\u0438\u0435\u043D\u0442 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F \u0434\u043B\u044F \u043D\u0435\u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u0438\u0432\u043D\u044B\u0445 \u0437\u0430\u0434\u0430\u0447 \u0432\u0440\u043E\u0434\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u043E\u0433\u043E \u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F/\u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u0411\u0414. dialog_setting_connection_driver_properties_description = \u0421\u0432\u043E\u0439\u0441\u0442\u0432\u0430 \u0434\u0440\u0430\u0439\u0432\u0435\u0440\u0430 dialog_setting_connection_driver_properties_title = \u0421\u0432\u043E\u0439\u0441\u0442\u0432\u0430 \u0434\u0440\u0430\u0439\u0432\u0435\u0440\u0430 -- GitLab