From d99641be401c73eabee379ce8fdf9e57dd85114a Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Thu, 14 Sep 2023 14:39:42 -0700 Subject: [PATCH] ADDED: Git integration support Adds support for project-based git integration, branch support, commit, revert and updates Fixes https://gitlab.com/kicad/code/kicad/issues/10441 --- CMakeLists.txt | 12 + cmake/Findlibgit2.cmake | 38 + common/CMakeLists.txt | 29 + common/advanced_config.cpp | 6 + common/bitmap_info.cpp | 14 + common/dialogs/git/dialog_git_auth.cpp | 96 ++ common/dialogs/git/dialog_git_auth.h | 59 + common/dialogs/git/dialog_git_commit.cpp | 204 +++ common/dialogs/git/dialog_git_commit.h | 62 + common/dialogs/git/dialog_git_progress.cpp | 49 + common/dialogs/git/dialog_git_repository.cpp | 448 ++++++ common/dialogs/git/dialog_git_repository.h | 124 ++ .../git/dialog_git_repository_base.cpp | 172 +++ .../git/dialog_git_repository_base.fbp | 1335 +++++++++++++++++ .../dialogs/git/dialog_git_repository_base.h | 82 + common/dialogs/git/dialog_git_switch.cpp | 288 ++++ common/dialogs/git/dialog_git_switch.h | 79 + common/dialogs/git/panel_git_repos.cpp | 377 +++++ common/dialogs/git/panel_git_repos.h | 59 + common/dialogs/git/panel_git_repos_base.cpp | 167 +++ common/dialogs/git/panel_git_repos_base.h | 73 + .../git/panel_git_repositories_base.fbp | 953 ++++++++++++ common/eda_base_frame.cpp | 11 + common/eda_shape.cpp | 123 ++ common/eda_text.cpp | 61 + common/gestfich.cpp | 50 + common/git/git_add_to_index_handler.cpp | 108 ++ common/git/git_add_to_index_handler.h | 50 + common/git/git_clone_handler.cpp | 82 + common/git/git_clone_handler.h | 56 + common/git/git_commit_handler.cpp | 49 + common/git/git_commit_handler.h | 58 + common/git/git_compare_handler.cpp | 0 common/git/git_compare_handler.h | 0 common/git/git_progress.h | 72 + common/git/git_pull_handler.cpp | 343 +++++ common/git/git_pull_handler.h | 100 ++ common/git/git_push_handler.cpp | 79 + common/git/git_push_handler.h | 57 + common/git/git_remove_from_index_handler.cpp | 98 ++ common/git/git_remove_from_index_handler.h | 50 + common/git/git_remove_vcs_handler.cpp | 0 common/git/git_remove_vcs_handler.h | 0 common/git/git_resolve_conflict_handler.cpp | 39 + common/git/git_resolve_conflict_handler.h | 43 + common/git/git_revert_handler.cpp | 108 ++ common/git/git_revert_handler.h | 54 + common/git/git_switch_branch_handler.cpp | 0 common/git/git_switch_branch_handler.h | 0 common/git/git_sync_handler.cpp | 44 + common/git/git_sync_handler.h | 43 + common/git/kicad_git_blob_reader.h | 97 ++ common/git/kicad_git_common.cpp | 541 +++++++ common/git/kicad_git_common.h | 145 ++ common/git/kicad_git_errors.cpp | 66 + common/git/kicad_git_errors.h | 81 + common/project/project_local_settings.cpp | 12 + common/settings/common_settings.cpp | 55 + common/single_top.cpp | 7 + eeschema/lib_field.cpp | 52 + eeschema/lib_field.h | 4 + eeschema/lib_item.cpp | 2 - eeschema/lib_item.h | 31 +- eeschema/lib_pin.cpp | 117 ++ eeschema/lib_pin.h | 11 +- eeschema/lib_shape.cpp | 27 + eeschema/lib_shape.h | 4 + eeschema/lib_symbol.cpp | 136 ++ eeschema/lib_symbol.h | 13 +- eeschema/lib_text.cpp | 27 + eeschema/lib_text.h | 4 + eeschema/lib_textbox.cpp | 29 + eeschema/lib_textbox.h | 4 + eeschema/sch_bitmap.cpp | 42 + eeschema/sch_bitmap.h | 4 + eeschema/sch_bus_entry.cpp | 39 + eeschema/sch_bus_entry.h | 4 + eeschema/sch_field.cpp | 68 + eeschema/sch_field.h | 4 + eeschema/sch_item.h | 9 + eeschema/sch_junction.cpp | 45 + eeschema/sch_junction.h | 4 + eeschema/sch_label.cpp | 63 + eeschema/sch_label.h | 4 + eeschema/sch_line.cpp | 63 + eeschema/sch_line.h | 4 + eeschema/sch_marker.h | 10 + eeschema/sch_no_connect.cpp | 31 + eeschema/sch_no_connect.h | 4 + eeschema/sch_pin.cpp | 37 + eeschema/sch_pin.h | 4 + eeschema/sch_shape.cpp | 24 + eeschema/sch_shape.h | 4 + eeschema/sch_sheet.cpp | 49 + eeschema/sch_sheet.h | 4 + eeschema/sch_sheet_path.cpp | 10 + eeschema/sch_sheet_pin.cpp | 33 + eeschema/sch_sheet_pin.h | 4 + eeschema/sch_symbol.cpp | 55 + eeschema/sch_symbol.h | 4 + eeschema/sch_text.cpp | 37 + eeschema/sch_text.h | 4 + eeschema/sch_textbox.cpp | 33 + eeschema/sch_textbox.h | 4 + eeschema/symbol_diff_frame.cpp | 211 +++ eeschema/symbol_diff_frame.h | 97 ++ include/advanced_config.h | 5 + include/bitmaps/bitmaps_list.h | 7 + include/board_item.h | 19 + include/eda_item.h | 18 + include/eda_shape.h | 4 + include/eda_text.h | 10 + include/frame_type.h | 4 + include/gestfich.h | 7 + include/pcb_group.h | 4 + include/project/project_local_settings.h | 6 + include/scoped_set_reset.h | 28 + include/settings/common_settings.h | 21 + kicad/kicad.cpp | 5 + kicad/kicad_id.h | 21 + kicad/menubar.cpp | 1 + kicad/project_tree.cpp | 25 + kicad/project_tree.h | 9 + kicad/project_tree_pane.cpp | 978 +++++++++++- kicad/project_tree_pane.h | 89 ++ kicad/tools/kicad_manager_actions.cpp | 8 + kicad/tools/kicad_manager_actions.h | 1 + kicad/tools/kicad_manager_control.cpp | 89 +- kicad/tools/kicad_manager_control.h | 4 + libs/core/include/core/kicad_algo.h | 31 + libs/kiplatform/CMakeLists.txt | 8 + libs/kiplatform/gtk/secrets.cpp | 84 ++ libs/kiplatform/include/kiplatform/secrets.h | 37 + libs/kiplatform/msw/secrets.cpp | 60 + libs/kiplatform/osx/secrets.mm | 75 + pcbnew/CMakeLists.txt | 6 + pcbnew/board.cpp | 90 +- pcbnew/board.h | 16 +- pcbnew/footprint.cpp | 71 + pcbnew/footprint.h | 4 + pcbnew/git/kigit_pcb_merge.cpp | 180 +++ pcbnew/git/kigit_pcb_merge.h | 85 ++ pcbnew/netinfo.h | 10 + pcbnew/pad.cpp | 180 +++ pcbnew/pad.h | 4 + pcbnew/pcb_bitmap.cpp | 66 + pcbnew/pcb_bitmap.h | 4 + pcbnew/pcb_dimension.cpp | 95 ++ pcbnew/pcb_dimension.h | 4 + pcbnew/pcb_field.cpp | 37 +- pcbnew/pcb_field.h | 4 + pcbnew/pcb_group.cpp | 47 + pcbnew/pcb_marker.h | 10 + pcbnew/pcb_shape.cpp | 62 + pcbnew/pcb_shape.h | 4 + pcbnew/pcb_target.cpp | 39 + pcbnew/pcb_target.h | 4 + pcbnew/pcb_text.cpp | 22 + pcbnew/pcb_text.h | 4 + pcbnew/pcb_textbox.cpp | 30 + pcbnew/pcb_textbox.h | 4 + pcbnew/pcb_track.cpp | 136 ++ pcbnew/pcb_track.h | 12 + .../scripting/pcbnew_action_plugins.cpp | 4 +- pcbnew/teardrop/teardrop_parameters.h | 18 + pcbnew/zone.cpp | 134 ++ pcbnew/zone.h | 4 + resources/bitmaps_png/CMakeLists.txt | 7 + resources/bitmaps_png/png/git_add_16.png | Bin 0 -> 120 bytes resources/bitmaps_png/png/git_add_dark_16.png | Bin 0 -> 116 bytes .../bitmaps_png/png/git_changed_ahead_16.png | Bin 0 -> 412 bytes .../png/git_changed_ahead_dark_16.png | Bin 0 -> 417 bytes resources/bitmaps_png/png/git_conflict_16.png | Bin 0 -> 303 bytes .../bitmaps_png/png/git_conflict_dark_16.png | Bin 0 -> 312 bytes resources/bitmaps_png/png/git_delete_16.png | Bin 0 -> 407 bytes .../bitmaps_png/png/git_delete_dark_16.png | Bin 0 -> 385 bytes .../bitmaps_png/png/git_good_check_16.png | Bin 0 -> 236 bytes .../png/git_good_check_dark_16.png | Bin 0 -> 236 bytes resources/bitmaps_png/png/git_modified_16.png | Bin 0 -> 348 bytes .../bitmaps_png/png/git_modified_dark_16.png | Bin 0 -> 336 bytes .../bitmaps_png/png/git_out_of_date_16.png | Bin 0 -> 405 bytes .../png/git_out_of_date_dark_16.png | Bin 0 -> 405 bytes .../bitmaps_png/sources/dark/git_add.svg | 82 + .../sources/dark/git_changed_ahead.svg | 89 ++ .../bitmaps_png/sources/dark/git_conflict.svg | 99 ++ .../bitmaps_png/sources/dark/git_delete.svg | 76 + .../sources/dark/git_good_check.svg | 84 ++ .../bitmaps_png/sources/dark/git_modified.svg | 77 + .../sources/dark/git_out_of_date.svg | 87 ++ .../bitmaps_png/sources/light/git_add.svg | 82 + .../sources/light/git_changed_ahead.svg | 89 ++ .../sources/light/git_conflict.svg | 99 ++ .../bitmaps_png/sources/light/git_delete.svg | 11 + .../sources/light/git_good_check.svg | 84 ++ .../sources/light/git_modified.svg | 77 + .../sources/light/git_out_of_date.svg | 87 ++ 196 files changed, 13121 insertions(+), 38 deletions(-) create mode 100644 cmake/Findlibgit2.cmake create mode 100644 common/dialogs/git/dialog_git_auth.cpp create mode 100644 common/dialogs/git/dialog_git_auth.h create mode 100644 common/dialogs/git/dialog_git_commit.cpp create mode 100644 common/dialogs/git/dialog_git_commit.h create mode 100644 common/dialogs/git/dialog_git_progress.cpp create mode 100644 common/dialogs/git/dialog_git_repository.cpp create mode 100644 common/dialogs/git/dialog_git_repository.h create mode 100644 common/dialogs/git/dialog_git_repository_base.cpp create mode 100644 common/dialogs/git/dialog_git_repository_base.fbp create mode 100644 common/dialogs/git/dialog_git_repository_base.h create mode 100644 common/dialogs/git/dialog_git_switch.cpp create mode 100644 common/dialogs/git/dialog_git_switch.h create mode 100644 common/dialogs/git/panel_git_repos.cpp create mode 100644 common/dialogs/git/panel_git_repos.h create mode 100644 common/dialogs/git/panel_git_repos_base.cpp create mode 100644 common/dialogs/git/panel_git_repos_base.h create mode 100644 common/dialogs/git/panel_git_repositories_base.fbp create mode 100644 common/git/git_add_to_index_handler.cpp create mode 100644 common/git/git_add_to_index_handler.h create mode 100644 common/git/git_clone_handler.cpp create mode 100644 common/git/git_clone_handler.h create mode 100644 common/git/git_commit_handler.cpp create mode 100644 common/git/git_commit_handler.h create mode 100644 common/git/git_compare_handler.cpp create mode 100644 common/git/git_compare_handler.h create mode 100644 common/git/git_progress.h create mode 100644 common/git/git_pull_handler.cpp create mode 100644 common/git/git_pull_handler.h create mode 100644 common/git/git_push_handler.cpp create mode 100644 common/git/git_push_handler.h create mode 100644 common/git/git_remove_from_index_handler.cpp create mode 100644 common/git/git_remove_from_index_handler.h create mode 100644 common/git/git_remove_vcs_handler.cpp create mode 100644 common/git/git_remove_vcs_handler.h create mode 100644 common/git/git_resolve_conflict_handler.cpp create mode 100644 common/git/git_resolve_conflict_handler.h create mode 100644 common/git/git_revert_handler.cpp create mode 100644 common/git/git_revert_handler.h create mode 100644 common/git/git_switch_branch_handler.cpp create mode 100644 common/git/git_switch_branch_handler.h create mode 100644 common/git/git_sync_handler.cpp create mode 100644 common/git/git_sync_handler.h create mode 100644 common/git/kicad_git_blob_reader.h create mode 100644 common/git/kicad_git_common.cpp create mode 100644 common/git/kicad_git_common.h create mode 100644 common/git/kicad_git_errors.cpp create mode 100644 common/git/kicad_git_errors.h create mode 100644 eeschema/symbol_diff_frame.cpp create mode 100644 eeschema/symbol_diff_frame.h create mode 100644 libs/kiplatform/gtk/secrets.cpp create mode 100644 libs/kiplatform/include/kiplatform/secrets.h create mode 100644 libs/kiplatform/msw/secrets.cpp create mode 100644 libs/kiplatform/osx/secrets.mm create mode 100644 pcbnew/git/kigit_pcb_merge.cpp create mode 100644 pcbnew/git/kigit_pcb_merge.h create mode 100644 resources/bitmaps_png/png/git_add_16.png create mode 100644 resources/bitmaps_png/png/git_add_dark_16.png create mode 100644 resources/bitmaps_png/png/git_changed_ahead_16.png create mode 100644 resources/bitmaps_png/png/git_changed_ahead_dark_16.png create mode 100644 resources/bitmaps_png/png/git_conflict_16.png create mode 100644 resources/bitmaps_png/png/git_conflict_dark_16.png create mode 100644 resources/bitmaps_png/png/git_delete_16.png create mode 100644 resources/bitmaps_png/png/git_delete_dark_16.png create mode 100644 resources/bitmaps_png/png/git_good_check_16.png create mode 100644 resources/bitmaps_png/png/git_good_check_dark_16.png create mode 100644 resources/bitmaps_png/png/git_modified_16.png create mode 100644 resources/bitmaps_png/png/git_modified_dark_16.png create mode 100644 resources/bitmaps_png/png/git_out_of_date_16.png create mode 100644 resources/bitmaps_png/png/git_out_of_date_dark_16.png create mode 100644 resources/bitmaps_png/sources/dark/git_add.svg create mode 100644 resources/bitmaps_png/sources/dark/git_changed_ahead.svg create mode 100644 resources/bitmaps_png/sources/dark/git_conflict.svg create mode 100644 resources/bitmaps_png/sources/dark/git_delete.svg create mode 100644 resources/bitmaps_png/sources/dark/git_good_check.svg create mode 100644 resources/bitmaps_png/sources/dark/git_modified.svg create mode 100644 resources/bitmaps_png/sources/dark/git_out_of_date.svg create mode 100644 resources/bitmaps_png/sources/light/git_add.svg create mode 100644 resources/bitmaps_png/sources/light/git_changed_ahead.svg create mode 100644 resources/bitmaps_png/sources/light/git_conflict.svg create mode 100644 resources/bitmaps_png/sources/light/git_delete.svg create mode 100644 resources/bitmaps_png/sources/light/git_good_check.svg create mode 100644 resources/bitmaps_png/sources/light/git_modified.svg create mode 100644 resources/bitmaps_png/sources/light/git_out_of_date.svg diff --git a/CMakeLists.txt b/CMakeLists.txt index ec62cbf753..ff0e180af9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -746,6 +746,12 @@ find_package(ZLIB REQUIRED) # find_package( CURL REQUIRED ) +# +# Find libssl, required +# +find_package( OpenSSL REQUIRED ) +include_directories( SYSTEM ${OPENSSL_INCLUDE_DIR} ) + # # Find Cairo library, required # @@ -755,6 +761,12 @@ include_directories( SYSTEM ${CAIRO_INCLUDE_DIR} ) find_package( Pixman 0.30 REQUIRED ) include_directories( SYSTEM ${PIXMAN_INCLUDE_DIR} ) +# Find libgit2, required +find_package( libgit2 REQUIRED ) + +# Set include directories for libgit +include_directories(${LIBGIT2_INCLUDE_DIRS}) + # # Find Boost headers and libraries, required. set( BOOST_REQUESTED_COMPONENTS locale ) # locale is required by nanoodbc/database libraries diff --git a/cmake/Findlibgit2.cmake b/cmake/Findlibgit2.cmake new file mode 100644 index 0000000000..5a1b1ef2bc --- /dev/null +++ b/cmake/Findlibgit2.cmake @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2014 Dan Leinir Turthra Jensen >& aBi aBitmapInfoCache[BITMAPS::e_48].emplace_back( BITMAPS::e_48, wxT( "e_48_16.png" ), 16, wxT( "light" ) ); aBitmapInfoCache[BITMAPS::e_96].emplace_back( BITMAPS::e_96, wxT( "e_96_16.png" ), 16, wxT( "light" ) ); aBitmapInfoCache[BITMAPS::e_192].emplace_back( BITMAPS::e_192, wxT( "e_192_16.png" ), 16, wxT( "light" ) ); + aBitmapInfoCache[BITMAPS::git_add].emplace_back( BITMAPS::git_add, wxT( "git_add_16.png" ), 16, wxT( "light" ) ); + aBitmapInfoCache[BITMAPS::git_changed_ahead].emplace_back( BITMAPS::git_changed_ahead, wxT( "git_changed_ahead_16.png" ), 16, wxT( "light" ) ); + aBitmapInfoCache[BITMAPS::git_conflict].emplace_back( BITMAPS::git_conflict, wxT( "git_conflict_16.png" ), 16, wxT( "light" ) ); + aBitmapInfoCache[BITMAPS::git_delete].emplace_back( BITMAPS::git_delete, wxT( "git_delete_16.png" ), 16, wxT( "light" ) ); + aBitmapInfoCache[BITMAPS::git_good_check].emplace_back( BITMAPS::git_good_check, wxT( "git_good_check_16.png" ), 16, wxT( "light" ) ); + aBitmapInfoCache[BITMAPS::git_modified].emplace_back( BITMAPS::git_modified, wxT( "git_modified_16.png" ), 16, wxT( "light" ) ); + aBitmapInfoCache[BITMAPS::git_out_of_date].emplace_back( BITMAPS::git_out_of_date, wxT( "git_out_of_date_16.png" ), 16, wxT( "light" ) ); aBitmapInfoCache[BITMAPS::icon_bitmap2component_16].emplace_back( BITMAPS::icon_bitmap2component_16, wxT( "icon_bitmap2component_16_16.png" ), 16, wxT( "light" ) ); aBitmapInfoCache[BITMAPS::icon_eeschema_16].emplace_back( BITMAPS::icon_eeschema_16, wxT( "icon_eeschema_16_16.png" ), 16, wxT( "light" ) ); aBitmapInfoCache[BITMAPS::icon_gerbview_16].emplace_back( BITMAPS::icon_gerbview_16, wxT( "icon_gerbview_16_16.png" ), 16, wxT( "light" ) ); @@ -101,6 +108,13 @@ void BuildBitmapInfo( std::unordered_map>& aBi aBitmapInfoCache[BITMAPS::visibility].emplace_back( BITMAPS::visibility, wxT( "visibility_16.png" ), 16, wxT( "light" ) ); aBitmapInfoCache[BITMAPS::visibility_off].emplace_back( BITMAPS::visibility_off, wxT( "visibility_off_16.png" ), 16, wxT( "light" ) ); aBitmapInfoCache[BITMAPS::www].emplace_back( BITMAPS::www, wxT( "www_16.png" ), 16, wxT( "light" ) ); + aBitmapInfoCache[BITMAPS::git_add].emplace_back( BITMAPS::git_add, wxT( "git_add_dark_16.png" ), 16, wxT( "dark" ) ); + aBitmapInfoCache[BITMAPS::git_changed_ahead].emplace_back( BITMAPS::git_changed_ahead, wxT( "git_changed_ahead_dark_16.png" ), 16, wxT( "dark" ) ); + aBitmapInfoCache[BITMAPS::git_conflict].emplace_back( BITMAPS::git_conflict, wxT( "git_conflict_dark_16.png" ), 16, wxT( "dark" ) ); + aBitmapInfoCache[BITMAPS::git_delete].emplace_back( BITMAPS::git_delete, wxT( "git_delete_dark_16.png" ), 16, wxT( "dark" ) ); + aBitmapInfoCache[BITMAPS::git_good_check].emplace_back( BITMAPS::git_good_check, wxT( "git_good_check_dark_16.png" ), 16, wxT( "dark" ) ); + aBitmapInfoCache[BITMAPS::git_modified].emplace_back( BITMAPS::git_modified, wxT( "git_modified_dark_16.png" ), 16, wxT( "dark" ) ); + aBitmapInfoCache[BITMAPS::git_out_of_date].emplace_back( BITMAPS::git_out_of_date, wxT( "git_out_of_date_dark_16.png" ), 16, wxT( "dark" ) ); aBitmapInfoCache[BITMAPS::icon_bitmap2component_16].emplace_back( BITMAPS::icon_bitmap2component_16, wxT( "icon_bitmap2component_16_dark_16.png" ), 16, wxT( "dark" ) ); aBitmapInfoCache[BITMAPS::icon_eeschema_16].emplace_back( BITMAPS::icon_eeschema_16, wxT( "icon_eeschema_16_dark_16.png" ), 16, wxT( "dark" ) ); aBitmapInfoCache[BITMAPS::icon_gerbview_16].emplace_back( BITMAPS::icon_gerbview_16, wxT( "icon_gerbview_16_dark_16.png" ), 16, wxT( "dark" ) ); diff --git a/common/dialogs/git/dialog_git_auth.cpp b/common/dialogs/git/dialog_git_auth.cpp new file mode 100644 index 0000000000..18fd2ed32f --- /dev/null +++ b/common/dialogs/git/dialog_git_auth.cpp @@ -0,0 +1,96 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "dialog_git_auth.h" + +DIALOG_GIT_AUTH::DIALOG_GIT_AUTH(wxWindow* parent) + : DIALOG_SHIM(parent, wxID_ANY, _("Connection"), wxDefaultPosition, wxSize(400, 300)) +{ + CreateControls(); + Centre(); +} + +DIALOG_GIT_AUTH::~DIALOG_GIT_AUTH() +{ +} + +void DIALOG_GIT_AUTH::CreateControls() +{ + wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); + this->SetSizer(mainSizer); + + m_NameTextCtrl = new wxTextCtrl(this, wxID_ANY); + m_UrlTextCtrl = new wxTextCtrl(this, wxID_ANY); + m_AuthChoice = new wxChoice(this, wxID_ANY); + m_AuthChoice->Append(_("Basic")); + m_AuthChoice->Append(_("SSH")); + m_UserNameTextCtrl = new wxTextCtrl(this, wxID_ANY); + m_PasswordTextCtrl = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD); + m_PublicKeyPicker = new wxFilePickerCtrl(this, wxID_ANY); + m_PublicKeyPicker->Hide(); + + m_TestButton = new wxButton(this, wxID_ANY, _("Test")); + m_OkButton = new wxButton(this, wxID_OK, _("OK")); + m_CancelButton = new wxButton(this, wxID_CANCEL, _("Cancel")); + + mainSizer->Add(new wxStaticText(this, wxID_ANY, _("Name")), 0, wxALL, 10); + mainSizer->Add(m_NameTextCtrl, 0, wxEXPAND | wxALL, 10); + mainSizer->Add(new wxStaticText(this, wxID_ANY, _("Url")), 0, wxALL, 10); + mainSizer->Add(m_UrlTextCtrl, 0, wxEXPAND | wxALL, 10); + mainSizer->Add(new wxStaticText(this, wxID_ANY, _("Authentication")), 0, wxALL, 10); + mainSizer->Add(m_AuthChoice, 0, wxEXPAND | wxALL, 10); + mainSizer->Add(new wxStaticText(this, wxID_ANY, _("User Name")), 0, wxALL, 10); + mainSizer->Add(m_UserNameTextCtrl, 0, wxEXPAND | wxALL, 10); + mainSizer->Add(new wxStaticText(this, wxID_ANY, _("Password")), 0, wxALL, 10); + mainSizer->Add(m_PasswordTextCtrl, 0, wxEXPAND | wxALL, 10); + mainSizer->Add(m_PublicKeyPicker, 0, wxEXPAND | wxALL, 10); + + + wxBoxSizer* buttonSizer = new wxBoxSizer(wxHORIZONTAL); + buttonSizer->Add(m_TestButton, 1, wxALL, 10); + buttonSizer->Add(m_OkButton, 1, wxALL, 10); + buttonSizer->Add(m_CancelButton, 1, wxALL, 10); + mainSizer->Add(buttonSizer, 0, wxALIGN_RIGHT); + + mainSizer->Layout(); + + // Bind event for authentication method choice change + m_AuthChoice->Bind(wxEVT_CHOICE, &DIALOG_GIT_AUTH::onAuthChoiceChanged, this); + m_TestButton->Bind(wxEVT_BUTTON, &DIALOG_GIT_AUTH::onTestClick, this ); +} + +void DIALOG_GIT_AUTH::onAuthChoiceChanged(wxCommandEvent& event) +{ + if (m_AuthChoice->GetStringSelection() == "SSH") + { + m_PasswordTextCtrl->Hide(); + m_PublicKeyPicker->Show(); + } + else + { + m_PasswordTextCtrl->Show(); + m_PublicKeyPicker->Hide(); + } + + Layout(); // Re-arrange the controls +} diff --git a/common/dialogs/git/dialog_git_auth.h b/common/dialogs/git/dialog_git_auth.h new file mode 100644 index 0000000000..4016a43b7c --- /dev/null +++ b/common/dialogs/git/dialog_git_auth.h @@ -0,0 +1,59 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_REPOSITORY_DIALOG_H +#define GIT_REPOSITORY_DIALOG_H + +#include +#include +#include +#include +#include + +#include + +class DIALOG_GIT_AUTH : public DIALOG_SHIM +{ +public: + DIALOG_GIT_AUTH( wxWindow* parent ); + virtual ~DIALOG_GIT_AUTH(); + +private: + void CreateControls(); + void onAuthChoiceChanged( wxCommandEvent& event ); + void onTestClick( wxCommandEvent& aEvent ); + void onOKClick( wxCommandEvent& aEvent ); + void onCancelClick( wxCommandEvent& aEvent ); + + wxTextCtrl* m_NameTextCtrl; + wxTextCtrl* m_UrlTextCtrl; + wxChoice* m_AuthChoice; + wxTextCtrl* m_UserNameTextCtrl; + wxTextCtrl* m_PasswordTextCtrl; + wxButton* m_TestButton; + wxButton* m_OkButton; + wxButton* m_CancelButton; + wxFilePickerCtrl* m_PublicKeyPicker; +}; + +#endif // GIT_REPOSITORY_DIALOG_H diff --git a/common/dialogs/git/dialog_git_commit.cpp b/common/dialogs/git/dialog_git_commit.cpp new file mode 100644 index 0000000000..d55144617c --- /dev/null +++ b/common/dialogs/git/dialog_git_commit.cpp @@ -0,0 +1,204 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "dialog_git_commit.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +DIALOG_GIT_COMMIT::DIALOG_GIT_COMMIT( wxWindow* parent, git_repository* repo, + const wxString& defaultAuthorName, + const wxString& defaultAuthorEmail, + const std::map& filesToCommit ) : + DIALOG_SHIM( parent, wxID_ANY, "Commit Changes" ) +{ + wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL ); + + + // List Control for files to commit + m_listCtrl = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, + wxLC_REPORT | wxLC_SINGLE_SEL ); + + // Set up columns + m_listCtrl->EnableCheckBoxes(); + m_listCtrl->AppendColumn( "Filename" ); + m_listCtrl->AppendColumn( "Placeholder" ); + + // Set column widths + m_listCtrl->SetColumnWidth(0, 200); + m_listCtrl->SetColumnWidth(1, 200); + + // Set up image list for icons + wxImageList* imageList = new wxImageList( 16, 16, true, + static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_LAST ) ); + + imageList->Add( KiBitmap( BITMAPS::git_good_check ) ); // PLACEHOLDER + imageList->Add( KiBitmap( BITMAPS::git_good_check ) ); // GIT_STATUS_CURRENT + imageList->Add( KiBitmap( BITMAPS::git_modified ) ); // GIT_STATUS_MODIFIED + imageList->Add( KiBitmap( BITMAPS::git_add ) ); // GIT_STATUS_ADDED + imageList->Add( KiBitmap( BITMAPS::git_delete ) ); // GIT_STATUS_DELETED + imageList->Add( KiBitmap( BITMAPS::git_out_of_date ) ); // GIT_STATUS_BEHIND + imageList->Add( KiBitmap( BITMAPS::git_changed_ahead ) );// GIT_STATUS_AHEAD + imageList->Add( KiBitmap( BITMAPS::git_conflict ) ); // GIT_STATUS_CONFLICTED + + // Assign the image list to the list control + m_listCtrl->SetImageList(imageList, wxIMAGE_LIST_SMALL); + + // Populate list control with items + for ( auto& [filename, status] : filesToCommit ) + { + int i = m_listCtrl->GetItemCount(); + m_listCtrl->InsertItem(i, filename ); + + if( status & ( GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_NEW ) ) + { + m_listCtrl->SetItem( i, 1, "New" ); + m_listCtrl->SetItemImage(i, static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED ) ); + } + else if( status & ( GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_MODIFIED ) ) + { + m_listCtrl->SetItem( i, 1, "Modified" ); + m_listCtrl->SetItemImage(i, static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_MODIFIED ) ); + } + else if( status & ( GIT_STATUS_INDEX_DELETED | GIT_STATUS_WT_DELETED ) ) + { + m_listCtrl->SetItem( i, 1, "Deleted" ); + m_listCtrl->SetItemImage(i, static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_DELETED ) ); + } + else + { + printf(" Unknown status: %d\n", status ); + } + } + + sizer->Add(m_listCtrl, 1, wxEXPAND | wxALL, 5); + + // Commit Message Text Control + wxStaticText* commitMessageLabel = new wxStaticText( this, wxID_ANY, _( "Commit Message:" ) ); + m_commitMessageTextCtrl = + new wxTextCtrl( this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE ); + sizer->Add( commitMessageLabel, 0, wxALL, 5 ); + sizer->Add( m_commitMessageTextCtrl, 1, wxEXPAND | wxALL, 5 ); + + // Author Name and Email Text Control + wxStaticText* authorLabel = new wxStaticText( this, wxID_ANY, _( "Author:" ) ); + wxString defaultAuthor = defaultAuthorName + " <" + defaultAuthorEmail + ">"; + m_authorTextCtrl = + new wxTextCtrl( this, wxID_ANY, defaultAuthor, wxDefaultPosition, wxDefaultSize, 0 ); + sizer->Add( authorLabel, 0, wxALL, 5 ); + sizer->Add( m_authorTextCtrl, 0, wxEXPAND | wxALL, 5 ); + + // OK and Cancel Buttons + + wxStdDialogButtonSizer* buttonSizer = new wxStdDialogButtonSizer(); + + m_okButton = new wxButton( this, wxID_OK, _( "OK" ) ); + wxButton* cancelButton = new wxButton( this, wxID_CANCEL, _( "Cancel" ) ); + buttonSizer->Add( cancelButton, 0, wxALL, 5 ); + buttonSizer->Add( m_okButton, 0, wxALL, 5 ); + buttonSizer->Realize(); + + sizer->Add( buttonSizer, 0, wxALIGN_RIGHT | wxALL, 5 ); + + SetSizerAndFit( sizer ); + + SetupStandardButtons( { { wxID_OK, _( "C&ommit" ) } } ); + + // Bind events + Bind( wxEVT_TEXT, &DIALOG_GIT_COMMIT::OnTextChanged, this, m_commitMessageTextCtrl->GetId() ); + + // Set the repository and defaults + m_repo = repo; + m_defaultAuthorName = defaultAuthorName; + m_defaultAuthorEmail = defaultAuthorEmail; +} + + +void DIALOG_GIT_COMMIT::OnTextChanged( wxCommandEvent& aEvent ) +{ + if( m_commitMessageTextCtrl->GetValue().IsEmpty() ) + { + m_okButton->Disable(); + m_okButton->SetToolTip( _( "Commit message cannot be empty" ) ); + } + else + { + m_okButton->Enable(); + m_okButton->SetToolTip( wxEmptyString ); + } +} + + +wxString DIALOG_GIT_COMMIT::GetCommitMessage() const +{ + return m_commitMessageTextCtrl->GetValue(); +} + + +wxString DIALOG_GIT_COMMIT::GetAuthorName() const +{ + wxString authorText = m_authorTextCtrl->GetValue(); + size_t pos = authorText.find( '<' ); + + if( pos != wxString::npos ) + return authorText.substr( 0, pos ).Trim(); + + return wxEmptyString; +} + + +wxString DIALOG_GIT_COMMIT::GetAuthorEmail() const +{ + wxString authorText = m_authorTextCtrl->GetValue(); + size_t startPos = authorText.find( '<' ); + size_t endPos = authorText.find( '>' ); + + if( startPos != wxString::npos && endPos != wxString::npos && startPos < endPos ) + return authorText.substr( startPos + 1, endPos - startPos - 1 ).Trim(); + + return wxEmptyString; +} + + +std::vector DIALOG_GIT_COMMIT::GetSelectedFiles() const +{ + std::vector selectedFiles; + + long item = -1; + while( ( item = m_listCtrl->GetNextItem( item, wxLIST_NEXT_ALL ) ) + != -1 ) + { + if( m_listCtrl->IsItemChecked( item ) ) + selectedFiles.push_back( m_listCtrl->GetItemText( item ) ); + } + + return selectedFiles; +} \ No newline at end of file diff --git a/common/dialogs/git/dialog_git_commit.h b/common/dialogs/git/dialog_git_commit.h new file mode 100644 index 0000000000..52e1e6ac6e --- /dev/null +++ b/common/dialogs/git/dialog_git_commit.h @@ -0,0 +1,62 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2021-2022 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef DIALOG_GIT_COMMIT_H +#define DIALOG_GIT_COMMIT_H + +#include +#include + +class wxCheckBox; +class wxListCtrl; +class DIALOG_GIT_COMMIT : public DIALOG_SHIM +{ +public: + DIALOG_GIT_COMMIT( wxWindow* parent, git_repository* repo, + const wxString& defaultAuthorName, + const wxString& defaultAuthorEmail, + const std::map& filesToCommit ); + + wxString GetCommitMessage() const; + + wxString GetAuthorName() const; + + wxString GetAuthorEmail() const; + + std::vector GetSelectedFiles() const; + + void OnTextChanged( wxCommandEvent& event ); + +private: + wxTextCtrl* m_commitMessageTextCtrl; + wxTextCtrl* m_authorTextCtrl; + wxListCtrl* m_listCtrl; + wxButton* m_okButton; + + git_repository* m_repo; + wxString m_defaultAuthorName; + wxString m_defaultAuthorEmail; + std::vector m_filesToCommit; +}; + +#endif // DIALOG_GIT_COMMIT_H \ No newline at end of file diff --git a/common/dialogs/git/dialog_git_progress.cpp b/common/dialogs/git/dialog_git_progress.cpp new file mode 100644 index 0000000000..633088a5e5 --- /dev/null +++ b/common/dialogs/git/dialog_git_progress.cpp @@ -0,0 +1,49 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2021-2022 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + + +#include +#include + +#include + +class GIT_PROGRESS : public DIALOG_SHIM { +public: + GIT_PROGRESS(wxWindow* aParent, int aMaxValue1, int aMaxValue2); + ~GIT_PROGRESS(); + + void SetStatusText(const wxString& aText); + bool UpdateProgressBar1(int aValue); + bool UpdateProgressBar2(int aValue); + +private: + void OnCancel(wxCommandEvent& aEvent); + + wxGauge* m_progBar1; + wxGauge* m_progBar2; + wxButton* m_cancelBtn; + wxStatusBar* m_statusBar; + bool m_cancelled; + + DECLARE_EVENT_TABLE() +}; diff --git a/common/dialogs/git/dialog_git_repository.cpp b/common/dialogs/git/dialog_git_repository.cpp new file mode 100644 index 0000000000..fa58270d7f --- /dev/null +++ b/common/dialogs/git/dialog_git_repository.cpp @@ -0,0 +1,448 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "dialog_git_repository.h" +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + + +DIALOG_GIT_REPOSITORY::DIALOG_GIT_REPOSITORY( wxWindow* aParent, git_repository* aRepository, wxString aURL ) : + DIALOG_GIT_REPOSITORY_BASE( aParent ), m_repository( aRepository ), m_prevFile( wxEmptyString ), + m_tested( 0 ), m_failedTest( false ), m_testError( wxEmptyString ), m_tempRepo( false ), + m_repoType( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) +{ + m_txtName->SetFocus(); + + if( !m_repository ) + { + // Make a temporary repository to test the connection + m_tempRepo = true; + m_tempPath = wxFileName::CreateTempFileName( "kicadtestrepo" ); + + git_repository_init_options options = GIT_REPOSITORY_INIT_OPTIONS_INIT; + options.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_NO_REINIT; + git_repository_init_ext( &m_repository, m_tempPath.ToStdString().c_str(), &options ); + } + + if( !aURL.empty() ) + m_txtURL->SetValue( aURL ); + else + extractClipboardData(); + + if( !m_txtURL->GetValue().IsEmpty() ) + updateURLData(); + + SetupStandardButtons(); + updateAuthControls(); +} + +DIALOG_GIT_REPOSITORY::~DIALOG_GIT_REPOSITORY() +{ + if( m_tempRepo ) + { + git_repository_free( m_repository ); + RmDirRecursive( m_tempPath ); + } +} + + +bool DIALOG_GIT_REPOSITORY::extractClipboardData() +{ + if( wxTheClipboard->Open() && wxTheClipboard->IsSupported( wxDF_TEXT ) ) + { + wxString clipboardText; + wxTextDataObject textData; + + if( wxTheClipboard->GetData( textData ) && !( clipboardText = textData.GetText() ).empty() ) + { + if( std::get<0>( isValidHTTPS( clipboardText ) ) + || std::get<0>( isValidSSH( clipboardText ) ) ) + m_txtURL->SetValue( clipboardText ); + } + + wxTheClipboard->Close(); + } + + return false; +} + + +void DIALOG_GIT_REPOSITORY::setDefaultSSHKey() +{ + wxFileName sshKey; + sshKey.SetPath( wxGetUserHome() ); + wxString retval; + + sshKey.AppendDir( ".ssh" ); + sshKey.SetFullName( "id_rsa" ); + + if( sshKey.FileExists() ) + { + retval = sshKey.GetFullPath(); + } + else if( sshKey.SetFullName( "id_dsa" ); sshKey.FileExists() ) + { + retval = sshKey.GetFullPath(); + } + else if( sshKey.SetFullName( "id_ecdsa" ); sshKey.FileExists() ) + { + retval = sshKey.GetFullPath(); + } + + if( !retval.empty() ) + { + m_fpSSHKey->SetFileName( retval ); + wxFileDirPickerEvent evt; + evt.SetPath( retval ); + OnFileUpdated( evt ); + } +} + + +void DIALOG_GIT_REPOSITORY::OnUpdateUI( wxUpdateUIEvent& event ) +{ + // event.Enable( !m_txtName->GetValue().IsEmpty() && !m_txtURL->GetValue().IsEmpty() ); +} + + +void DIALOG_GIT_REPOSITORY::SetEncrypted( bool aEncrypted ) +{ + if( aEncrypted ) + { + m_txtPassword->Enable(); + m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) ); + } + else + { + m_txtPassword->SetValue( wxEmptyString ); + m_txtPassword->SetToolTip( wxEmptyString ); + m_txtPassword->Disable(); + } +} + +std::tuple DIALOG_GIT_REPOSITORY::isValidHTTPS( const wxString& url ) +{ + wxRegEx regex( R"((https?:\/\/)(([^:]+)(:([^@]+))?@)?([^\/]+\/[^\s]+))" ); + + if( regex.Matches( url ) ) + { + wxString username = regex.GetMatch( url, 3 ); + wxString password = regex.GetMatch( url, 5 ); + wxString repoAddress = regex.GetMatch( url, 1 ) + regex.GetMatch( url, 6 ); + return std::make_tuple( true, username, password, repoAddress ); + } + + return std::make_tuple( false, "", "", "" ); +} + + +std::tuple DIALOG_GIT_REPOSITORY::isValidSSH( const wxString& url ) +{ + wxRegEx regex( R"((?:ssh:\/\/)?([^@]+)@([^\/]+\/[^\s]+))" ); + + if( regex.Matches( url ) ) + { + wxString username = regex.GetMatch( url, 1 ); + wxString repoAddress = regex.GetMatch( url, 2 ); + return std::make_tuple( true, username, repoAddress ); + } + + return std::make_tuple( false, "", "" ); +} + + +static wxString get_repo_name( wxString& aRepoAddr ) +{ + wxString retval; + size_t last_slash = aRepoAddr.find_last_of( '/' ); + bool ends_with_dot_git = aRepoAddr.EndsWith( ".git" ); + + if( ends_with_dot_git ) + retval = aRepoAddr.substr( last_slash + 1, aRepoAddr.size() - last_slash - 5 ); + else + retval = aRepoAddr.substr( last_slash + 1, aRepoAddr.size() - last_slash ); + + return retval; +} + + +void DIALOG_GIT_REPOSITORY::OnLocationExit( wxFocusEvent& event ) +{ + updateURLData(); + updateAuthControls(); +} + + +void DIALOG_GIT_REPOSITORY::updateURLData() +{ + wxString url = m_txtURL->GetValue(); + + if( url.IsEmpty() ) + return; + + if( url.Contains( "https://" ) || url.Contains( "http://" ) ) + { + auto [valid, username, password, repoAddress] = isValidHTTPS( url ); + + if( valid ) + { + m_ConnType->SetSelection( static_cast( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) ); + SetUsername( username ); + SetPassword( password ); + m_txtURL->SetValue( repoAddress ); + + if( m_txtName->GetValue().IsEmpty() ) + m_txtName->SetValue( get_repo_name( repoAddress ) ); + + } + } + else if( url.Contains( "ssh://" ) || url.Contains( "git@" ) ) + { + auto [valid, username, repoAddress] = isValidSSH( url ); + + if( valid ) + { + m_ConnType->SetSelection( static_cast( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) ); + m_txtUsername->SetValue( username ); + m_txtURL->SetValue( repoAddress ); + + if( m_txtName->GetValue().IsEmpty() ) + m_txtName->SetValue( get_repo_name( repoAddress ) ); + + setDefaultSSHKey(); + } + } +} + + +void DIALOG_GIT_REPOSITORY::OnTestClick( wxCommandEvent& event ) +{ + git_remote* remote = nullptr; + git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; + + // We track if we have already tried to connect. + // If we have, the server may come back to offer another connection + // type, so we need to keep track of how many times we have tried. + m_tested = 0; + + callbacks.credentials = []( git_cred** aOut, const char* aUrl, const char* aUsername, + unsigned int aAllowedTypes, void* aPayload ) -> int + { + DIALOG_GIT_REPOSITORY* dialog = static_cast( aPayload ); + + if( dialog->GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) + return GIT_PASSTHROUGH; + + if( aAllowedTypes & GIT_CREDENTIAL_USERNAME + && !( dialog->GetTested() & GIT_CREDTYPE_USERNAME ) ) + { + wxString username = dialog->GetUsername().Trim().Trim( false ); + git_cred_username_new( aOut, username.ToStdString().c_str() ); + dialog->GetTested() |= GIT_CREDTYPE_USERNAME; + } + else if( dialog->GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS + && ( aAllowedTypes & GIT_CREDENTIAL_USERPASS_PLAINTEXT ) + && !( dialog->GetTested() & GIT_CREDTYPE_USERPASS_PLAINTEXT ) ) + { + wxString username = dialog->GetUsername().Trim().Trim( false ); + wxString password = dialog->GetPassword().Trim().Trim( false ); + + git_cred_userpass_plaintext_new( aOut, username.ToStdString().c_str(), + password.ToStdString().c_str() ); + dialog->GetTested() |= GIT_CREDTYPE_USERPASS_PLAINTEXT; + } + else if( dialog->GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH + && ( aAllowedTypes & GIT_CREDENTIAL_SSH_KEY ) + && !( dialog->GetTested() & GIT_CREDTYPE_SSH_KEY ) ) + { + // SSH key authentication + wxString sshKey = dialog->GetRepoSSHPath(); + wxString sshPubKey = sshKey + ".pub"; + wxString username = dialog->GetUsername().Trim().Trim( false ); + wxString password = dialog->GetPassword().Trim().Trim( false ); + + git_cred_ssh_key_new( aOut, username.ToStdString().c_str(), + sshPubKey.ToStdString().c_str(), sshKey.ToStdString().c_str(), + password.ToStdString().c_str() ); + dialog->GetTested() |= GIT_CREDTYPE_SSH_KEY; + } + else + { + return GIT_PASSTHROUGH; + } + + return GIT_OK; + }; + + callbacks.payload = this; + + git_remote_create_with_fetchspec( &remote, m_repository, "origin", m_txtURL->GetValue(), + "+refs/heads/*:refs/remotes/origin/*" ); + + if( git_remote_connect( remote, GIT_DIRECTION_FETCH, &callbacks, nullptr, nullptr ) != GIT_OK ) + SetTestResult( true, git_error_last()->message ); + else + SetTestResult( false, wxEmptyString ); + + git_remote_disconnect( remote ); + git_remote_free( remote ); + + auto dlg = wxMessageDialog( this, wxEmptyString, _( "Test connection" ), wxOK | wxICON_INFORMATION ); + + if( !m_failedTest ) + { + dlg.SetMessage( _( "Connection successful" ) ); + } + else + { + dlg.SetMessage( wxString::Format( _( "Could not connect to '%s' " ), m_txtURL->GetValue() ) ); + dlg.SetExtendedMessage( m_testError ); + } + + dlg.ShowModal(); +} + + +void DIALOG_GIT_REPOSITORY::OnFileUpdated( wxFileDirPickerEvent& aEvent ) +{ + wxString file = aEvent.GetPath(); + + if( file.ends_with( ".pub" ) ) + file = file.Left( file.size() - 4 ); + + std::ifstream ifs( file.ToStdString() ); + + if( !ifs.good() || !ifs.is_open() ) + { + DisplayErrorMessage( this, wxString::Format( _( "Could not open private key '%s'" ), file ), + wxString::Format( "%s: %d", std::strerror( errno ), errno ) ); + return; + } + + std::string line; + std::getline( ifs, line ); + + bool isValid = ( line.find( "PRIVATE KEY" ) != std::string::npos ); + bool isEncrypted = ( line.find( "ENCRYPTED" ) != std::string::npos ); + + if( !isValid ) + { + DisplayErrorMessage( this, _( "Invalid SSH Key" ), _( "The selected file is not a valid SSH private key" ) ); + CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } ); + return; + } + + if( isEncrypted ) + { + m_txtPassword->Enable(); + m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) ); + } + else + { + m_txtPassword->SetValue( wxEmptyString ); + m_txtPassword->SetToolTip( wxEmptyString ); + m_txtPassword->Disable(); + } + + ifs.close(); + + std::ifstream pubIfs( file + ".pub" ); + + if( !pubIfs.good() || !pubIfs.is_open() ) + { + DisplayErrorMessage( this, wxString::Format( _( "Could not open public key '%s'" ), file + ".pub" ), + wxString::Format( "%s: %d", std::strerror( errno ), errno ) ); + aEvent.SetPath( wxEmptyString ); + CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } ); + return; + } + + m_prevFile = file; + pubIfs.close(); +} + + +void DIALOG_GIT_REPOSITORY::OnOKClick( wxCommandEvent& event ) +{ + // Save the repository details + + if( m_txtName->GetValue().IsEmpty() ) + { + DisplayErrorMessage( this, _( "Missing information" ), _( "Please enter a name for the repository" ) ); + return; + } + + if( m_txtURL->GetValue().IsEmpty() ) + { + DisplayErrorMessage( this, _( "Missing information" ), _( "Please enter a URL for the repository" ) ); + return; + } + + EndModal( wxID_OK ); +} + +void DIALOG_GIT_REPOSITORY::updateAuthControls() +{ + if( m_ConnType->GetSelection() == static_cast( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) ) + { + m_panelAuth->Show( false ); + } + else + { + m_panelAuth->Show( true ); + + if( m_ConnType->GetSelection() == static_cast( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) ) + { + m_fpSSHKey->Show( true ); + m_labelSSH->Show( true ); + m_labelPass1->SetLabel( _( "SSH Key Password" ) ); + } + else + { + m_fpSSHKey->Show( false ); + m_labelSSH->Show( false ); + m_labelPass1->SetLabel( _( "Password" ) ); + setDefaultSSHKey(); + } + } +} + + +void DIALOG_GIT_REPOSITORY::OnSelectConnType( wxCommandEvent& event ) +{ + updateAuthControls(); +} diff --git a/common/dialogs/git/dialog_git_repository.h b/common/dialogs/git/dialog_git_repository.h new file mode 100644 index 0000000000..8f4abd5c38 --- /dev/null +++ b/common/dialogs/git/dialog_git_repository.h @@ -0,0 +1,124 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef DIALOG_GIT_REPOSITORY_H_ +#define DIALOG_GIT_REPOSITORY_H_ + +#include "dialog_git_repository_base.h" + +#include +#include + +class DIALOG_GIT_REPOSITORY : public DIALOG_GIT_REPOSITORY_BASE +{ +public: + DIALOG_GIT_REPOSITORY( wxWindow* aParent, git_repository* aRepository, wxString aURL = wxEmptyString ); + ~DIALOG_GIT_REPOSITORY() override; + + void SetTestResult( bool aFailed, const wxString& aError ) + { + m_failedTest = aFailed; + m_testError = aError; + } + + void SetRepoType( KIGIT_COMMON::GIT_CONN_TYPE aType ) + { + m_ConnType->SetSelection( static_cast( aType ) ); + updateAuthControls(); + } + + KIGIT_COMMON::GIT_CONN_TYPE GetRepoType() const { return static_cast( m_ConnType->GetSelection() ); } + + void SetRepoName( const wxString& aName ) { m_txtName->SetValue( aName ); } + wxString GetRepoName() const { return m_txtName->GetValue(); } + + void SetRepoURL( const wxString& aURL ) { m_txtURL->SetValue( aURL ); } + wxString GetRepoURL() const { return m_txtURL->GetValue(); } + + /** + * @brief Get the Bare Repo U R L object + * + * @return wxString without the protocol + */ + wxString GetBareRepoURL() const + { + wxString url = m_txtURL->GetValue(); + + if( url.StartsWith( "https://" ) ) + url = url.Mid( 8 ); + else if( url.StartsWith( "http://" ) ) + url = url.Mid( 7 ); + else if( url.StartsWith( "ssh://" ) ) + url = url.Mid( 6 ); + + return url; + } + + void SetUsername( const wxString& aUsername ) { m_txtUsername->SetValue( aUsername ); } + wxString GetUsername() const { return m_txtUsername->GetValue(); } + + void SetPassword( const wxString& aPassword ) { m_txtPassword->SetValue( aPassword ); } + wxString GetPassword() const { return m_txtPassword->GetValue(); } + + void SetRepoSSHPath( const wxString& aPath ) { m_fpSSHKey->SetFileName( aPath ); m_prevFile = aPath; } + wxString GetRepoSSHPath() const { return m_fpSSHKey->GetFileName().GetFullPath(); } + + unsigned& GetTested() { return m_tested; } + + void SetEncrypted( bool aEncrypted = true ); + +private: + void OnUpdateUI( wxUpdateUIEvent& event ) override; + void OnLocationExit( wxFocusEvent& event ) override; + void OnOKClick( wxCommandEvent& event ) override; + + void OnSelectConnType( wxCommandEvent& event ) override; + void OnTestClick( wxCommandEvent& event ) override; + + void OnFileUpdated( wxFileDirPickerEvent& event ) override; + + void setDefaultSSHKey(); + + void updateAuthControls(); + void updateURLData(); + bool extractClipboardData(); + + std::tuple isValidHTTPS( const wxString& url ); + std::tuple isValidSSH( const wxString& url ); + + git_repository* m_repository; + + wxString m_prevFile; + + unsigned m_tested; + bool m_failedTest; + wxString m_testError; + + bool m_tempRepo; + wxString m_tempPath; + + KIGIT_COMMON::GIT_CONN_TYPE m_repoType; + +}; + +#endif /* DIALOG_GIT_REPOSITORY_H_ */ \ No newline at end of file diff --git a/common/dialogs/git/dialog_git_repository_base.cpp b/common/dialogs/git/dialog_git_repository_base.cpp new file mode 100644 index 0000000000..332dbc1d7f --- /dev/null +++ b/common/dialogs/git/dialog_git_repository_base.cpp @@ -0,0 +1,172 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-254-gc2ef7767) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#include "dialog_git_repository_base.h" + +/////////////////////////////////////////////////////////////////////////// + +DIALOG_GIT_REPOSITORY_BASE::DIALOG_GIT_REPOSITORY_BASE( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : DIALOG_SHIM( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1,-1 ), wxDefaultSize ); + + wxBoxSizer* bSizerMain; + bSizerMain = new wxBoxSizer( wxVERTICAL ); + + m_staticText1 = new wxStaticText( this, wxID_ANY, _("Connection"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1->Wrap( -1 ); + bSizerMain->Add( m_staticText1, 0, wxLEFT|wxTOP, 10 ); + + m_staticline1 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizerMain->Add( m_staticline1, 0, wxEXPAND|wxTOP, 5 ); + + wxFlexGridSizer* fgSizer2; + fgSizer2 = new wxFlexGridSizer( 0, 2, 0, 0 ); + fgSizer2->AddGrowableCol( 1 ); + fgSizer2->SetFlexibleDirection( wxBOTH ); + fgSizer2->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_staticText3 = new wxStaticText( this, wxID_ANY, _("Name"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText3->Wrap( -1 ); + fgSizer2->Add( m_staticText3, 0, wxALL, 5 ); + + m_txtName = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer2->Add( m_txtName, 0, wxALL|wxEXPAND, 5 ); + + m_staticText4 = new wxStaticText( this, wxID_ANY, _("Location"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText4->Wrap( -1 ); + fgSizer2->Add( m_staticText4, 0, wxALL, 5 ); + + m_txtURL = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + fgSizer2->Add( m_txtURL, 0, wxALL|wxEXPAND, 5 ); + + m_staticText9 = new wxStaticText( this, wxID_ANY, _("Connection Type"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText9->Wrap( -1 ); + fgSizer2->Add( m_staticText9, 0, wxALL, 5 ); + + wxBoxSizer* bSizer3; + bSizer3 = new wxBoxSizer( wxHORIZONTAL ); + + wxString m_ConnTypeChoices[] = { _("HTTPS"), _("SSH"), _("Local") }; + int m_ConnTypeNChoices = sizeof( m_ConnTypeChoices ) / sizeof( wxString ); + m_ConnType = new wxChoice( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_ConnTypeNChoices, m_ConnTypeChoices, 0 ); + m_ConnType->SetSelection( 0 ); + bSizer3->Add( m_ConnType, 1, wxEXPAND|wxLEFT|wxRIGHT, 5 ); + + + bSizer3->Add( 0, 0, 1, wxEXPAND, 5 ); + + + fgSizer2->Add( bSizer3, 1, wxEXPAND, 5 ); + + + bSizerMain->Add( fgSizer2, 1, wxEXPAND, 5 ); + + m_panelAuth = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + wxBoxSizer* m_szAuth; + m_szAuth = new wxBoxSizer( wxVERTICAL ); + + + m_szAuth->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxBoxSizer* bSizer11; + bSizer11 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticText2 = new wxStaticText( m_panelAuth, wxID_ANY, _("Authentication"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText2->Wrap( -1 ); + bSizer11->Add( m_staticText2, 0, wxLEFT|wxTOP, 10 ); + + + bSizer11->Add( 0, 0, 1, wxEXPAND, 5 ); + + + m_szAuth->Add( bSizer11, 0, wxEXPAND, 5 ); + + m_staticline2 = new wxStaticLine( m_panelAuth, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + m_szAuth->Add( m_staticline2, 0, wxEXPAND | wxALL, 5 ); + + wxFlexGridSizer* fgSshSizer; + fgSshSizer = new wxFlexGridSizer( 0, 2, 0, 0 ); + fgSshSizer->AddGrowableCol( 1 ); + fgSshSizer->SetFlexibleDirection( wxBOTH ); + fgSshSizer->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_labelSSH = new wxStaticText( m_panelAuth, wxID_ANY, _("SSH Private Key"), wxDefaultPosition, wxDefaultSize, 0 ); + m_labelSSH->Wrap( -1 ); + fgSshSizer->Add( m_labelSSH, 0, wxALIGN_CENTER_VERTICAL|wxALL|wxRESERVE_SPACE_EVEN_IF_HIDDEN, 5 ); + + wxBoxSizer* bSizer41; + bSizer41 = new wxBoxSizer( wxHORIZONTAL ); + + m_fpSSHKey = new wxFilePickerCtrl( m_panelAuth, wxID_ANY, wxEmptyString, _("Select SSH private key file"), _("*"), wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST|wxFLP_OPEN ); + bSizer41->Add( m_fpSSHKey, 1, wxEXPAND|wxLEFT|wxRESERVE_SPACE_EVEN_IF_HIDDEN|wxRIGHT, 5 ); + + m_btnTest = new wxButton( m_panelAuth, wxID_ANY, _("Test"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer41->Add( m_btnTest, 0, wxLEFT|wxRIGHT, 5 ); + + + fgSshSizer->Add( bSizer41, 1, wxEXPAND, 5 ); + + m_staticText11 = new wxStaticText( m_panelAuth, wxID_ANY, _("Username"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText11->Wrap( -1 ); + fgSshSizer->Add( m_staticText11, 0, wxALL, 5 ); + + m_txtUsername = new wxTextCtrl( m_panelAuth, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + fgSshSizer->Add( m_txtUsername, 0, wxALL|wxEXPAND, 5 ); + + m_labelPass1 = new wxStaticText( m_panelAuth, wxID_ANY, _("SSH Key Password"), wxDefaultPosition, wxDefaultSize, 0 ); + m_labelPass1->Wrap( -1 ); + fgSshSizer->Add( m_labelPass1, 0, wxALL, 5 ); + + m_txtPassword = new wxTextCtrl( m_panelAuth, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + fgSshSizer->Add( m_txtPassword, 0, wxALL|wxEXPAND, 5 ); + + + m_szAuth->Add( fgSshSizer, 1, wxEXPAND, 5 ); + + + m_panelAuth->SetSizer( m_szAuth ); + m_panelAuth->Layout(); + m_szAuth->Fit( m_panelAuth ); + bSizerMain->Add( m_panelAuth, 1, wxALL|wxEXPAND|wxRESERVE_SPACE_EVEN_IF_HIDDEN, 0 ); + + m_sdbSizer = new wxStdDialogButtonSizer(); + m_sdbSizerOK = new wxButton( this, wxID_OK ); + m_sdbSizer->AddButton( m_sdbSizerOK ); + m_sdbSizerCancel = new wxButton( this, wxID_CANCEL ); + m_sdbSizer->AddButton( m_sdbSizerCancel ); + m_sdbSizer->Realize(); + + bSizerMain->Add( m_sdbSizer, 0, wxALL|wxEXPAND, 5 ); + + + this->SetSizer( bSizerMain ); + this->Layout(); + + this->Centre( wxBOTH ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnClose ) ); + this->Connect( wxEVT_UPDATE_UI, wxUpdateUIEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnUpdateUI ) ); + m_txtURL->Connect( wxEVT_KILL_FOCUS, wxFocusEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnLocationExit ), NULL, this ); + m_ConnType->Connect( wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnSelectConnType ), NULL, this ); + m_fpSSHKey->Connect( wxEVT_COMMAND_FILEPICKER_CHANGED, wxFileDirPickerEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnFileUpdated ), NULL, this ); + m_btnTest->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnTestClick ), NULL, this ); + m_sdbSizerOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnOKClick ), NULL, this ); +} + +DIALOG_GIT_REPOSITORY_BASE::~DIALOG_GIT_REPOSITORY_BASE() +{ + // Disconnect Events + this->Disconnect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnClose ) ); + this->Disconnect( wxEVT_UPDATE_UI, wxUpdateUIEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnUpdateUI ) ); + m_txtURL->Disconnect( wxEVT_KILL_FOCUS, wxFocusEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnLocationExit ), NULL, this ); + m_ConnType->Disconnect( wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnSelectConnType ), NULL, this ); + m_fpSSHKey->Disconnect( wxEVT_COMMAND_FILEPICKER_CHANGED, wxFileDirPickerEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnFileUpdated ), NULL, this ); + m_btnTest->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnTestClick ), NULL, this ); + m_sdbSizerOK->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_GIT_REPOSITORY_BASE::OnOKClick ), NULL, this ); + +} diff --git a/common/dialogs/git/dialog_git_repository_base.fbp b/common/dialogs/git/dialog_git_repository_base.fbp new file mode 100644 index 0000000000..ede00eae2d --- /dev/null +++ b/common/dialogs/git/dialog_git_repository_base.fbp @@ -0,0 +1,1335 @@ + + + + + + C++ + 1 + source_name + 0 + 0 + res + UTF-8 + connect + dialog_git_repository_base + 1000 + none + + + 1 + DIALOG_GIT_REPOSITORY_BASE + + . + + 1 + 1 + 1 + 1 + UI + 0 + 0 + 0 + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 0 + 1 + impl_virtual + + + + 0 + wxID_ANY + + -1,-1 + DIALOG_GIT_REPOSITORY_BASE + + 682,598 + wxCAPTION|wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER + DIALOG_SHIM; dialog_shim.h + Git Repository + + 0 + + + + OnClose + OnUpdateUI + + + bSizerMain + wxVERTICAL + none + + 10 + wxLEFT|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Connection + 0 + + 0 + + + 0 + + 1 + m_staticText1 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_staticline1 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + + + 5 + wxEXPAND + 1 + + 2 + wxBOTH + 1 + + 0 + + fgSizer2 + wxFLEX_GROWMODE_SPECIFIED + none + 0 + 0 + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Name + 0 + + 0 + + + 0 + + 1 + m_staticText3 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_txtName + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Location + 0 + + 0 + + + 0 + + 1 + m_staticText4 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_txtURL + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + OnLocationExit + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Connection Type + 0 + + 0 + + + 0 + + 1 + m_staticText9 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 1 + + + bSizer3 + wxHORIZONTAL + none + + 5 + wxEXPAND|wxLEFT|wxRIGHT + 1 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + "HTTPS" "SSH" "Local" + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_ConnType + 1 + + + protected + 1 + + Resizable + 0 + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnSelectConnType + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + + + + + 0 + wxALL|wxEXPAND|wxRESERVE_SPACE_EVEN_IF_HIDDEN + 1 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_panelAuth + 1 + + + protected + 1 + + Resizable + 1 + + ; ; forward_declare + 0 + + + + wxTAB_TRAVERSAL + + + m_szAuth + wxVERTICAL + none + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxEXPAND + 0 + + + bSizer11 + wxHORIZONTAL + none + + 10 + wxLEFT|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Authentication + 0 + + 0 + + + 0 + + 1 + m_staticText2 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_staticline2 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + + + 5 + wxEXPAND + 1 + + 2 + wxBOTH + 1 + + 0 + + fgSshSizer + wxFLEX_GROWMODE_SPECIFIED + none + 0 + 0 + + 5 + wxALIGN_CENTER_VERTICAL|wxALL|wxRESERVE_SPACE_EVEN_IF_HIDDEN + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + SSH Private Key + 0 + + 0 + + + 0 + + 1 + m_labelSSH + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 1 + + + bSizer41 + wxHORIZONTAL + none + + 5 + wxEXPAND|wxLEFT|wxRESERVE_SPACE_EVEN_IF_HIDDEN|wxRIGHT + 1 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + Select SSH private key file + + 0 + + 1 + m_fpSSHKey + 1 + + + protected + 1 + + Resizable + 1 + + wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST|wxFLP_OPEN + + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + * + + + + OnFileUpdated + + + + 5 + wxLEFT|wxRIGHT + 0 + + 1 + 1 + 1 + 1 + + + + + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + Test + + 0 + + 0 + + + 0 + + 1 + m_btnTest + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnTestClick + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Username + 0 + + 0 + + + 0 + + 1 + m_staticText11 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_txtUsername + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + SSH Key Password + 0 + + 0 + + + 0 + + 1 + m_labelPass1 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_txtPassword + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + + + + 5 + wxALL|wxEXPAND + 0 + + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + + m_sdbSizer + protected + OnOKClick + + + + + + diff --git a/common/dialogs/git/dialog_git_repository_base.h b/common/dialogs/git/dialog_git_repository_base.h new file mode 100644 index 0000000000..0ba8c329c7 --- /dev/null +++ b/common/dialogs/git/dialog_git_repository_base.h @@ -0,0 +1,82 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-254-gc2ef7767) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "dialog_shim.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////// + + +/////////////////////////////////////////////////////////////////////////////// +/// Class DIALOG_GIT_REPOSITORY_BASE +/////////////////////////////////////////////////////////////////////////////// +class DIALOG_GIT_REPOSITORY_BASE : public DIALOG_SHIM +{ + private: + + protected: + wxStaticText* m_staticText1; + wxStaticLine* m_staticline1; + wxStaticText* m_staticText3; + wxTextCtrl* m_txtName; + wxStaticText* m_staticText4; + wxTextCtrl* m_txtURL; + wxStaticText* m_staticText9; + wxChoice* m_ConnType; + wxPanel* m_panelAuth; + wxStaticText* m_staticText2; + wxStaticLine* m_staticline2; + wxStaticText* m_labelSSH; + wxFilePickerCtrl* m_fpSSHKey; + wxButton* m_btnTest; + wxStaticText* m_staticText11; + wxTextCtrl* m_txtUsername; + wxStaticText* m_labelPass1; + wxTextCtrl* m_txtPassword; + wxStdDialogButtonSizer* m_sdbSizer; + wxButton* m_sdbSizerOK; + wxButton* m_sdbSizerCancel; + + // Virtual event handlers, override them in your derived class + virtual void OnClose( wxCloseEvent& event ) { event.Skip(); } + virtual void OnUpdateUI( wxUpdateUIEvent& event ) { event.Skip(); } + virtual void OnLocationExit( wxFocusEvent& event ) { event.Skip(); } + virtual void OnSelectConnType( wxCommandEvent& event ) { event.Skip(); } + virtual void OnFileUpdated( wxFileDirPickerEvent& event ) { event.Skip(); } + virtual void OnTestClick( wxCommandEvent& event ) { event.Skip(); } + virtual void OnOKClick( wxCommandEvent& event ) { event.Skip(); } + + + public: + + DIALOG_GIT_REPOSITORY_BASE( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Git Repository"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 682,598 ), long style = wxCAPTION|wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); + + ~DIALOG_GIT_REPOSITORY_BASE(); + +}; + diff --git a/common/dialogs/git/dialog_git_switch.cpp b/common/dialogs/git/dialog_git_switch.cpp new file mode 100644 index 0000000000..92e57f5e41 --- /dev/null +++ b/common/dialogs/git/dialog_git_switch.cpp @@ -0,0 +1,288 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + + +#include "dialog_git_switch.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + + +DIALOG_GIT_SWITCH::DIALOG_GIT_SWITCH( wxWindow* aParent, git_repository* aRepository ) : + DIALOG_SHIM( aParent, wxID_ANY, _( "Git Branch Switch" ), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER ), + m_timer( this ), m_repository( aRepository ) +{ + wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL ); + + // Add explanation text + wxStaticText* explanationText = + new wxStaticText( this, wxID_ANY, _( "Select or enter a branch name:" ) ); + sizer->Add( explanationText, 0, wxALL, 10 ); + + // Add branch list with three columns + m_branchList = new wxListView( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, + wxLC_REPORT | wxLC_SINGLE_SEL ); + m_branchList->InsertColumn( 0, _( "Branch" ) ); + m_branchList->InsertColumn( 1, _( "Last Commit" ) ); + m_branchList->InsertColumn( 2, _( "Last Updated" ) ); + sizer->Add( m_branchList, 1, wxALL | wxEXPAND, 10 ); + + // Add branch name text box + m_branchNameText = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxDefaultSize, wxTE_PROCESS_ENTER ); + sizer->Add( m_branchNameText, 0, wxALL | wxEXPAND, 10 ); + + // Add buttons + wxStdDialogButtonSizer* buttonSizer = new wxStdDialogButtonSizer(); + m_switchButton = new wxButton( this, wxID_OK, _( "Switch" ) ); + buttonSizer->AddButton( m_switchButton ); + m_switchButton->Disable(); + wxButton* cancelButton = new wxButton( this, wxID_CANCEL, _( "Cancel" ) ); + buttonSizer->AddButton( cancelButton ); + buttonSizer->Realize(); + sizer->Add( buttonSizer, 0, wxALIGN_RIGHT | wxALL, 10 ); + + // Bind events + Bind( wxEVT_LIST_ITEM_SELECTED, &DIALOG_GIT_SWITCH::OnBranchListSelection, this, m_branchList->GetId() ); + Bind( wxEVT_LIST_ITEM_ACTIVATED, &DIALOG_GIT_SWITCH::OnBranchListDClick, this, m_branchList->GetId() ); + Bind( wxEVT_BUTTON, &DIALOG_GIT_SWITCH::OnSwitchButton, this, m_switchButton->GetId() ); + Bind( wxEVT_BUTTON, &DIALOG_GIT_SWITCH::OnCancelButton, this, cancelButton->GetId() ); + Bind( wxEVT_TEXT, &DIALOG_GIT_SWITCH::OnTextChanged, this, m_branchNameText->GetId() ); + Bind( wxEVT_TIMER, &DIALOG_GIT_SWITCH::OnTimer, this, m_timer.GetId() ); + + // Populate branch list + PopulateBranchList(); + + // Set sizer for the dialog + SetSizerAndFit( sizer ); + + finishDialogSettings(); + + m_existingBranch = false; +} + +DIALOG_GIT_SWITCH::~DIALOG_GIT_SWITCH() +{ + StopTimer(); + Unbind( wxEVT_TIMER, &DIALOG_GIT_SWITCH::OnTimer, this, m_timer.GetId() ); +} + +void DIALOG_GIT_SWITCH::PopulateBranchList() +{ + m_branchList->DeleteAllItems(); + + // Get the branches + GetBranches(); + + // Populate the list + for( auto& [ name, data ] : m_branches ) + { + wxDateTime lastUpdated( data.lastUpdated ); + wxString lastUpdatedString = lastUpdated.Format(); + + long itemIndex = m_branchList->InsertItem( m_branchList->GetItemCount(), name ); + m_branchList->SetItem( itemIndex, 1, data.commitString ); + m_branchList->SetItem( itemIndex, 2, lastUpdatedString ); + } + + m_branchList->SetColumnWidth( 0, wxLIST_AUTOSIZE ); + m_branchList->SetColumnWidth( 1, wxLIST_AUTOSIZE ); + m_branchList->SetColumnWidth( 2, wxLIST_AUTOSIZE ); + +} + + +void DIALOG_GIT_SWITCH::OnBranchListDClick( wxListEvent& aEvent ) +{ + int selection = aEvent.GetIndex(); + + if( selection != wxNOT_FOUND ) + { + wxString branchName = m_branchList->GetItemText( selection ); + m_branchNameText->SetValue( branchName ); + + if( branchName != m_currentBranch ) + EndModal( wxID_OK ); + } +} + + +void DIALOG_GIT_SWITCH::OnBranchListSelection( wxListEvent& aEvent ) +{ + int selection = aEvent.GetIndex(); + + if( selection != wxNOT_FOUND ) + { + wxString branchName = m_branchList->GetItemText( selection ); + m_branchNameText->SetValue( branchName ); + m_switchButton->SetLabel( _( "Switch" ) ); + m_switchButton->Enable( branchName != m_currentBranch ); + } + else + { + // Deselect all elements in the list + for( int ii = 0; ii < m_branchList->GetItemCount(); ++ii ) + m_branchList->SetItemState( ii, 0, 0 ); + } +} + +void DIALOG_GIT_SWITCH::OnSwitchButton(wxCommandEvent& aEvent) +{ + wxString branchName = m_branchNameText->GetValue(); + + // Check if the branch name exists + bool branchExists = m_branches.count(branchName); + + if (branchExists) + { + EndModal(wxID_OK); // Return Switch code + } + else + { + EndModal(wxID_ADD); // Return Add code + } +} + + +void DIALOG_GIT_SWITCH::OnCancelButton(wxCommandEvent& aEvent) +{ + EndModal(wxID_CANCEL); // Return Cancel code +} + + +wxString DIALOG_GIT_SWITCH::GetBranchName() const +{ + return m_branchNameText->GetValue(); +} + + +void DIALOG_GIT_SWITCH::StartTimer() +{ + m_timer.Start( 500, true ); +} + + +void DIALOG_GIT_SWITCH::StopTimer() +{ + m_timer.Stop(); +} + + +void DIALOG_GIT_SWITCH::OnTimer( wxTimerEvent& aEvt ) +{ + wxString branchName = m_branchNameText->GetValue(); + + if( branchName == m_lastEnteredText ) + return; + + m_lastEnteredText = branchName; + + // Check if the branch name exists + bool branchExists = m_branches.count( branchName ); + + if( branchExists ) + { + m_switchButton->SetLabel( _( "Switch" ) ); + m_switchButton->Enable( branchName != m_currentBranch ); + } + else + { + m_switchButton->SetLabel( _( "Add" ) ); + m_switchButton->Enable(); + } +} + + +void DIALOG_GIT_SWITCH::OnTextChanged( wxCommandEvent& aEvt ) +{ + StartTimer(); +} + + +void DIALOG_GIT_SWITCH::GetBranches() +{ + // Clear the branch list + m_branches.clear(); + + git_branch_iterator* branchIterator = nullptr; + git_branch_t branchType; + + // Get Current Branch + git_reference* currentBranchReference = nullptr; + git_repository_head( ¤tBranchReference, m_repository ); + + // Get the current branch name + if( currentBranchReference ) + { + m_currentBranch = git_reference_shorthand( currentBranchReference ); + git_reference_free( currentBranchReference ); + } + + // Initialize branch iterator + git_branch_iterator_new( &branchIterator, m_repository, GIT_BRANCH_ALL ); + + // Iterate over local branches + git_reference* branchReference = nullptr; + while( git_branch_next( &branchReference, &branchType, branchIterator ) == 0 ) + { + // Get the branch OID + const git_oid* branchOid = git_reference_target( branchReference ); + + // Skip this branch if it doesn't have an OID + if( !branchOid ) + { + git_reference_free( branchReference ); + continue; + } + + git_commit* commit = nullptr; + + if( git_commit_lookup( &commit, m_repository, branchOid ) ) + { + // Skip this branch if it doesn't have a commit + git_reference_free( branchReference ); + continue; + } + + // Retrieve commit details + BranchData branchData; + branchData.commitString = git_commit_message( commit ); + branchData.lastUpdated = static_cast( git_commit_time( commit ) ); + branchData.isRemote = branchType == GIT_BRANCH_REMOTE; + + m_branches[git_reference_shorthand( branchReference )] = branchData; + + git_commit_free( commit ); + git_reference_free( branchReference ); + } + + git_branch_iterator_free( branchIterator ); +} \ No newline at end of file diff --git a/common/dialogs/git/dialog_git_switch.h b/common/dialogs/git/dialog_git_switch.h new file mode 100644 index 0000000000..b1dc22a87c --- /dev/null +++ b/common/dialogs/git/dialog_git_switch.h @@ -0,0 +1,79 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2021-2022 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + + +#ifndef DIALOG_GIT_SWITCH_H +#define DIALOG_GIT_SWITCH_H + +#include +#include + +class wxButton; +class wxListView; +class wxTextCtrl; +class wxTimer; +class wxListEvent; + +struct BranchData +{ + wxString commitString; + time_t lastUpdated; + bool isRemote; +}; + +class DIALOG_GIT_SWITCH : public DIALOG_SHIM +{ +public: + DIALOG_GIT_SWITCH(wxWindow* aParent, git_repository* aRepository); + virtual ~DIALOG_GIT_SWITCH(); + + wxString GetBranchName() const; + +private: + void PopulateBranchList(); + void OnBranchListSelection(wxListEvent& event); + void OnBranchListDClick(wxListEvent& event); + void OnSwitchButton(wxCommandEvent& event); + void OnCancelButton(wxCommandEvent& event); + void OnTextChanged(wxCommandEvent& event); + void OnTimer(wxTimerEvent& event); + void GetBranches(); + + wxListView* m_branchList; + wxTextCtrl* m_branchNameText; + wxButton* m_switchButton; + wxTimer m_timer; + + wxString m_currentBranch; + + git_repository* m_repository; + wxString m_lastEnteredText; + bool m_existingBranch; + + std::map m_branches; + + void StartTimer(); + void StopTimer(); +}; + +#endif // DIALOG_GIT_SWITCH_H diff --git a/common/dialogs/git/panel_git_repos.cpp b/common/dialogs/git/panel_git_repos.cpp new file mode 100644 index 0000000000..2ab0158b9c --- /dev/null +++ b/common/dialogs/git/panel_git_repos.cpp @@ -0,0 +1,377 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2018-2023 KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "panel_git_repos.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + + +PANEL_GIT_REPOS::PANEL_GIT_REPOS( wxWindow* aParent ) : PANEL_GIT_REPOS_BASE( aParent) +{ + + m_btnAddRepo->SetBitmap( KiBitmap( BITMAPS::small_plus ) ); + m_btnEditRepo->SetBitmap( KiBitmap( BITMAPS::small_edit ) ); + m_btnDelete->SetBitmap( KiBitmap( BITMAPS::small_trash ) ); + +} + +PANEL_GIT_REPOS::~PANEL_GIT_REPOS() +{ +} + + +void PANEL_GIT_REPOS::ResetPanel() +{ + m_grid->ClearGrid(); + m_cbDefault->SetValue( true ); + m_author->SetValue( wxEmptyString ); + m_authorEmail->SetValue( wxEmptyString ); +} + +static std::pair getDefaultAuthorEmail() +{ + wxString name; + wxString email; + git_config_entry* name_c = nullptr; + git_config_entry* email_c = nullptr; + + git_config* config = nullptr; + + if( git_config_open_default( &config ) != 0 ) + { + printf( "Failed to open default Git config: %s\n", giterr_last()->message ); + return std::make_pair( name, email ); + } + + if( git_config_get_entry( &name_c, config, "user.name" ) != 0 ) + { + printf( "Failed to get user.name from Git config: %s\n", giterr_last()->message ); + } + if( git_config_get_entry( &email_c, config, "user.email" ) != 0 ) + { + printf( "Failed to get user.email from Git config: %s\n", giterr_last()->message ); + } + + if( name_c ) + name = name_c->value; + + if( email_c ) + email = email_c->value; + + git_config_entry_free( name_c ); + git_config_entry_free( email_c ); + git_config_free( config ); + + return std::make_pair( name, email ); +} + +bool PANEL_GIT_REPOS::TransferDataFromWindow() +{ + COMMON_SETTINGS* settings = Pgm().GetCommonSettings(); + auto& repos = settings->m_Git.repositories; + + repos.clear(); + + for( int row = 0; row < m_grid->GetNumberRows(); row++ ) + { + COMMON_SETTINGS::GIT_REPOSITORY repo; + + repo.active = m_grid->GetCellValue( row, COL_ACTIVE ) == "1"; + repo.name = m_grid->GetCellValue( row, COL_NAME ); + repo.path = m_grid->GetCellValue( row, COL_PATH ); + repo.authType = m_grid->GetCellValue( row, COL_AUTH_TYPE ); + repo.username = m_grid->GetCellValue( row, COL_USERNAME ); + + KIPLATFORM::SECRETS::StoreSecret( repo.path, repo.username, m_grid->GetCellValue( row, COL_PASSWORD ) ); + repo.ssh_path = m_grid->GetCellValue( row, COL_SSH_PATH ); + repos.push_back( repo ); + } + + settings->m_Git.useDefaultAuthor = m_cbDefault->GetValue(); + settings->m_Git.authorName = m_author->GetValue(); + settings->m_Git.authorEmail = m_authorEmail->GetValue(); + + return true; +} + +static bool testRepositoryConnection( COMMON_SETTINGS::GIT_REPOSITORY& repository) +{ + git_libgit2_init(); + + git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; + + typedef struct + { + COMMON_SETTINGS::GIT_REPOSITORY* repo; + bool success; + } callbacksPayload; + + callbacksPayload cb_data( { &repository, true } ); // If we don't need authentication, then, we are successful + callbacks.payload = &cb_data; + callbacks.credentials = [](git_cred** out, const char* url, const char* username, unsigned int allowed_types, void* payload) -> int { + + // If we are asking for credentials, then, we need authentication + + callbacksPayload* data = static_cast(payload); + + data->success = false; + + if( allowed_types & GIT_CREDTYPE_USERNAME ) + { + data->success = true; + } + else if( data->repo->authType == "ssh" && ( allowed_types & GIT_CREDTYPE_SSH_KEY ) ) + { + wxString sshKeyPath = data->repo->ssh_path; + + // Check if the SSH key exists and is readable + if( wxFileExists( sshKeyPath ) && wxFile::Access( sshKeyPath, wxFile::read ) ) + data->success = true; + } + else if( data->repo->authType == "password" ) + { + data->success = ( allowed_types & GIT_CREDTYPE_USERPASS_PLAINTEXT ); + } + + return 0; + }; + + // Create a temporary directory to initialize the Git repository + wxString tempDirPath = wxFileName::CreateTempFileName(wxT("kigit_temp")); + wxMkDir(tempDirPath, wxS_DIR_DEFAULT ); + + // Initialize the Git repository + git_repository* repo = nullptr; + int result = git_repository_init(&repo, tempDirPath.mb_str(wxConvUTF8), 0); + if (result != 0) { + git_repository_free(repo); + git_libgit2_shutdown(); + wxRmdir(tempDirPath); + return false; + } + + git_remote* remote = nullptr; + result = git_remote_create_anonymous(&remote, repo, tempDirPath.mb_str(wxConvUTF8)); + if (result != 0) { + git_remote_free(remote); + git_repository_free(repo); + git_libgit2_shutdown(); + wxRmdir(tempDirPath); + return false; + } + + // We don't really care about the result of this call, the authentication callback + // will set the return values we need + git_remote_connect(remote, GIT_DIRECTION_FETCH, &callbacks, nullptr, nullptr); + + git_remote_disconnect(remote); + git_remote_free(remote); + git_repository_free(repo); + + git_libgit2_shutdown(); + + // Clean up the temporary directory + wxRmdir(tempDirPath); + + return cb_data.success; +} + +bool PANEL_GIT_REPOS::TransferDataToWindow() +{ + COMMON_SETTINGS* settings = Pgm().GetCommonSettings(); + + m_grid->ClearGrid(); + + for( COMMON_SETTINGS::GIT_REPOSITORY& repo : settings->m_Git.repositories ) + { + if( repo.name.IsEmpty() || repo.path.IsEmpty() ) + continue; + + int row = m_grid->GetNumberRows(); + m_grid->AppendRows( 1 ); + + m_grid->SetCellRenderer( row, COL_ACTIVE, new wxGridCellBoolRenderer() ); + m_grid->SetCellEditor( row, COL_ACTIVE, new wxGridCellBoolEditor() ); + m_grid->SetCellValue( row, COL_ACTIVE, repo.active ? "1" : "0" ); + + m_grid->SetCellValue( row, COL_NAME, repo.name ); + m_grid->SetCellValue( row, COL_PATH, repo.path ); + m_grid->SetCellValue( row, COL_AUTH_TYPE, repo.authType ); + m_grid->SetCellValue( row, COL_USERNAME, repo.username ); + + wxString password; + KIPLATFORM::SECRETS::GetSecret( repo.path, repo.username, password ); + m_grid->SetCellValue( row, COL_PASSWORD, password ); + m_grid->SetCellValue( row, COL_SSH_PATH, repo.ssh_path ); + + if( repo.active ) + m_grid->SetCellValue( row, 3, testRepositoryConnection( repo ) ? "C" : "NC" ); + + } + + m_cbDefault->SetValue( settings->m_Git.useDefaultAuthor ); + + if( settings->m_Git.useDefaultAuthor ) + { + auto defaultAuthor = getDefaultAuthorEmail(); + m_author->SetValue( defaultAuthor.first ); + m_authorEmail->SetValue( defaultAuthor.second ); + m_author->Disable(); + m_authorEmail->Disable(); + } + else + { + m_author->SetValue( settings->m_Git.authorName ); + m_authorEmail->SetValue( settings->m_Git.authorEmail ); + } + + return true; +} + +void PANEL_GIT_REPOS::onDefaultClick( wxCommandEvent& event ) +{ + m_author->Enable( !m_cbDefault->GetValue() ); + m_authorEmail->Enable( !m_cbDefault->GetValue() ); + m_authorLabel->Enable( !m_cbDefault->GetValue() ); + m_authorEmailLabel->Enable( !m_cbDefault->GetValue() ); +} + + +void PANEL_GIT_REPOS::onGridDClick( wxGridEvent& event ) +{ + if( m_grid->GetNumberRows() <= 0 ) + { + wxCommandEvent evt; + onAddClick( evt ); + return; + } + + int row = event.GetRow(); + + if( row < 0 || row >= m_grid->GetNumberRows() ) + return; + + DIALOG_GIT_REPOSITORY dialog( this, nullptr ); + + dialog.SetRepoName( m_grid->GetCellValue( row, COL_NAME ) ); + dialog.SetRepoURL( m_grid->GetCellValue( row, COL_PATH ) ); + dialog.SetUsername( m_grid->GetCellValue( row, COL_USERNAME ) ); + dialog.SetRepoSSHPath( m_grid->GetCellValue( row, COL_SSH_PATH ) ); + dialog.SetPassword( m_grid->GetCellValue( row, COL_PASSWORD ) ); + + wxString type = m_grid->GetCellValue( row, COL_AUTH_TYPE ); + + if( type == "password" ) + dialog.SetRepoType( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ); + else if( type == "ssh" ) + dialog.SetRepoType( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ); + else + dialog.SetRepoType( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL); + + if( dialog.ShowModal() == wxID_OK ) + { + m_grid->SetCellValue( row, COL_NAME, dialog.GetRepoName() ); + m_grid->SetCellValue( row, COL_PATH, dialog.GetRepoURL() ); + m_grid->SetCellValue( row, COL_USERNAME, dialog.GetUsername() ); + m_grid->SetCellValue( row, COL_SSH_PATH, dialog.GetRepoSSHPath() ); + m_grid->SetCellValue( row, COL_PASSWORD, dialog.GetPassword() ); + + if( dialog.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) + { + m_grid->SetCellValue( row, COL_AUTH_TYPE, "password" ); + } + else if( dialog.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) + { + m_grid->SetCellValue( row, COL_AUTH_TYPE, "ssh" ); + } + else + { + m_grid->SetCellValue( row, COL_AUTH_TYPE, "none" ); + } + } + +} + + +void PANEL_GIT_REPOS::onAddClick( wxCommandEvent& event ) +{ + + DIALOG_GIT_REPOSITORY dialog( m_parent, nullptr ); + + if( dialog.ShowModal() == wxID_OK ) + { + int row = m_grid->GetNumberRows(); + m_grid->AppendRows( 1 ); + + m_grid->SetCellValue( row, COL_NAME, dialog.GetRepoName() ); + m_grid->SetCellValue( row, COL_PATH, dialog.GetRepoURL() ); + m_grid->SetCellValue( row, COL_USERNAME, dialog.GetUsername() ); + m_grid->SetCellValue( row, COL_SSH_PATH, dialog.GetRepoSSHPath() ); + m_grid->SetCellValue( row, COL_PASSWORD, dialog.GetPassword() ); + + if( dialog.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) + { + m_grid->SetCellValue( row, COL_AUTH_TYPE, "password" ); + } + else if( dialog.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) + { + m_grid->SetCellValue( row, COL_AUTH_TYPE, "ssh" ); + } + else + { + m_grid->SetCellValue( row, COL_AUTH_TYPE, "none" ); + } + + m_grid->MakeCellVisible( row, 0 ); + } +} + + +void PANEL_GIT_REPOS::onEditClick( wxCommandEvent& event ) +{ + wxGridEvent evt( m_grid->GetId(), wxEVT_GRID_CELL_LEFT_DCLICK, m_grid, + m_grid->GetGridCursorRow(), m_grid->GetGridCursorCol() ); + onGridDClick( evt ); +} + + +void PANEL_GIT_REPOS::onDeleteClick( wxCommandEvent& event ) +{ + if( !m_grid->CommitPendingChanges() || m_grid->GetNumberRows() <= 0 ) + return; + + int curRow = m_grid->GetGridCursorRow(); + + m_grid->DeleteRows( curRow ); + + curRow = std::max( 0, curRow - 1 ); + m_grid->MakeCellVisible( curRow, m_grid->GetGridCursorCol() ); + m_grid->SetGridCursor( curRow, m_grid->GetGridCursorCol() ); +} \ No newline at end of file diff --git a/common/dialogs/git/panel_git_repos.h b/common/dialogs/git/panel_git_repos.h new file mode 100644 index 0000000000..7a2501f296 --- /dev/null +++ b/common/dialogs/git/panel_git_repos.h @@ -0,0 +1,59 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2018-2023 KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef PANEL_GIT_REPOS_H +#define PANEL_GIT_REPOS_H + +#include +#include + +class PANEL_GIT_REPOS : public PANEL_GIT_REPOS_BASE +{ +public: + PANEL_GIT_REPOS( wxWindow* parent ); + ~PANEL_GIT_REPOS() override; + + void ResetPanel() override; + + bool TransferDataFromWindow() override; + bool TransferDataToWindow() override; + + enum COLS + { + COL_ACTIVE = 0, + COL_NAME, + COL_PATH, + COL_STATUS, + COL_AUTH_TYPE, + COL_USERNAME, + COL_PASSWORD, + COL_SSH_KEY, + COL_SSH_PATH + }; + +private: + void onDefaultClick( wxCommandEvent& event ) override; + void onGridDClick( wxGridEvent& event ) override; + void onAddClick( wxCommandEvent& event ) override; + void onEditClick( wxCommandEvent& event ) override; + void onDeleteClick( wxCommandEvent& event ) override; + +}; + +#endif // PANEL_GIT_REPOS_H \ No newline at end of file diff --git a/common/dialogs/git/panel_git_repos_base.cpp b/common/dialogs/git/panel_git_repos_base.cpp new file mode 100644 index 0000000000..0640698ef7 --- /dev/null +++ b/common/dialogs/git/panel_git_repos_base.cpp @@ -0,0 +1,167 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-254-gc2ef7767) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#include "widgets/std_bitmap_button.h" +#include "widgets/wx_grid.h" + +#include "panel_git_repos_base.h" + +/////////////////////////////////////////////////////////////////////////// + +PANEL_GIT_REPOS_BASE::PANEL_GIT_REPOS_BASE( wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, const wxString& name ) : RESETTABLE_PANEL( parent, id, pos, size, style, name ) +{ + wxBoxSizer* bPanelSizer; + bPanelSizer = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bLeftSizer; + bLeftSizer = new wxBoxSizer( wxVERTICAL ); + + m_staticText12 = new wxStaticText( this, wxID_ANY, _("Git Commit Data"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText12->Wrap( -1 ); + bLeftSizer->Add( m_staticText12, 0, wxEXPAND|wxLEFT|wxTOP, 10 ); + + wxFlexGridSizer* fgSizer1; + fgSizer1 = new wxFlexGridSizer( 0, 2, 0, 0 ); + fgSizer1->AddGrowableCol( 1 ); + fgSizer1->SetFlexibleDirection( wxBOTH ); + fgSizer1->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_cbDefault = new wxCheckBox( this, wxID_ANY, _("Use default values"), wxDefaultPosition, wxDefaultSize, 0 ); + m_cbDefault->SetValue(true); + fgSizer1->Add( m_cbDefault, 0, wxALL, 5 ); + + + fgSizer1->Add( 0, 0, 0, wxEXPAND, 5 ); + + m_authorLabel = new wxStaticText( this, wxID_ANY, _("Author name:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_authorLabel->Wrap( -1 ); + m_authorLabel->Enable( false ); + + fgSizer1->Add( m_authorLabel, 0, wxALL, 5 ); + + m_author = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + m_author->Enable( false ); + + fgSizer1->Add( m_author, 0, wxALL|wxEXPAND, 5 ); + + m_authorEmailLabel = new wxStaticText( this, wxID_ANY, _("Author e-mail:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_authorEmailLabel->Wrap( -1 ); + m_authorEmailLabel->Enable( false ); + + fgSizer1->Add( m_authorEmailLabel, 0, wxALL, 5 ); + + m_authorEmail = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + m_authorEmail->Enable( false ); + + fgSizer1->Add( m_authorEmail, 0, wxALL|wxEXPAND, 5 ); + + + bLeftSizer->Add( fgSizer1, 1, wxBOTTOM|wxEXPAND|wxLEFT|wxTOP, 13 ); + + m_staticline3 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bLeftSizer->Add( m_staticline3, 0, wxEXPAND|wxBOTTOM, 5 ); + + m_staticText20 = new wxStaticText( this, wxID_ANY, _("Git Repositories"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText20->Wrap( -1 ); + bLeftSizer->Add( m_staticText20, 0, wxEXPAND|wxLEFT|wxRIGHT, 13 ); + + wxBoxSizer* bAntialiasingSizer; + bAntialiasingSizer = new wxBoxSizer( wxVERTICAL ); + + m_grid = new WX_GRID( this, wxID_ANY, wxDefaultPosition, wxSize( 820,200 ), 0 ); + + // Grid + m_grid->CreateGrid( 0, 10 ); + m_grid->EnableEditing( false ); + m_grid->EnableGridLines( true ); + m_grid->EnableDragGridSize( false ); + m_grid->SetMargins( 0, 0 ); + + // Columns + m_grid->SetColSize( 0, 60 ); + m_grid->SetColSize( 1, 200 ); + m_grid->SetColSize( 2, 500 ); + m_grid->SetColSize( 3, 60 ); + m_grid->SetColSize( 4, 0 ); + m_grid->SetColSize( 5, 0 ); + m_grid->SetColSize( 6, 0 ); + m_grid->SetColSize( 7, 0 ); + m_grid->SetColSize( 8, 0 ); + m_grid->SetColSize( 9, 0 ); + m_grid->EnableDragColMove( false ); + m_grid->EnableDragColSize( true ); + m_grid->SetColLabelValue( 0, _("Active") ); + m_grid->SetColLabelValue( 1, _("Name") ); + m_grid->SetColLabelValue( 2, _("Path") ); + m_grid->SetColLabelValue( 3, _("Status") ); + m_grid->SetColLabelSize( 22 ); + m_grid->SetColLabelAlignment( wxALIGN_CENTER, wxALIGN_CENTER ); + + // Rows + m_grid->EnableDragRowSize( true ); + m_grid->SetRowLabelSize( 0 ); + m_grid->SetRowLabelAlignment( wxALIGN_CENTER, wxALIGN_CENTER ); + + // Label Appearance + + // Cell Defaults + m_grid->SetDefaultCellAlignment( wxALIGN_LEFT, wxALIGN_TOP ); + bAntialiasingSizer->Add( m_grid, 5, wxALL|wxEXPAND, 5 ); + + + bLeftSizer->Add( bAntialiasingSizer, 0, wxEXPAND|wxLEFT|wxTOP, 5 ); + + wxBoxSizer* bButtonsSizer; + bButtonsSizer = new wxBoxSizer( wxHORIZONTAL ); + + m_btnAddRepo = new STD_BITMAP_BUTTON( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + m_btnAddRepo->SetToolTip( _("Add new repository") ); + + bButtonsSizer->Add( m_btnAddRepo, 0, wxALL, 5 ); + + m_btnEditRepo = new STD_BITMAP_BUTTON( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + m_btnEditRepo->SetToolTip( _("Edit repository properties") ); + + bButtonsSizer->Add( m_btnEditRepo, 0, wxALL, 5 ); + + + bButtonsSizer->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_btnDelete = new STD_BITMAP_BUTTON( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 ); + m_btnDelete->SetToolTip( _("Remove Git Repository") ); + + bButtonsSizer->Add( m_btnDelete, 0, wxBOTTOM|wxRIGHT|wxTOP, 5 ); + + + bLeftSizer->Add( bButtonsSizer, 1, wxALL|wxEXPAND, 5 ); + + + bPanelSizer->Add( bLeftSizer, 0, wxRIGHT, 20 ); + + + this->SetSizer( bPanelSizer ); + this->Layout(); + bPanelSizer->Fit( this ); + + // Connect Events + m_cbDefault->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( PANEL_GIT_REPOS_BASE::onDefaultClick ), NULL, this ); + m_grid->Connect( wxEVT_GRID_CELL_LEFT_DCLICK, wxGridEventHandler( PANEL_GIT_REPOS_BASE::onGridDClick ), NULL, this ); + m_btnAddRepo->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_GIT_REPOS_BASE::onAddClick ), NULL, this ); + m_btnEditRepo->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_GIT_REPOS_BASE::onEditClick ), NULL, this ); + m_btnDelete->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_GIT_REPOS_BASE::onDeleteClick ), NULL, this ); +} + +PANEL_GIT_REPOS_BASE::~PANEL_GIT_REPOS_BASE() +{ + // Disconnect Events + m_cbDefault->Disconnect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( PANEL_GIT_REPOS_BASE::onDefaultClick ), NULL, this ); + m_grid->Disconnect( wxEVT_GRID_CELL_LEFT_DCLICK, wxGridEventHandler( PANEL_GIT_REPOS_BASE::onGridDClick ), NULL, this ); + m_btnAddRepo->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_GIT_REPOS_BASE::onAddClick ), NULL, this ); + m_btnEditRepo->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_GIT_REPOS_BASE::onEditClick ), NULL, this ); + m_btnDelete->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_GIT_REPOS_BASE::onDeleteClick ), NULL, this ); + +} diff --git a/common/dialogs/git/panel_git_repos_base.h b/common/dialogs/git/panel_git_repos_base.h new file mode 100644 index 0000000000..c6001fdac3 --- /dev/null +++ b/common/dialogs/git/panel_git_repos_base.h @@ -0,0 +1,73 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version 3.10.1-254-gc2ef7767) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +class STD_BITMAP_BUTTON; +class WX_GRID; + +#include "widgets/resettable_panel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +/// Class PANEL_GIT_REPOS_BASE +/////////////////////////////////////////////////////////////////////////////// +class PANEL_GIT_REPOS_BASE : public RESETTABLE_PANEL +{ + private: + + protected: + wxStaticText* m_staticText12; + wxCheckBox* m_cbDefault; + wxStaticText* m_authorLabel; + wxTextCtrl* m_author; + wxStaticText* m_authorEmailLabel; + wxTextCtrl* m_authorEmail; + wxStaticLine* m_staticline3; + wxStaticText* m_staticText20; + WX_GRID* m_grid; + STD_BITMAP_BUTTON* m_btnAddRepo; + STD_BITMAP_BUTTON* m_btnEditRepo; + STD_BITMAP_BUTTON* m_btnDelete; + + // Virtual event handlers, override them in your derived class + virtual void onDefaultClick( wxCommandEvent& event ) { event.Skip(); } + virtual void onGridDClick( wxGridEvent& event ) { event.Skip(); } + virtual void onAddClick( wxCommandEvent& event ) { event.Skip(); } + virtual void onEditClick( wxCommandEvent& event ) { event.Skip(); } + virtual void onDeleteClick( wxCommandEvent& event ) { event.Skip(); } + + + public: + + PANEL_GIT_REPOS_BASE( wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1,-1 ), long style = wxTAB_TRAVERSAL, const wxString& name = wxEmptyString ); + + ~PANEL_GIT_REPOS_BASE(); + +}; + diff --git a/common/dialogs/git/panel_git_repositories_base.fbp b/common/dialogs/git/panel_git_repositories_base.fbp new file mode 100644 index 0000000000..da03f75ab2 --- /dev/null +++ b/common/dialogs/git/panel_git_repositories_base.fbp @@ -0,0 +1,953 @@ + + + + + + C++ + 1 + source_name + 0 + 0 + res + UTF-8 + connect + panel_git_repos_base + 1000 + none + + + 1 + PanelGitRepos + + . + + 1 + 1 + 1 + 1 + UI + 0 + 1 + 0 + + 0 + wxAUI_MGR_DEFAULT + + + 1 + 0 + 1 + impl_virtual + + + 0 + wxID_ANY + + + PANEL_GIT_REPOS_BASE + + -1,-1 + RESETTABLE_PANEL; widgets/resettable_panel.h; Not forward_declare + + 0 + + + wxTAB_TRAVERSAL + + + bPanelSizer + wxHORIZONTAL + none + + 20 + wxRIGHT + 0 + + + bLeftSizer + wxVERTICAL + none + + 10 + wxEXPAND|wxLEFT|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Git Commit Data + 0 + + 0 + + + 0 + + 1 + m_staticText12 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 13 + wxBOTTOM|wxEXPAND|wxLEFT|wxTOP + 1 + + 2 + wxBOTH + 1 + + 0 + + fgSizer1 + wxFLEX_GROWMODE_SPECIFIED + none + 0 + 0 + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Use default values + + 0 + + + 0 + + 1 + m_cbDefault + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + onDefaultClick + + + + 5 + wxEXPAND + 0 + + 0 + protected + 0 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 0 + + 1 + + 0 + 0 + wxID_ANY + Author name: + 0 + + 0 + + + 0 + + 1 + m_authorLabel + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 0 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_author + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 0 + + 1 + + 0 + 0 + wxID_ANY + Author e-mail: + 0 + + 0 + + + 0 + + 1 + m_authorEmailLabel + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 0 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + m_authorEmail + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + + + 5 + wxEXPAND|wxBOTTOM + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_staticline3 + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + + + 13 + wxEXPAND|wxLEFT|wxRIGHT + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Git Repositories + 0 + + 0 + + + 0 + + 1 + m_staticText20 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND|wxLEFT|wxTOP + 0 + + + bAntialiasingSizer + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 5 + + 1 + 1 + 1 + 1 + + + + + 0 + 0 + + + + 1 + + + wxALIGN_LEFT + + wxALIGN_TOP + 0 + 1 + wxALIGN_CENTER + 22 + "Active" "Name" "Path" "Status" + wxALIGN_CENTER + 10 + 60,200,500,60,0,0,0,0,0,0 + + 1 + 0 + Dock + 0 + Left + 0 + 0 + 1 + 0 + 1 + 0 + 1 + + 1 + + + 1 + 0 + 0 + wxID_ANY + + + + 0 + 0 + + 0 + + + 0 + + 1 + m_grid + 1 + + + protected + 1 + + Resizable + wxALIGN_CENTER + 0 + + wxALIGN_CENTER + + 0 + 1 + 820,200 + WX_GRID; widgets/wx_grid.h; forward_declare + 0 + + + + + onGridDClick + + + + + + 5 + wxALL|wxEXPAND + 1 + + + bButtonsSizer + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + MyButton + + 0 + + 0 + + + 0 + + 1 + m_btnAddRepo + 1 + + + protected + 1 + + + + Resizable + 1 + + + STD_BITMAP_BUTTON; widgets/std_bitmap_button.h; forward_declare + 0 + Add new repository + + wxFILTER_NONE + wxDefaultValidator + + + + + onAddClick + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + MyButton + + 0 + + 0 + + + 0 + + 1 + m_btnEditRepo + 1 + + + protected + 1 + + + + Resizable + 1 + + + STD_BITMAP_BUTTON; widgets/std_bitmap_button.h; forward_declare + 0 + Edit repository properties + + wxFILTER_NONE + wxDefaultValidator + + + + + onEditClick + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 5 + wxBOTTOM|wxRIGHT|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + MyButton + + 0 + + 0 + + + 0 + + 1 + m_btnDelete + 1 + + + protected + 1 + + + + Resizable + 1 + + + STD_BITMAP_BUTTON; widgets/std_bitmap_button.h; forward_declare + 0 + Remove Git Repository + + wxFILTER_NONE + wxDefaultValidator + + + + + onDeleteClick + + + + + + + + + + diff --git a/common/eda_base_frame.cpp b/common/eda_base_frame.cpp index ff9443f155..37212b08d2 100644 --- a/common/eda_base_frame.cpp +++ b/common/eda_base_frame.cpp @@ -23,9 +23,11 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ +#include #include #include #include +#include #include #include #include @@ -1071,6 +1073,15 @@ void EDA_BASE_FRAME::ShowPreferences( wxString aStartPage, wxString aStartParent book->AddPage( hotkeysPanel, _( "Hotkeys" ) ); +// This currently allows pre-defined repositories that we +// don't use, so keep it disabled at the moment +if( ADVANCED_CFG::GetCfg().m_EnableGit && false ) + book->AddLazyPage( + []( wxWindow* aParent ) -> wxWindow* + { + return new PANEL_GIT_REPOS( aParent ); + }, _( "Version Control" ) ); + #ifdef KICAD_USE_SENTRY book->AddLazyPage( []( wxWindow* aParent ) -> wxWindow* diff --git a/common/eda_shape.cpp b/common/eda_shape.cpp index b7519a6358..5c2149f2b2 100644 --- a/common/eda_shape.cpp +++ b/common/eda_shape.cpp @@ -1721,6 +1721,129 @@ PLOT_DASH_TYPE EDA_SHAPE::GetLineStyle() const } +bool EDA_SHAPE::operator==( const EDA_SHAPE& aOther ) const +{ + if( GetShape() != aOther.GetShape() ) + return false; + + if( m_fill != aOther.m_fill ) + return false; + + if( m_stroke.GetWidth() != aOther.m_stroke.GetWidth() ) + return false; + + if( m_stroke.GetPlotStyle() != aOther.m_stroke.GetPlotStyle() ) + return false; + + if( m_fillColor != aOther.m_fillColor ) + return false; + + if( m_start != aOther.m_start ) + return false; + + if( m_end != aOther.m_end ) + return false; + + if( m_arcCenter != aOther.m_arcCenter ) + return false; + + if( m_bezierC1 != aOther.m_bezierC1 ) + return false; + + if( m_bezierC2 != aOther.m_bezierC2 ) + return false; + + if( m_bezierPoints != aOther.m_bezierPoints ) + return false; + + for( int ii = 0; ii < m_poly.TotalVertices(); ++ii ) + { + if( m_poly.CVertex( ii ) != aOther.m_poly.CVertex( ii ) ) + return false; + } + + return true; +} + + +double EDA_SHAPE::Similarity( const EDA_SHAPE& aOther ) const +{ + if( GetShape() != aOther.GetShape() ) + return 0.0; + + double similarity = 1.0; + + if( m_fill != aOther.m_fill ) + similarity *= 0.9; + + if( m_stroke.GetWidth() != aOther.m_stroke.GetWidth() ) + similarity *= 0.9; + + if( m_stroke.GetPlotStyle() != aOther.m_stroke.GetPlotStyle() ) + similarity *= 0.9; + + if( m_fillColor != aOther.m_fillColor ) + similarity *= 0.9; + + if( m_start != aOther.m_start ) + similarity *= 0.9; + + if( m_end != aOther.m_end ) + similarity *= 0.9; + + if( m_arcCenter != aOther.m_arcCenter ) + similarity *= 0.9; + + if( m_bezierC1 != aOther.m_bezierC1 ) + similarity *= 0.9; + + if( m_bezierC2 != aOther.m_bezierC2 ) + similarity *= 0.9; + + { + int m = m_bezierPoints.size(); + int n = aOther.m_bezierPoints.size(); + + size_t longest = alg::longest_common_subset( m_bezierPoints, aOther.m_bezierPoints ); + + similarity *= std::pow( 0.9, m + n - 2 * longest ); + } + + { + int m = m_poly.TotalVertices(); + int n = aOther.m_poly.TotalVertices(); + std::vector poly; + std::vector otherPoly; + VECTOR2I lastPt( 0, 0 ); + + // We look for the longest common subset of the two polygons, but we need to + // offset each point because we're actually looking for overall similarity, not just + // exact matches. So if the zone is moved by 1IU, we only want one point to be + // considered "moved" rather than the entire polygon. In this case, the first point + // will not be a match but the rest of the sequence will. + for( int ii = 0; ii < m; ++ii ) + { + poly.emplace_back( lastPt - m_poly.CVertex( ii ) ); + lastPt = m_poly.CVertex( ii ); + } + + lastPt = VECTOR2I( 0, 0 ); + + for( int ii = 0; ii < n; ++ii ) + { + otherPoly.emplace_back( lastPt - aOther.m_poly.CVertex( ii ) ); + lastPt = aOther.m_poly.CVertex( ii ); + } + + size_t longest = alg::longest_common_subset( poly, otherPoly ); + + similarity *= std::pow( 0.9, m + n - 2 * longest ); + } + + return similarity; +} + + IMPLEMENT_ENUM_TO_WXANY( SHAPE_T ) IMPLEMENT_ENUM_TO_WXANY( PLOT_DASH_TYPE ) diff --git a/common/eda_text.cpp b/common/eda_text.cpp index f1d3be7d7b..34df1ed6c7 100644 --- a/common/eda_text.cpp +++ b/common/eda_text.cpp @@ -1025,6 +1025,67 @@ bool EDA_TEXT::ValidateHyperlink( const wxString& aURL ) return false; } +double EDA_TEXT::Levenshtein( const EDA_TEXT& aOther ) const +{ + // Compute the Levenshtein distance between the two strings + const wxString& str1 = GetText(); + const wxString& str2 = aOther.GetText(); + + int m = str1.length(); + int n = str2.length(); + + if( n == 0 || m == 0 ) + return 0.0; + + // Create a matrix to store the distance values + std::vector> distance(m + 1, std::vector(n + 1)); + + // Initialize the matrix + for( int i = 0; i <= m; i++ ) + distance[i][0] = i; + for( int j = 0; j <= n; j++ ) + distance[0][j] = j; + + // Calculate the distance + for( int i = 1; i <= m; i++ ) + { + for( int j = 1; j <= n; j++ ) + { + if( str1[i - 1] == str2[j - 1] ) + { + distance[i][j] = distance[i - 1][j - 1]; + } + else + { + distance[i][j] = std::min( { distance[i - 1][j], distance[i][j - 1], + distance[i - 1][j - 1] } ) + 1; + } + } + } + + // Calculate similarity score + int maxLen = std::max( m, n ); + double similarity = 1.0 - ( static_cast( distance[m][n] ) / maxLen ); + + return similarity; +} + + +double EDA_TEXT::Similarity( const EDA_TEXT& aOther ) const +{ + double retval = 1.0; + + if( !( m_attributes == aOther.m_attributes ) ) + retval *= 0.9; + + if( m_pos != aOther.m_pos ) + retval *= 0.9; + + retval *= Levenshtein( aOther ); + + return retval; +} + bool EDA_TEXT::IsGotoPageHref( const wxString& aHref, wxString* aDestination ) { diff --git a/common/gestfich.cpp b/common/gestfich.cpp index 6861845d4e..4dfec20206 100644 --- a/common/gestfich.cpp +++ b/common/gestfich.cpp @@ -40,6 +40,8 @@ #include #include "wx/tokenzr.h" +#include + void QuoteString( wxString& string ) { if( !string.StartsWith( wxT( "\"" ) ) ) @@ -292,3 +294,51 @@ wxString QuoteFullPath( wxFileName& fn, wxPathFormat format ) { return wxT( "\"" ) + fn.GetFullPath( format ) + wxT( "\"" ); } + + +bool RmDirRecursive( const wxString& aFileName, wxString* aErrors ) +{ + namespace fs = std::filesystem; + + std::string rmDir = aFileName.ToStdString(); + + if( rmDir.length() < 3 ) + { + if( aErrors ) + *aErrors = _( "Invalid directory name, cannot remove root" ); + + return false; + } + + if( !fs::exists( rmDir ) ) + { + if( aErrors ) + *aErrors = wxString::Format( _( "Directory '%s' does not exist" ), aFileName ); + + return false; + } + + fs::path path( rmDir ); + + if( !fs::is_directory( path ) ) + { + if( aErrors ) + *aErrors = wxString::Format( _( "'%s' is not a directory" ), aFileName ); + + return false; + } + + try + { + fs::remove_all( path ); + } + catch( const fs::filesystem_error& e ) + { + if( aErrors ) + *aErrors = wxString::Format( _( "Error removing directory '%s': %s" ), aFileName, e.what() ); + + return false; + } + + return true; +} \ No newline at end of file diff --git a/common/git/git_add_to_index_handler.cpp b/common/git/git_add_to_index_handler.cpp new file mode 100644 index 0000000000..daf44758b5 --- /dev/null +++ b/common/git/git_add_to_index_handler.cpp @@ -0,0 +1,108 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_add_to_index_handler.h" + + +#include +#include + +GIT_ADD_TO_INDEX_HANDLER::GIT_ADD_TO_INDEX_HANDLER( git_repository* aRepository ) +{ + m_repository = aRepository; + m_filesToAdd.clear(); +} + +GIT_ADD_TO_INDEX_HANDLER::~GIT_ADD_TO_INDEX_HANDLER() +{ +} + + +bool GIT_ADD_TO_INDEX_HANDLER::AddToIndex( const wxString& aFilePath ) +{ + // Test if file is currently in the index + + git_index* index = nullptr; + size_t at_pos = 0; + + if( git_repository_index( &index, m_repository ) != 0 ) + { + wxLogError( "Failed to get repository index" ); + return false; + } + + git_index_find( &at_pos, index, aFilePath.ToUTF8().data() ); + + if( at_pos >= 0) + { + git_index_free( index ); + wxLogError( "%s already in index", aFilePath ); + return false; + } + + git_index_free( index ); + + // Add file to index if not already there + m_filesToAdd.push_back( aFilePath ); + + return true; +} + + +bool GIT_ADD_TO_INDEX_HANDLER::PerformAddToIndex() +{ + git_index* index = nullptr; + + m_filesFailedToAdd.clear(); + + if( git_repository_index( &index, m_repository ) != 0 ) + { + wxLogError( "Failed to get repository index" ); + std::copy( m_filesToAdd.begin(), m_filesToAdd.end(), std::back_inserter( m_filesFailedToAdd ) ); + return false; + } + + for( auto& file : m_filesToAdd ) + { + if( git_index_add_bypath( index, file.ToUTF8().data() ) != 0 ) + { + wxLogError( "Failed to add %s to index", file ); + m_filesFailedToAdd.push_back( file ); + continue; + } + } + + + if( git_index_write( index ) != 0 ) + { + wxLogError( "Failed to write index" ); + m_filesFailedToAdd.clear(); + std::copy( m_filesToAdd.begin(), m_filesToAdd.end(), std::back_inserter( m_filesFailedToAdd ) ); + git_index_free( index ); + return false; + } + + git_index_free( index ); + + return true; +} \ No newline at end of file diff --git a/common/git/git_add_to_index_handler.h b/common/git/git_add_to_index_handler.h new file mode 100644 index 0000000000..68c0983d2a --- /dev/null +++ b/common/git/git_add_to_index_handler.h @@ -0,0 +1,50 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_ADD_TO_INDEX_HANDLER_H_ +#define GIT_ADD_TO_INDEX_HANDLER_H_ + +#include +#include + +class wxString; + +class GIT_ADD_TO_INDEX_HANDLER +{ +public: + GIT_ADD_TO_INDEX_HANDLER( git_repository* aRepository ); + virtual ~GIT_ADD_TO_INDEX_HANDLER(); + + bool AddToIndex( const wxString& aFilePath ); + + bool PerformAddToIndex(); + +private: + + git_repository* m_repository; + + std::vector m_filesToAdd; + std::vector m_filesFailedToAdd; + }; + +#endif /* GIT_ADD_TO_INDEX_HANDLER_H_ */ \ No newline at end of file diff --git a/common/git/git_clone_handler.cpp b/common/git/git_clone_handler.cpp new file mode 100644 index 0000000000..a466b69181 --- /dev/null +++ b/common/git/git_clone_handler.cpp @@ -0,0 +1,82 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_clone_handler.h" + +#include + +#include +#include + +GIT_CLONE_HANDLER::GIT_CLONE_HANDLER() : KIGIT_COMMON( nullptr ) +{} + +GIT_CLONE_HANDLER::~GIT_CLONE_HANDLER() +{ + if( m_repo ) + git_repository_free( m_repo ); +} + + +bool GIT_CLONE_HANDLER::PerformClone() +{ + wxFileName clonePath( m_clonePath ); + + if( !clonePath.DirExists() ) + { + if( !clonePath.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + AddErrorString( wxString::Format( _( "Could not create directory '%s'" ), m_clonePath ) ); + return false; + } + } + + git_clone_options cloneOptions = GIT_CLONE_OPTIONS_INIT; + cloneOptions.checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; + cloneOptions.checkout_opts.progress_cb = clone_progress_cb; + cloneOptions.checkout_opts.progress_payload = this; + cloneOptions.fetch_opts.callbacks.transfer_progress = transfer_progress_cb; + cloneOptions.fetch_opts.callbacks.credentials = credentials_cb; + cloneOptions.fetch_opts.callbacks.payload = this; + + m_testedTypes = 0; + + if( git_clone( &m_repo, m_URL.ToStdString().c_str(), m_clonePath.ToStdString().c_str(), &cloneOptions ) != 0 ) + { + AddErrorString( wxString::Format( _( "Could not clone repository '%s'" ), m_URL ) ); + return false; + } + + if( m_progressReporter ) + m_progressReporter->Hide(); + + m_previousProgress = 0; + + return true; +} + + +void GIT_CLONE_HANDLER::UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) +{ + ReportProgress( aCurrent, aTotal, aMessage ); +} diff --git a/common/git/git_clone_handler.h b/common/git/git_clone_handler.h new file mode 100644 index 0000000000..b90c78f7b5 --- /dev/null +++ b/common/git/git_clone_handler.h @@ -0,0 +1,56 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_CLONE_HANDLER_H_ +#define GIT_CLONE_HANDLER_H_ + +#include +#include +class GIT_CLONE_HANDLER : public KIGIT_COMMON, public GIT_PROGRESS +{ +public: + GIT_CLONE_HANDLER(); + ~GIT_CLONE_HANDLER(); + + bool PerformClone(); + + void SetURL( const wxString& aURL ) { m_URL = aURL; } + wxString GetURL() const { return m_URL; } + + void SetBranch( const wxString& aBranch ) { m_branch = aBranch; } + wxString GetBranch() const { return m_branch; } + + void SetClonePath( const wxString& aPath ) { m_clonePath = aPath; } + wxString GetClonePath() const { return m_clonePath; } + + void UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) override; + +private: + + wxString m_URL; + wxString m_branch; + wxString m_clonePath; +}; + + +#endif \ No newline at end of file diff --git a/common/git/git_commit_handler.cpp b/common/git/git_commit_handler.cpp new file mode 100644 index 0000000000..c5b413757a --- /dev/null +++ b/common/git/git_commit_handler.cpp @@ -0,0 +1,49 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_commit_handler.h" + +GIT_COMMIT_HANDLER::GIT_COMMIT_HANDLER( git_repository* aRepo ) : + KIGIT_COMMON( aRepo ) +{} + +GIT_COMMIT_HANDLER::~GIT_COMMIT_HANDLER() +{} + + +GIT_COMMIT_HANDLER::CommitResult GIT_COMMIT_HANDLER::PerformCommit( const std::vector& aFilesToCommit ) +{ + + return CommitResult::Success; +} + +std::string GIT_COMMIT_HANDLER::GetErrorString() const +{ + return m_errorString; +} + +void GIT_COMMIT_HANDLER::AddErrorString( const std::string& aErrorString ) +{ + m_errorString += aErrorString; +} + diff --git a/common/git/git_commit_handler.h b/common/git/git_commit_handler.h new file mode 100644 index 0000000000..ead3484f6e --- /dev/null +++ b/common/git/git_commit_handler.h @@ -0,0 +1,58 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_COMMIT_HANDLER_H +#define GIT_COMMIT_HANDLER_H + +// Define a class to handle git commit operations + +#include +#include + +#include +#include + +class GIT_COMMIT_HANDLER : public KIGIT_COMMON +{ +public: + GIT_COMMIT_HANDLER( git_repository* aRepo ); + virtual ~GIT_COMMIT_HANDLER(); + + enum class CommitResult + { + Success, + Error, + Cancelled + }; + + CommitResult PerformCommit( const std::vector& aFilesToCommit ); + + std::string GetErrorString() const; + +private: + void AddErrorString( const std::string& aErrorString ); + + std::string m_errorString; +}; + +#endif // GIT_COMMIT_HANDLER_H \ No newline at end of file diff --git a/common/git/git_compare_handler.cpp b/common/git/git_compare_handler.cpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/git/git_compare_handler.h b/common/git/git_compare_handler.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/git/git_progress.h b/common/git/git_progress.h new file mode 100644 index 0000000000..b75528e3a4 --- /dev/null +++ b/common/git/git_progress.h @@ -0,0 +1,72 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_PROGRESS_H_ +#define GIT_PROGRESS_H_ + +#include + +#include + +class GIT_PROGRESS +{ +public: + GIT_PROGRESS() : m_previousProgress( 0 ) + { + m_progressReporter.reset(); + } + + + void SetProgressReporter( std::unique_ptr aProgressReporter ) + { + m_progressReporter = std::move( aProgressReporter ); + } + + void ReportProgress( int aCurrent, int aTotal, const wxString& aMessage ) + { + + if( m_progressReporter ) + { + if( aCurrent == m_previousProgress || aTotal == 0 ) + { + m_progressReporter->Pulse( aMessage ); + } + else + { + m_progressReporter->SetCurrentProgress( static_cast( aCurrent ) / aTotal ); + m_progressReporter->Report( aMessage ); + } + + m_previousProgress = aCurrent; + m_progressReporter->KeepRefreshing(); + } + } + + +protected: + int m_previousProgress; + + std::unique_ptr m_progressReporter; +}; + +#endif // GIT_PROGRESS_H__ \ No newline at end of file diff --git a/common/git/git_pull_handler.cpp b/common/git/git_pull_handler.cpp new file mode 100644 index 0000000000..d72ffd589c --- /dev/null +++ b/common/git/git_pull_handler.cpp @@ -0,0 +1,343 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_pull_handler.h" +#include + +#include + +GIT_PULL_HANDLER::GIT_PULL_HANDLER( git_repository* aRepo ) : KIGIT_COMMON( aRepo ) +{} + +GIT_PULL_HANDLER::~GIT_PULL_HANDLER() +{} + + +bool GIT_PULL_HANDLER::PerformFetch() +{ + // Fetch updates from remote repository + git_remote* remote = nullptr; + + if( git_remote_lookup( &remote, m_repo, "origin" ) != 0 ) + { + AddErrorString( wxString::Format( _( "Could not lookup remote '%s'" ), "origin" ).ToStdString() ); + return false; + } + + git_remote_callbacks remoteCallbacks = GIT_REMOTE_CALLBACKS_INIT; + remoteCallbacks.sideband_progress = progress_cb; + remoteCallbacks.transfer_progress = transfer_progress_cb; + remoteCallbacks.credentials = credentials_cb; + remoteCallbacks.payload = this; + + if( git_remote_connect( remote, GIT_DIRECTION_FETCH, &remoteCallbacks, nullptr, nullptr ) ) + { + git_remote_free( remote ); + AddErrorString( wxString::Format( _( "Could not connect to remote '%s'" ), "origin" ).ToStdString() ); + return false; + } + + git_fetch_options fetchOptions = GIT_FETCH_OPTIONS_INIT; + fetchOptions.callbacks = remoteCallbacks; + + if( git_remote_fetch( remote, nullptr, &fetchOptions, nullptr ) ) + { + git_remote_free( remote ); + AddErrorString( wxString::Format( _( "Could not fetch data from remote '%s'" ), "origin" ) ); + return false; + } + + git_remote_free( remote ); + + return true; +} + + +PullResult GIT_PULL_HANDLER::PerformPull() +{ + PullResult result = PullResult::Success; + + if( !PerformFetch() ) + return PullResult::Error; + + git_oid pull_merge_oid = {}; + + if( git_repository_fetchhead_foreach( m_repo, fetchhead_foreach_cb, &pull_merge_oid ) ) + { + AddErrorString( _( "Could not read 'FETCH_HEAD'" ) ); + return PullResult::Error; + } + + git_annotated_commit* fetchhead_commit; + + if( git_annotated_commit_lookup( &fetchhead_commit, m_repo, &pull_merge_oid ) ) + { + AddErrorString( _( "Could not lookup commit" ) ); + return PullResult::Error; + } + + const git_annotated_commit* merge_commits[] = { fetchhead_commit }; + git_merge_analysis_t merge_analysis; + git_merge_preference_t merge_preference = GIT_MERGE_PREFERENCE_NONE; + + if( git_merge_analysis( &merge_analysis, &merge_preference, m_repo, merge_commits, 1 ) ) + { + AddErrorString( _( "Could not analyze merge" ) ); + git_annotated_commit_free( fetchhead_commit ); + return PullResult::Error; + } + + if( !( merge_analysis & GIT_MERGE_ANALYSIS_NORMAL ) ) + git_annotated_commit_free( fetchhead_commit ); + + if( merge_analysis & GIT_MERGE_ANALYSIS_UNBORN ) + { + AddErrorString( _( "Invalid HEAD. Cannot merge." ) ); + return PullResult::MergeFailed; + } + + // Nothing to do if the repository is up to date + if( merge_analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE ) + { + git_repository_state_cleanup( m_repo ); + return PullResult::UpToDate; + } + + // Fast-forward is easy, just update the local reference + if( merge_analysis & GIT_MERGE_ANALYSIS_FASTFORWARD ) + { + return handleFastForward(); + } + + if( merge_analysis & GIT_MERGE_ANALYSIS_NORMAL ) + { + PullResult ret = handleMerge( merge_commits, 1 ); + git_annotated_commit_free( fetchhead_commit ); + return ret; + } + + //TODO: handle merges when they need to be resolved + + return result; +} + +const std::vector>>& GIT_PULL_HANDLER::GetFetchResults() const { + return m_fetchResults; +} + +std::string GIT_PULL_HANDLER::getFirstLineFromCommitMessage( const std::string& aMessage ) +{ + size_t firstLineEnd = aMessage.find_first_of( '\n' ); + + if( firstLineEnd != std::string::npos ) + return aMessage.substr( 0, firstLineEnd ); + + return aMessage; +} + +std::string GIT_PULL_HANDLER::getFormattedCommitDate( const git_time& aTime ) +{ + char dateBuffer[64]; + strftime( dateBuffer, sizeof( dateBuffer ), "%Y-%b-%d %H:%M:%S", gmtime( &aTime.time ) ); + return dateBuffer; +} + + +PullResult GIT_PULL_HANDLER::handleFastForward() +{ + // Update local references with fetched data + git_reference* updatedRef = nullptr; + + if( git_repository_head( &updatedRef, m_repo ) ) + { + AddErrorString( _( "Could not get repository head" ) ); + return PullResult::Error; + } + + const char* updatedRefName = git_reference_name( updatedRef ); + git_reference_free( updatedRef ); + + git_oid updatedRefOid; + if( git_reference_name_to_id( &updatedRefOid, m_repo, updatedRefName ) ) + { + AddErrorString( wxString::Format( _( "Could not get reference OID for reference '%s'" ), updatedRefName ) ); + return PullResult::Error; + } + + git_checkout_options checkoutOptions = GIT_CHECKOUT_OPTIONS_INIT; + checkoutOptions.checkout_strategy = GIT_CHECKOUT_SAFE; + if( git_checkout_head( m_repo, &checkoutOptions ) ) + { + AddErrorString( _( "Failed to perform checkout operation." ) ); + return PullResult::Error; + } + + // Collect commit details for updated references + git_revwalk* revWalker = nullptr; + git_revwalk_new( &revWalker, m_repo ); + git_revwalk_sorting( revWalker, GIT_SORT_TIME ); + git_revwalk_push_glob( revWalker, updatedRefName ); + + git_oid commitOid; + while( git_revwalk_next( &commitOid, revWalker ) == 0 ) + { + git_commit* commit = nullptr; + + if( git_commit_lookup( &commit, m_repo, &commitOid ) ) + { + AddErrorString( wxString::Format( _( "Could not lookup commit '{}'" ), git_oid_tostr_s( &commitOid ) ) ); + git_revwalk_free( revWalker ); + return PullResult::Error; + } + + CommitDetails details; + details.m_sha = git_oid_tostr_s( &commitOid ); + details.m_firstLine = getFirstLineFromCommitMessage( git_commit_message( commit ) ); + details.m_author = git_commit_author( commit )->name; + details.m_date = getFormattedCommitDate( git_commit_author( commit )->when ); + + std::pair>& branchCommits = + m_fetchResults.emplace_back(); + branchCommits.first = updatedRefName; + branchCommits.second.push_back( details ); + + //TODO: log these to the parent + git_commit_free( commit ); + } + + git_revwalk_free( revWalker ); + + git_repository_state_cleanup( m_repo ); + return PullResult::FastForward; +} + + +PullResult GIT_PULL_HANDLER::handleMerge( const git_annotated_commit** aMergeHeads, + size_t aMergeHeadsCount ) +{ + git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT; + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; + + if( git_merge( m_repo, aMergeHeads, aMergeHeadsCount, &merge_opts, &checkout_opts ) ) + { + AddErrorString( _( "Could not merge commits" ) ); + return PullResult::Error; + } + + // Get the repository index + git_index* index; + if( git_repository_index( &index, m_repo ) ) + { + AddErrorString( _( "Could not get repository index" ) ); + return PullResult::Error; + } + + // Check for conflicts + git_index_conflict_iterator* conflicts; + if( git_index_conflict_iterator_new( &conflicts, index ) ) + { + AddErrorString( _( "Could not get conflict iterator" ) ); + return PullResult::Error; + } + + const git_index_entry* ancestor; + const git_index_entry* our; + const git_index_entry* their; + std::vector conflict_data; + + while( git_index_conflict_next( &ancestor, &our, &their, conflicts ) == 0 ) + { + // Case 3: Both files have changed + if( ancestor && our && their ) + { + ConflictData conflict_datum; + conflict_datum.filename = our->path; + conflict_datum.our_oid = our->id; + conflict_datum.their_oid = their->id; + conflict_datum.our_commit_time = our->mtime.seconds; + conflict_datum.their_commit_time = their->mtime.seconds; + conflict_datum.our_status = _( "Changed" ); + conflict_datum.their_status = _( "Changed" ); + + conflict_data.push_back( conflict_datum ); + } + // Case 4: File added in both ours and theirs + else if( !ancestor && our && their ) + { + ConflictData conflict_datum; + conflict_datum.filename = our->path; + conflict_datum.our_oid = our->id; + conflict_datum.their_oid = their->id; + conflict_datum.our_commit_time = our->mtime.seconds; + conflict_datum.their_commit_time = their->mtime.seconds; + conflict_datum.our_status = _( "Added" ); + conflict_datum.their_status = _( "Added" ); + + conflict_data.push_back( conflict_datum ); + } + // Case 1: Remote file has changed or been added, local file has not + else if( their && !our ) + { + // Accept their changes + git_index_add( index, their ); + } + // Case 2: Local file has changed or been added, remote file has not + else if( our && !their ) + { + // Accept our changes + git_index_add( index, our ); + } + else + { + ConflictData conflict_datum; + conflict_datum.filename = our->path; + conflict_datum.our_oid = our->id; + conflict_datum.their_oid = their->id; + conflict_datum.our_commit_time = our->mtime.seconds; + conflict_datum.their_commit_time = their->mtime.seconds; + conflict_datum.our_status = _( "Other" ); + conflict_datum.their_status = _( "Other" ); + + conflict_data.push_back( conflict_datum ); + } + } + + if( conflict_data.empty() ) + { + git_index_conflict_cleanup( index ); + git_index_write( index ); + } + + git_index_conflict_iterator_free( conflicts ); + git_index_free( index ); + + return conflict_data.empty() ? PullResult::Success : PullResult::MergeFailed; +} + + +void GIT_PULL_HANDLER::UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) +{ + ReportProgress( aCurrent, aTotal, aMessage ); +} diff --git a/common/git/git_pull_handler.h b/common/git/git_pull_handler.h new file mode 100644 index 0000000000..0486340021 --- /dev/null +++ b/common/git/git_pull_handler.h @@ -0,0 +1,100 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GITPULLHANDLER_HPP +#define GITPULLHANDLER_HPP + +#include +#include +#include +#include + +#include "kicad_git_common.h" + +#include + +// Structure to store commit details +struct CommitDetails +{ + std::string m_sha; + std::string m_firstLine; + std::string m_author; + std::string m_date; +}; + +// Enum for result codes +enum class PullResult +{ + Success, + Error, + UpToDate, + FastForward, + MergeFailed +}; + +struct ConflictData +{ + std::string filename; + std::string our_status; + std::string their_status; + git_oid our_oid; + git_oid their_oid; + git_time_t our_commit_time; + git_time_t their_commit_time; + bool use_ours; // Flag indicating user's choice (true = ours, false = theirs) +}; + + +class GIT_PULL_HANDLER : public KIGIT_COMMON, public GIT_PROGRESS +{ +public: + GIT_PULL_HANDLER( git_repository* aRepo ); + ~GIT_PULL_HANDLER(); + + PullResult PerformPull(); + + bool PerformFetch(); + + const std::vector>>& GetFetchResults() const; + + // Set the callback function for conflict resolution + void SetConflictCallback( + std::function& aConflicts )> aCallback ) + { + m_conflictCallback = aCallback; + } + + void UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) override; + +private: + std::string getFirstLineFromCommitMessage( const std::string& aMessage ); + std::string getFormattedCommitDate( const git_time& aTime );private: + + PullResult handleFastForward(); + PullResult handleMerge( const git_annotated_commit** aMergeHeads, size_t aMergeHeadsCount); + + std::vector>> m_fetchResults; + std::function& aConflicts )> m_conflictCallback; +}; + +#endif // GITPULLHANDLER_HPP diff --git a/common/git/git_push_handler.cpp b/common/git/git_push_handler.cpp new file mode 100644 index 0000000000..325ce2687f --- /dev/null +++ b/common/git/git_push_handler.cpp @@ -0,0 +1,79 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_push_handler.h" +#include + +#include + +GIT_PUSH_HANDLER::GIT_PUSH_HANDLER( git_repository* aRepo ) : KIGIT_COMMON( aRepo ) +{} + +GIT_PUSH_HANDLER::~GIT_PUSH_HANDLER() +{} + +PushResult GIT_PUSH_HANDLER::PerformPush() +{ + PushResult result = PushResult::Success; + + // Fetch updates from remote repository + git_remote* remote = nullptr; + + if( git_remote_lookup( &remote, m_repo, "origin" ) != 0 ) + { + AddErrorString( _( "Could not lookup remote" ) ); + return PushResult::Error; + } + + git_remote_callbacks remoteCallbacks = GIT_REMOTE_CALLBACKS_INIT; + remoteCallbacks.sideband_progress = progress_cb; + remoteCallbacks.transfer_progress = transfer_progress_cb; + remoteCallbacks.update_tips = update_cb; + remoteCallbacks.push_transfer_progress = push_transfer_progress_cb; + remoteCallbacks.payload = this; + + if( git_remote_connect( remote, GIT_DIRECTION_PUSH, &remoteCallbacks, nullptr, nullptr ) ) + { + git_remote_free( remote ); + AddErrorString( _( "Could not connect to remote" ) ); + return PushResult::Error; + } + + git_push_options pushOptions = GIT_PUSH_OPTIONS_INIT; + pushOptions.callbacks = remoteCallbacks; + + if( git_remote_push( remote, nullptr, &pushOptions ) ) + { + git_remote_free( remote ); + AddErrorString( _( "Could not push to remote" ) ); + return PushResult::Error; + } + + return result; +} + + +void GIT_PUSH_HANDLER::UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) +{ + ReportProgress( aCurrent, aTotal, aMessage ); +} diff --git a/common/git/git_push_handler.h b/common/git/git_push_handler.h new file mode 100644 index 0000000000..4a159e5397 --- /dev/null +++ b/common/git/git_push_handler.h @@ -0,0 +1,57 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GITPUSHHANDLER_HPP +#define GITPUSHHANDLER_HPP + +#include +#include +#include +#include + +#include "kicad_git_common.h" +#include + +// Enum for result codes +enum class PushResult +{ + Success, + Error, + UpToDate +}; + +class GIT_PUSH_HANDLER : public KIGIT_COMMON, public GIT_PROGRESS +{ +public: + GIT_PUSH_HANDLER( git_repository* aRepo ); + ~GIT_PUSH_HANDLER(); + + PushResult PerformPush(); + + void UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) override; + +private: + +}; + +#endif // GITPUSHHANDLER_HPP diff --git a/common/git/git_remove_from_index_handler.cpp b/common/git/git_remove_from_index_handler.cpp new file mode 100644 index 0000000000..10ccbfa621 --- /dev/null +++ b/common/git/git_remove_from_index_handler.cpp @@ -0,0 +1,98 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include +#include + +#include "git_remove_from_index_handler.h" + +GIT_REMOVE_FROM_INDEX_HANDLER::GIT_REMOVE_FROM_INDEX_HANDLER( git_repository* aRepository ) +{ + m_repository = aRepository; + m_filesToRemove.clear(); +} + +GIT_REMOVE_FROM_INDEX_HANDLER::~GIT_REMOVE_FROM_INDEX_HANDLER() +{ +} + +bool GIT_REMOVE_FROM_INDEX_HANDLER::RemoveFromIndex( const wxString& aFilePath ) +{ + // Test if file is currently in the index + + git_index* index = nullptr; + size_t at_pos = 0; + + if( git_repository_index( &index, m_repository ) != 0 ) + { + wxLogError( "Failed to get repository index" ); + return false; + } + + if( git_index_find( &at_pos, index, aFilePath.ToUTF8().data() ) != 0 ) + { + git_index_free( index ); + wxLogError( "Failed to find index entry for %s", aFilePath ); + return false; + } + + git_index_free( index ); + + m_filesToRemove.push_back( aFilePath ); + return true; +} + +void GIT_REMOVE_FROM_INDEX_HANDLER::PerformRemoveFromIndex() +{ + for( auto& file : m_filesToRemove ) + { + git_index* index = nullptr; + git_oid oid; + + if( git_repository_index( &index, m_repository ) != 0 ) + { + wxLogError( "Failed to get repository index" ); + return; + } + + if( git_index_remove_bypath( index, file.ToUTF8().data() ) != 0 ) + { + wxLogError( "Failed to remove index entry for %s", file ); + return; + } + + if( git_index_write( index ) != 0 ) + { + wxLogError( "Failed to write index" ); + return; + } + + if( git_index_write_tree( &oid, index ) != 0 ) + { + wxLogError( "Failed to write index tree" ); + return; + } + + git_index_free( index ); + } +} \ No newline at end of file diff --git a/common/git/git_remove_from_index_handler.h b/common/git/git_remove_from_index_handler.h new file mode 100644 index 0000000000..62aa08392b --- /dev/null +++ b/common/git/git_remove_from_index_handler.h @@ -0,0 +1,50 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_REMOVE_FROM_INDEX_HANDLER_H_ +#define GIT_REMOVE_FROM_INDEX_HANDLER_H_ + +#include +#include + +class wxString; + +class GIT_REMOVE_FROM_INDEX_HANDLER +{ +public: + GIT_REMOVE_FROM_INDEX_HANDLER( git_repository* aRepository ); + virtual ~GIT_REMOVE_FROM_INDEX_HANDLER(); + + bool RemoveFromIndex( const wxString& aFilePath ); + + void PerformRemoveFromIndex(); + +private: + + git_repository* m_repository; + + std::vector m_filesToRemove; +}; + + +#endif /* GIT_REMOVE_FROM_INDEX_HANDLER_H_ */ diff --git a/common/git/git_remove_vcs_handler.cpp b/common/git/git_remove_vcs_handler.cpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/git/git_remove_vcs_handler.h b/common/git/git_remove_vcs_handler.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/git/git_resolve_conflict_handler.cpp b/common/git/git_resolve_conflict_handler.cpp new file mode 100644 index 0000000000..35117ab7e6 --- /dev/null +++ b/common/git/git_resolve_conflict_handler.cpp @@ -0,0 +1,39 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_resolve_conflict_handler.h" + +GIT_RESOLVE_CONFLICT_HANDLER::GIT_RESOLVE_CONFLICT_HANDLER( git_repository* aRepository ) +{ + m_repository = aRepository; +} + +GIT_RESOLVE_CONFLICT_HANDLER::~GIT_RESOLVE_CONFLICT_HANDLER() +{ +} + +bool GIT_RESOLVE_CONFLICT_HANDLER::PerformResolveConflict() +{ + return true; +} + diff --git a/common/git/git_resolve_conflict_handler.h b/common/git/git_resolve_conflict_handler.h new file mode 100644 index 0000000000..a2bfe6754b --- /dev/null +++ b/common/git/git_resolve_conflict_handler.h @@ -0,0 +1,43 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_RESOLVE_CONFLICT_HANDLER_H +#define GIT_RESOLVE_CONFLICT_HANDLER_H + +#include + +class wxString; + +class GIT_RESOLVE_CONFLICT_HANDLER +{ +public: + GIT_RESOLVE_CONFLICT_HANDLER( git_repository* aRepository ); + virtual ~GIT_RESOLVE_CONFLICT_HANDLER(); + + bool PerformResolveConflict(); + +private: + git_repository* m_repository; +}; + +#endif // GIT_RESOLVE_CONFLICT_HANDLER_H diff --git a/common/git/git_revert_handler.cpp b/common/git/git_revert_handler.cpp new file mode 100644 index 0000000000..baf6b2a327 --- /dev/null +++ b/common/git/git_revert_handler.cpp @@ -0,0 +1,108 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_revert_handler.h" + +#include +#include + +GIT_REVERT_HANDLER::GIT_REVERT_HANDLER( git_repository* aRepository ) +{ + m_repository = aRepository; +} + +GIT_REVERT_HANDLER::~GIT_REVERT_HANDLER() +{ +} + +bool GIT_REVERT_HANDLER::Revert( const wxString& aFilePath ) +{ + m_filesToRevert.push_back( aFilePath ); + return true; +} + +static void checkout_progress_cb(const char *path, size_t cur, size_t tot, void *payload) +{ + wxLogDebug( "checkout_progress_cb: %s %zu/%zu", path, cur, tot ); +} + + +static int checkout_notify_cb(git_checkout_notify_t why, const char *path, + const git_diff_file *baseline, + const git_diff_file *target, + const git_diff_file *workdir, void *payload) +{ + GIT_REVERT_HANDLER* handler = static_cast(payload); + + if( why & ( GIT_CHECKOUT_NOTIFY_CONFLICT | GIT_CHECKOUT_NOTIFY_IGNORED | GIT_CHECKOUT_NOTIFY_UPDATED ) ) + handler->PushFailedFile( path ); + + return 0; +} + +void GIT_REVERT_HANDLER::PerformRevert() +{ + git_object* head_commit = NULL; + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + + // Get the HEAD commit + if (git_revparse_single(&head_commit, m_repository, "HEAD") != 0) { + // Handle error. If we cannot get the HEAD, then there's no point proceeding. + return; + } + + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + char** paths = new char*[m_filesToRevert.size()]; + + for( size_t ii = 0; ii < m_filesToRevert.size(); ii++ ) + { + // Set paths to the specific file + paths[ii] = wxStrdup( m_filesToRevert[ii].ToUTF8() ); + } + + git_strarray arr = { paths, m_filesToRevert.size() }; + + opts.paths = arr; + opts.progress_cb = checkout_progress_cb; + opts.notify_cb = checkout_notify_cb; + opts.notify_payload = static_cast(this); + + // Attempt to checkout the file(s) + if (git_checkout_tree(m_repository, head_commit, &opts) != 0) + { + const git_error *e = git_error_last(); + if (e) + { + wxLogError( "Checkout failed: %d: %s", e->klass, e->message ); + } + } + + // Free the HEAD commit + for( size_t ii = 0; ii < m_filesToRevert.size(); ii++ ) + delete( paths[ii] ); + + delete[] paths; + + git_object_free(head_commit); +} + diff --git a/common/git/git_revert_handler.h b/common/git/git_revert_handler.h new file mode 100644 index 0000000000..c004834650 --- /dev/null +++ b/common/git/git_revert_handler.h @@ -0,0 +1,54 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_REVERT_HANDLER_H_ +#define GIT_REVERT_HANDLER_H_ + +#include +#include + +class wxString; + +class GIT_REVERT_HANDLER +{ +public: + GIT_REVERT_HANDLER( git_repository* aRepository ); + virtual ~GIT_REVERT_HANDLER(); + + bool Revert( const wxString& aFilePath ); + + void PerformRevert(); + + void PushFailedFile( const wxString& aFilePath ) + { + m_filesFailedToRevert.push_back( aFilePath ); + } + +private: + git_repository* m_repository; + + std::vector m_filesToRevert; + std::vector m_filesFailedToRevert; +}; + +#endif /* GIT_REVERT_HANDLER_H_ */ \ No newline at end of file diff --git a/common/git/git_switch_branch_handler.cpp b/common/git/git_switch_branch_handler.cpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/git/git_switch_branch_handler.h b/common/git/git_switch_branch_handler.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/git/git_sync_handler.cpp b/common/git/git_sync_handler.cpp new file mode 100644 index 0000000000..57f81c80e2 --- /dev/null +++ b/common/git/git_sync_handler.cpp @@ -0,0 +1,44 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_sync_handler.h" + +#include + +#include + +GIT_SYNC_HANDLER::GIT_SYNC_HANDLER( git_repository* aRepository ) +{ + m_repository = aRepository; +} + + +GIT_SYNC_HANDLER::~GIT_SYNC_HANDLER() +{ +} + + +bool GIT_SYNC_HANDLER::PerformSync() +{ + return true; +} diff --git a/common/git/git_sync_handler.h b/common/git/git_sync_handler.h new file mode 100644 index 0000000000..234fcc7fe1 --- /dev/null +++ b/common/git/git_sync_handler.h @@ -0,0 +1,43 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_SYNC_HANDLER_H_ +#define GIT_SYNC_HANDLER_H_ + +#include + +class wxString; + +class GIT_SYNC_HANDLER +{ +public: + GIT_SYNC_HANDLER( git_repository* aRepository ); + virtual ~GIT_SYNC_HANDLER(); + + bool PerformSync(); + +private: + git_repository* m_repository; +}; + +#endif /* GIT_SYNC_HANDLER_H_ */ \ No newline at end of file diff --git a/common/git/kicad_git_blob_reader.h b/common/git/kicad_git_blob_reader.h new file mode 100644 index 0000000000..fd28bf0474 --- /dev/null +++ b/common/git/kicad_git_blob_reader.h @@ -0,0 +1,97 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include +#include +#include +#include + +#include + + +class BLOB_BUFFER_STREAM : public std::streambuf +{ + public: + BLOB_BUFFER_STREAM( git_blob* aBlob ) + { + // Yay C++ + setg( const_cast( static_cast( git_blob_rawcontent( aBlob ) ) ), + const_cast( static_cast( git_blob_rawcontent( aBlob ) ) ), + const_cast( static_cast( git_blob_rawcontent( aBlob ) ) ) + git_blob_rawsize( aBlob ) ); + } + + ~BLOB_BUFFER_STREAM() override + { + } + + int overflow( int c ) override + { + return traits_type::eof(); + } + + std::streamsize xsputn( const char* s, std::streamsize n ) override + { + return 0; + } +}; + +// Build a class that implements LINE_READER for git_blobs +class BLOB_READER : public LINE_READER +{ + public: + BLOB_READER( git_blob* aBlob ) : m_blob( aBlob ) + { + m_stream = new BLOB_BUFFER_STREAM( m_blob ); + m_istream = new std::istream( m_stream ); + m_line = nullptr; + m_lineNum = 0; + } + + ~BLOB_READER() override + { + delete m_istream; + delete m_stream; + } + + char* ReadLine() override + { + getline( *m_istream, m_buffer ); + + m_buffer.append( 1, '\n' ); + + m_length = m_buffer.size(); + m_line = (char*) m_buffer.data(); //ew why no const?? + + // lineNum is incremented even if there was no line read, because this + // leads to better error reporting when we hit an end of file. + ++m_lineNum; + + return m_istream->eof() ? nullptr : m_line; + } + + private: + git_blob* m_blob; + BLOB_BUFFER_STREAM* m_stream; + std::istream* m_istream; + std::string m_buffer; +}; \ No newline at end of file diff --git a/common/git/kicad_git_common.cpp b/common/git/kicad_git_common.cpp new file mode 100644 index 0000000000..2e1ce4b2e2 --- /dev/null +++ b/common/git/kicad_git_common.cpp @@ -0,0 +1,541 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "kicad_git_common.h" + +#include +#include +#include +#include + +KIGIT_COMMON::KIGIT_COMMON( git_repository* aRepo ) : m_repo( aRepo ) +{} + +KIGIT_COMMON::~KIGIT_COMMON() +{} + +git_repository* KIGIT_COMMON::GetRepo() const +{ + return m_repo; +} + +wxString KIGIT_COMMON::GetCurrentBranchName() const +{ + git_reference* head = nullptr; + + int retval = git_repository_head( &head, m_repo ); + + if( retval && retval != GIT_EUNBORNBRANCH && retval != GIT_ENOTFOUND ) + return wxEmptyString; + + git_reference *branch; + + if( git_reference_resolve( &branch, head ) ) + { + git_reference_free( head ); + return wxEmptyString; + } + + git_reference_free( head ); + const char* branchName = ""; + + if( git_branch_name( &branchName, branch ) ) + { + git_reference_free( branch ); + return wxEmptyString; + } + + git_reference_free( branch ); + + return branchName; +} + + +std::vector KIGIT_COMMON::GetBranchNames() const +{ + std::vector branchNames; + std::map branchNamesMap; + wxString firstName; + + git_branch_iterator* branchIterator = nullptr; + + if( git_branch_iterator_new( &branchIterator, m_repo, GIT_BRANCH_LOCAL ) ) + return branchNames; + + git_reference* branchReference = nullptr; + git_branch_t branchType; + + while( git_branch_next( &branchReference, &branchType, branchIterator ) != GIT_ITEROVER ) + { + const char* branchName = ""; + + if( git_branch_name( &branchName, branchReference ) ) + continue; + + const git_oid* commitId = git_reference_target( branchReference ); + + git_commit* commit = nullptr; + + if( git_commit_lookup( &commit, m_repo, commitId ) ) + continue; + + git_time_t commitTime = git_commit_time( commit ); + + if( git_branch_is_head( branchReference ) ) + firstName = branchName; + else + branchNamesMap.emplace( commitTime, branchName ); + + git_commit_free( commit ); + git_reference_free( branchReference ); + } + + git_branch_iterator_free( branchIterator ); + + // Add the current branch to the top of the list + if( !firstName.IsEmpty() ) + branchNames.push_back( firstName ); + + // Add the remaining branches in order from newest to oldest + for( auto rit = branchNamesMap.rbegin(); rit != branchNamesMap.rend(); ++rit ) + branchNames.push_back( rit->second ); + + return branchNames; +} + + +std::vector KIGIT_COMMON::GetProjectDirs() +{ + std::vector projDirs; + + git_oid oid; + git_commit* commit; + git_tree *tree; + + if( git_reference_name_to_id( &oid, m_repo, "HEAD" ) != GIT_OK ) + { + wxLogError( "An error occurred: %s", git_error_last()->message ); + return projDirs; + } + + if( git_commit_lookup( &commit, m_repo, &oid ) != GIT_OK ) + { + wxLogError( "An error occurred: %s", git_error_last()->message ); + return projDirs; + } + + if( git_commit_tree( &tree, commit ) != GIT_OK ) + { + wxLogError( "An error occurred: %s", git_error_last()->message ); + return projDirs; + } + + // Define callback + git_tree_walk( + tree, GIT_TREEWALK_PRE, + []( const char* root, const git_tree_entry* entry, void* payload ) + { + std::vector* prjs = static_cast*>( payload ); + wxFileName root_fn( git_tree_entry_name( entry ) ); + + root_fn.SetPath( root ); + + if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB + && ( ( root_fn.GetExt() == "kicad_pro" ) || ( root_fn.GetExt() == "pro" ) ) ) + { + prjs->push_back( root_fn.GetFullPath() ); + } + + return 0; // continue walking + }, + &projDirs ); + + git_tree_free( tree ); + git_commit_free( commit ); + + std::sort( projDirs.begin(), projDirs.end(), + []( const wxString& a, const wxString& b ) + { + int a_freq = a.Freq( wxFileName::GetPathSeparator() ); + int b_freq = b.Freq( wxFileName::GetPathSeparator() ); + + if( a_freq == b_freq ) + return a < b; + else + return a_freq < b_freq; + + } ); + + return projDirs; +} + + +std::pair,std::set> KIGIT_COMMON::GetDifferentFiles() const +{ + auto get_modified_files = [&]( git_oid* from_oid, git_oid* to_oid ) -> std::set + { + std::set modified_set; + git_revwalk* walker = nullptr; + + if( git_revwalk_new( &walker, m_repo ) != GIT_OK ) + return modified_set; + + if( ( git_revwalk_push( walker, from_oid ) != GIT_OK ) + || ( git_revwalk_hide( walker, to_oid ) != GIT_OK ) ) + { + git_revwalk_free( walker ); + return modified_set; + } + + git_oid oid; + git_commit* commit; + + // iterate over all local commits not in remote + while( git_revwalk_next( &oid, walker ) == GIT_OK ) + { + if( git_commit_lookup( &commit, m_repo, &oid ) != GIT_OK ) + continue; + + git_tree *tree, *parent_tree = nullptr; + if( git_commit_tree( &tree, commit ) != GIT_OK ) + { + git_commit_free( commit ); + continue; + } + + // get parent commit tree to diff against + if( !git_commit_parentcount( commit ) ) + { + git_tree_free( tree ); + git_commit_free( commit ); + continue; + } + + + git_commit* parent; + if( git_commit_parent( &parent, commit, 0 ) != GIT_OK ) + { + git_tree_free( tree ); + git_commit_free( commit ); + continue; + } + + + if( git_commit_tree( &parent_tree, parent ) != GIT_OK ) + { + git_tree_free( tree ); + git_commit_free( commit ); + git_commit_free( parent ); + continue; + } + + + git_diff* diff; + git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; + + if( git_diff_tree_to_tree( &diff, m_repo, parent_tree, tree, &diff_opts ) == GIT_OK ) + { + size_t num_deltas = git_diff_num_deltas( diff ); + + for( size_t i = 0; i < num_deltas; ++i ) + { + const git_diff_delta* delta = git_diff_get_delta( diff, i ); + modified_set.insert( delta->new_file.path ); + } + + git_diff_free( diff ); + } + + git_tree_free( parent_tree ); + git_commit_free( parent ); + git_tree_free( tree ); + git_commit_free( commit ); + } + + git_revwalk_free( walker ); + + return modified_set; + }; + + std::pair,std::set> modified_files; + + if( !m_repo ) + return modified_files; + + git_reference* head = nullptr; + git_reference* remote_head = nullptr; + + if( git_repository_head( &head, m_repo ) != GIT_OK ) + return modified_files; + + if( git_branch_upstream( &remote_head, head ) != GIT_OK ) + { + git_reference_free( head ); + return modified_files; + } + + git_oid head_oid = *git_reference_target( head ); + git_oid remote_oid = *git_reference_target( remote_head ); + + git_reference_free( head ); + git_reference_free( remote_head ); + + modified_files.first = get_modified_files( &head_oid, &remote_oid ); + modified_files.second = get_modified_files( &remote_oid, &head_oid ); + + return modified_files; +} + + +bool KIGIT_COMMON::HasLocalCommits() const +{ + if( !m_repo ) + return false; + + git_reference* head = nullptr; + git_reference* remote_head = nullptr; + + if( git_repository_head( &head, m_repo ) != GIT_OK ) + return false; + + if( git_branch_upstream( &remote_head, head ) != GIT_OK ) + { + git_reference_free( head ); + return false; + } + + git_oid head_oid = *git_reference_target( head ); + git_oid remote_oid = *git_reference_target( remote_head ); + + git_reference_free( head ); + git_reference_free( remote_head ); + + git_revwalk* walker = nullptr; + + if( git_revwalk_new( &walker, m_repo ) != GIT_OK ) + return false; + + if( ( git_revwalk_push( walker, &head_oid ) != GIT_OK ) + || ( git_revwalk_hide( walker, &remote_oid ) != GIT_OK ) ) + { + git_revwalk_free( walker ); + return false; + } + + git_oid oid; + + // If we can't walk to the next commit, then we are at or behind the remote + if( git_revwalk_next( &oid, walker ) != GIT_OK ) + { + git_revwalk_free( walker ); + return false; + } + + git_revwalk_free( walker ); + return true; +} + + +bool KIGIT_COMMON::HasPushAndPullRemote() const +{ + git_remote* remote = nullptr; + + if( git_remote_lookup( &remote, m_repo, "origin" ) != GIT_OK ) + { + return false; + } + + // Get the URLs associated with the remote + const char* fetch_url = git_remote_url( remote ); + const char* push_url = git_remote_pushurl( remote ); + + // If no push URL is set, libgit2 defaults to using the fetch URL for pushing + if( !push_url ) + { + push_url = fetch_url; + } + + // Clean up the remote object + git_remote_free( remote ); + + // Check if both URLs are valid (i.e., not NULL) + return fetch_url && push_url; +} + + +extern "C" int fetchhead_foreach_cb( const char*, const char*, + const git_oid* aOID, unsigned int aIsMerge, void* aPayload ) +{ + if( aIsMerge ) + git_oid_cpy( (git_oid*) aPayload, aOID ); + + return 0; +} + + +extern "C" void clone_progress_cb( const char* aStr, size_t aLen, size_t aTotal, void* data ) +{ + KIGIT_COMMON* parent = (KIGIT_COMMON*) data; + + wxString progressMessage( aStr ); + parent->UpdateProgress( aLen, aTotal, progressMessage ); +} + + +extern "C" int progress_cb( const char* str, int len, void* data ) +{ + KIGIT_COMMON* parent = (KIGIT_COMMON*) data; + + wxString progressMessage( str, len ); + parent->UpdateProgress( 0, 0, progressMessage ); + + return 0; +} + +extern "C" int transfer_progress_cb( const git_transfer_progress* aStats, void* aPayload ) +{ + KIGIT_COMMON* parent = (KIGIT_COMMON*) aPayload; + wxString progressMessage = wxString::Format( _( "Received %u of %u objects" ), + aStats->received_objects, aStats->total_objects ); + + parent->UpdateProgress( aStats->received_objects, aStats->total_objects, progressMessage ); + + return 0; +} + +extern "C" int update_cb( const char* aRefname, const git_oid* aFirst, const git_oid* aSecond, + void* aPayload ) +{ + constexpr int cstring_len = 8; + char a_str[cstring_len + 1]; + char b_str[cstring_len + 1]; + + KIGIT_COMMON* parent = (KIGIT_COMMON*) aPayload; + wxString status; + + git_oid_tostr( b_str, cstring_len, aSecond ); + + if( !git_oid_is_zero( aFirst ) ) + { + git_oid_tostr( a_str, cstring_len, aFirst ); + status = wxString::Format( _( "* [updated] %s..%s %s" ), a_str, b_str, aRefname ); + } + else + { + status = wxString::Format( _( "* [new] %s %s" ), b_str, aRefname ); + } + + parent->UpdateProgress( 0, 0, status ); + + return 0; +} + + +extern "C" int push_transfer_progress_cb( unsigned int aCurrent, unsigned int aTotal, size_t aBytes, + void* aPayload ) +{ + int64_t progress = 100; + KIGIT_COMMON* parent = (KIGIT_COMMON*) aPayload; + + if( aTotal != 0 ) + { + progress = ( aCurrent * 100 ) / aTotal; + } + + wxString progressMessage = wxString::Format( _( "Writing objects: %d%% (%d/%d), %d bytes" ), + progress, aCurrent, aTotal, aBytes ); + parent->UpdateProgress( aCurrent, aTotal, progressMessage ); + + return 0; +} + + +extern "C" int push_update_reference_cb( const char* aRefname, const char* aStatus, void* aPayload ) +{ + KIGIT_COMMON* parent = (KIGIT_COMMON*) aPayload; + wxString status( aStatus ); + + if( !status.IsEmpty() ) + { + wxString statusMessage = wxString::Format( _( "* [rejected] %s (%s)" ), aRefname, aStatus ); + parent->UpdateProgress( 0, 0, statusMessage ); + } + else + { + wxString statusMessage = wxString::Format( _( "[updated] %s" ), aRefname ); + parent->UpdateProgress( 0, 0, statusMessage ); + } + + return 0; +} + + +extern "C" int credentials_cb( git_cred** aOut, const char* aUrl, const char* aUsername, + unsigned int aAllowedTypes, void* aPayload ) +{ + KIGIT_COMMON* parent = static_cast( aPayload ); + + if( parent->GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) + return GIT_PASSTHROUGH; + + if( aAllowedTypes & GIT_CREDENTIAL_USERNAME + && !( parent->TestedTypes() & GIT_CREDTYPE_USERNAME ) ) + { + wxString username = parent->GetUsername().Trim().Trim( false ); + git_cred_username_new( aOut, username.ToStdString().c_str() ); + parent->TestedTypes() |= GIT_CREDTYPE_USERNAME; + } + else if( parent->GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS + && ( aAllowedTypes & GIT_CREDENTIAL_USERPASS_PLAINTEXT ) + && !( parent->TestedTypes() & GIT_CREDTYPE_USERPASS_PLAINTEXT ) ) + { + wxString username = parent->GetUsername().Trim().Trim( false ); + wxString password = parent->GetPassword().Trim().Trim( false ); + + git_cred_userpass_plaintext_new( aOut, username.ToStdString().c_str(), + password.ToStdString().c_str() ); + parent->TestedTypes() |= GIT_CREDTYPE_USERPASS_PLAINTEXT; + } + else if( parent->GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH + && ( aAllowedTypes & GIT_CREDENTIAL_SSH_KEY ) + && !( parent->TestedTypes() & GIT_CREDTYPE_SSH_KEY ) ) + { + // SSH key authentication + wxString sshKey = parent->GetSSHKey(); + wxString sshPubKey = sshKey + ".pub"; + wxString username = parent->GetUsername().Trim().Trim( false ); + wxString password = parent->GetPassword().Trim().Trim( false ); + + git_cred_ssh_key_new( aOut, username.ToStdString().c_str(), + sshPubKey.ToStdString().c_str(), + sshKey.ToStdString().c_str(), + password.ToStdString().c_str() ); + parent->TestedTypes() |= GIT_CREDTYPE_SSH_KEY; + } + else + { + return GIT_PASSTHROUGH; + } + + return GIT_OK; +}; \ No newline at end of file diff --git a/common/git/kicad_git_common.h b/common/git/kicad_git_common.h new file mode 100644 index 0000000000..40e7098f41 --- /dev/null +++ b/common/git/kicad_git_common.h @@ -0,0 +1,145 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef _GIT_COMMON_H_ +#define _GIT_COMMON_H_ + +#include + +#include +#include + +#include + +class KIGIT_COMMON : public KIGIT_ERRORS +{ + +public: + KIGIT_COMMON( git_repository* aRepo ); + ~KIGIT_COMMON(); + + git_repository* GetRepo() const; + + void SetRepo( git_repository* aRepo ) + { + m_repo = aRepo; + } + + wxString GetCurrentBranchName() const; + + std::vector GetBranchNames() const; + + virtual void UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) {}; + + /** + * Return a vector of project files in the repository. Sorted by the depth of + * the project file in the directory tree + * + * @return std::vector of project files + */ + std::vector GetProjectDirs(); + + /** + * Return a pair of sets of files that differ locally from the remote repository + * The first set is files that have been committed locally but not pushed + * The second set is files that have been committed remotely but not pulled + */ + std::pair,std::set> GetDifferentFiles() const; + + enum class GIT_STATUS + { + GIT_STATUS_UNTRACKED, + GIT_STATUS_CURRENT, + GIT_STATUS_MODIFIED, // File changed but not committed to local repository + GIT_STATUS_ADDED, + GIT_STATUS_DELETED, + GIT_STATUS_BEHIND, // File changed in remote repository but not in local + GIT_STATUS_AHEAD, // File changed in local repository but not in remote + GIT_STATUS_CONFLICTED, + GIT_STATUS_LAST + }; + + enum class GIT_CONN_TYPE + { + GIT_CONN_HTTPS = 0, + GIT_CONN_SSH, + GIT_CONN_LOCAL, + GIT_CONN_LAST + }; + + wxString GetUsername() const { return m_username; } + wxString GetPassword() const { return m_password; } + wxString GetSSHKey() const { return m_sshKey; } + GIT_CONN_TYPE GetConnType() const { return m_connType; } + + void SetUsername( const wxString& aUsername ) { m_username = aUsername; } + void SetPassword( const wxString& aPassword ) { m_password = aPassword; } + void SetSSHKey( const wxString& aSSHKey ) { m_sshKey = aSSHKey; } + + void SetConnType( GIT_CONN_TYPE aConnType ) { m_connType = aConnType; } + void SetConnType( unsigned aConnType ) + { + if( aConnType < static_cast( GIT_CONN_TYPE::GIT_CONN_LAST ) ) + m_connType = static_cast( aConnType ); + } + + // Holds a temporary variable that can be used by the authentication callback + // to remember which types of authentication have been tested so that we + // don't loop forever. + unsigned& TestedTypes() { return m_testedTypes; } + + // Returns true if the repository has local commits that have not been pushed + bool HasLocalCommits() const; + + // Returns true if the repository has a remote that can be pushed to pulled from + bool HasPushAndPullRemote() const; + +protected: + git_repository* m_repo; + + GIT_CONN_TYPE m_connType; + wxString m_username; + wxString m_password; + wxString m_sshKey; + + unsigned m_testedTypes; + +}; + + +extern "C" int progress_cb( const char* str, int len, void* data ); +extern "C" void clone_progress_cb( const char* str, size_t len, size_t total, void* data ); +extern "C" int transfer_progress_cb( const git_transfer_progress* aStats, void* aPayload ); +extern "C" int update_cb( const char* aRefname, const git_oid* aFirst, const git_oid* aSecond, + void* aPayload ); +extern "C" int push_transfer_progress_cb( unsigned int aCurrent, unsigned int aTotal, + size_t aBytes, void* aPayload ); +extern "C" int push_update_reference_cb( const char* aRefname, const char* aStatus, + void* aPayload ); + +extern "C" int fetchhead_foreach_cb( const char*, const char*, + const git_oid* aOID, unsigned int aIsMerge, void* aPayload ); +extern "C" int credentials_cb( git_cred** aOut, const char* aUrl, const char* aUsername, + unsigned int aAllowedTypes, void* aPayload ); + +#endif // _GIT_COMMON_H_ \ No newline at end of file diff --git a/common/git/kicad_git_errors.cpp b/common/git/kicad_git_errors.cpp new file mode 100644 index 0000000000..97195b1b89 --- /dev/null +++ b/common/git/kicad_git_errors.cpp @@ -0,0 +1,66 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +namespace KIGIT_ERROR +{ + #undef _ + #define _(a) a + + // General errors + const char* const kInvalidRepository = _("Invalid repository."); + const char* const kCommitFailed = _("Failed to commit changes."); + const char* const kMergeFailed = _("Failed to merge branches."); + + // Clone errors + const char* const kCloneFailed = _("Failed to clone repository."); + const char* const kRemoteNotFound = _("Remote repository not found."); + const char* const kAuthenticationFailed = _("Authentication failed for remote repository."); + + // Branch errors + const char* const kBranchNotFound = _("Branch not found."); + const char* const kBranchCreationFailed = _("Failed to create branch."); + const char* const kBranchDeletionFailed = _("Failed to delete branch."); + + // Checkout errors + const char* const kCheckoutFailed = _("Failed to perform checkout operation."); + const char* const kFileNotFoundInCheckout = _("File not found during checkout operation."); + + // Conflict errors + const char* const kMergeConflict = _("Merge conflict encountered."); + const char* const kRebaseConflict = _("Rebase conflict encountered."); + + // Pull/Push errors + const char* const kPullFailed = _("Failed to pull changes from remote repository."); + const char* const kPushFailed = _("Failed to push changes to remote repository."); + const char* const kNoUpstreamBranch = _("No upstream branch configured."); + const char* const kRemoteConnectionError = _("Failed to establish connection with remote repository."); + + // Tag errors + const char* const kTagNotFound = _("Tag not found."); + const char* const kTagCreationFailed = _("Failed to create tag."); + const char* const kTagDeletionFailed = _("Failed to delete tag."); + + const char* const kUnknownError = _("Unknown error."); + const char* const kNoError = _("No error."); + +} \ No newline at end of file diff --git a/common/git/kicad_git_errors.h b/common/git/kicad_git_errors.h new file mode 100644 index 0000000000..2846dbfb1b --- /dev/null +++ b/common/git/kicad_git_errors.h @@ -0,0 +1,81 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef KICAD_GIT_ERRORS_H +#define KICAD_GIT_ERRORS_H + +#include + +#include + +class KIGIT_ERRORS +{ +public: + + KIGIT_ERRORS() = default; + + const std::vector& GetErrorStrings() const + { + return m_errorStrings; + } + + const wxString& PeekErrorString() const + { + if( m_errorStrings.empty() ) + return _( "No error" ); + else + return m_errorStrings.back(); + } + + wxString GetErrorString() + { + if( m_errorStrings.empty() ) + return _( "No error" ); + + const wxString errorString( m_errorStrings.back() ); + m_errorStrings.pop_back(); + return errorString; + } + + void AddErrorString( const wxString aErrorString ) + { + m_errorStrings.emplace_back( aErrorString ); + } + + void AddErrorString( const std::string aErrorString ) + { + m_errorStrings.emplace_back( aErrorString ); + } + + void ClearErrorStrings() + { + m_errorStrings.clear(); + } + +private: + + std::vector m_errorStrings; + +}; + +#endif // KICAD_GIT_ERRORS_H diff --git a/common/project/project_local_settings.cpp b/common/project/project_local_settings.cpp index 0b34328953..29ff3f359c 100644 --- a/common/project/project_local_settings.cpp +++ b/common/project/project_local_settings.cpp @@ -177,6 +177,18 @@ PROJECT_LOCAL_SETTINGS::PROJECT_LOCAL_SETTINGS( PROJECT* aProject, const wxStrin ZONE_DISPLAY_MODE::SHOW_FILLED, ZONE_DISPLAY_MODE::SHOW_FILLED, ZONE_DISPLAY_MODE::SHOW_TRIANGULATION ) ); + m_params.emplace_back( new PARAM( "git.repo_username", + &m_GitRepoUsername, "" ) ); + + m_params.emplace_back( new PARAM( "git.repo_password", + &m_GitRepoPassword, "" ) ); + + m_params.emplace_back( new PARAM( "git.repo_type", + &m_GitRepoType, "" ) ); + + m_params.emplace_back( new PARAM( "git.ssh_key", + &m_GitSSHKey, "" ) ); + m_params.emplace_back( new PARAM_LAMBDA( "project.files", [&]() -> nlohmann::json { diff --git a/common/settings/common_settings.cpp b/common/settings/common_settings.cpp index 527dbe581a..7e89063095 100644 --- a/common/settings/common_settings.cpp +++ b/common/settings/common_settings.cpp @@ -347,6 +347,61 @@ COMMON_SETTINGS::COMMON_SETTINGS() : m_params.emplace_back( new PARAM( "package_manager.sash_pos", &m_PackageManager.sash_pos, 380 ) ); + m_params.emplace_back( new PARAM_LAMBDA( "git.repositories", + [&]() -> nlohmann::json + { + nlohmann::json ret = {}; + + for( const GIT_REPOSITORY& repo : m_Git.repositories ) + { + nlohmann::json repoJson = {}; + + repoJson["name"] = repo.name; + repoJson["path"] = repo.path; + repoJson["authType"] = repo.authType; + repoJson["username"] = repo.username; + repoJson["ssh_path"] = repo.ssh_path; + repoJson["active"] = repo.active; + + ret.push_back( repoJson ); + } + + return ret; + }, + [&]( const nlohmann::json& aJson ) + { + if( !aJson.is_array() ) + return; + + m_Git.repositories.clear(); + + for( const auto& repoJson : aJson ) + { + GIT_REPOSITORY repo; + + repo.name = repoJson["name"].get(); + repo.path = repoJson["path"].get(); + repo.authType = repoJson["authType"].get(); + repo.username = repoJson["username"].get(); + repo.ssh_path = repoJson["ssh_path"].get(); + repo.active = repoJson["active"].get(); + + m_Git.repositories.push_back( repo ); + } + }, + {} ) ); + + m_params.emplace_back( new PARAM( "git.authorName", + &m_Git.authorName, wxS( "" ) ) ); + + m_params.emplace_back( new PARAM( "git.authorEmail", + &m_Git.authorEmail, wxS( "" ) ) ); + + m_params.emplace_back( new PARAM( "git.useDefaultAuthor", + &m_Git.useDefaultAuthor, true ) ); + + + registerMigration( 0, 1, std::bind( &COMMON_SETTINGS::migrateSchema0to1, this ) ); registerMigration( 1, 2, std::bind( &COMMON_SETTINGS::migrateSchema1to2, this ) ); registerMigration( 2, 3, std::bind( &COMMON_SETTINGS::migrateSchema2to3, this ) ); diff --git a/common/single_top.cpp b/common/single_top.cpp index d045de79f7..63c6d19794 100644 --- a/common/single_top.cpp +++ b/common/single_top.cpp @@ -52,6 +52,8 @@ #include #include +#include + #ifdef KICAD_USE_SENTRY #include #endif @@ -73,6 +75,7 @@ static struct PGM_SINGLE_TOP : public PGM_BASE void OnPgmExit() { + Kiway.OnKiwayEnd(); if( m_settings_manager && m_settings_manager->IsOK() ) @@ -84,6 +87,7 @@ static struct PGM_SINGLE_TOP : public PGM_BASE // Destroy everything in PGM_BASE, especially wxSingleInstanceCheckerImpl // earlier than wxApp and earlier than static destruction would. PGM_BASE::Destroy(); + git_libgit2_shutdown(); } void MacOpenFile( const wxString& aFileName ) override @@ -298,6 +302,9 @@ bool PGM_SINGLE_TOP::OnPgmInit() } #endif + // Initialize the git library before trying to initialize individual programs + git_libgit2_init(); + // Not all kicad applications use the python stuff. skip python init // for these apps. bool skip_python_initialization = false; diff --git a/eeschema/lib_field.cpp b/eeschema/lib_field.cpp index 0d432c9867..1ff99ee776 100644 --- a/eeschema/lib_field.cpp +++ b/eeschema/lib_field.cpp @@ -589,6 +589,58 @@ bool LIB_FIELD::IsMandatory() const } +bool LIB_FIELD::operator==( const LIB_ITEM& aItem ) const +{ + if( aItem.Type() != LIB_FIELD_T ) + return false; + + const LIB_FIELD& field = static_cast( aItem ); + + if( m_id != field.m_id ) + return false; + + if( m_name != field.m_name ) + return false; + + if( m_parent->m_Uuid != aItem.GetParent()->m_Uuid ) + return false; + + if( m_id < MANDATORY_FIELDS ) + return true; + + if( m_Uuid == field.m_Uuid ) + return true; + + return EDA_TEXT::operator==( field ); +} + + +double LIB_FIELD::Similarity( const LIB_ITEM& aItem ) const +{ + if( aItem.Type() != LIB_FIELD_T ) + return 0.0; + + const LIB_FIELD& field = static_cast( aItem ); + + if( m_id != field.m_id && m_id < MANDATORY_FIELDS ) + return 0.0; + + if( m_parent->m_Uuid != aItem.GetParent()->m_Uuid ) + return 0.0; + + if( m_id < MANDATORY_FIELDS ) + return 1.0; + + if( m_Uuid == field.m_Uuid ) + return 1.0; + + double similarity = SimilarityBase( field ); + + similarity *= EDA_TEXT::Similarity( field ); + + return similarity; +} + static struct LIB_FIELD_DESC { LIB_FIELD_DESC() diff --git a/eeschema/lib_field.h b/eeschema/lib_field.h index 03e5fa1c0c..2f3a510151 100644 --- a/eeschema/lib_field.h +++ b/eeschema/lib_field.h @@ -198,6 +198,10 @@ public: bool ShowInChooser() const { return m_showInChooser; } void SetShowInChooser( bool aShow = true ) { m_showInChooser = aShow; } + double Similarity( const LIB_ITEM& aItem ) const override; + + bool operator==( const LIB_ITEM& aItem ) const override; + private: /** diff --git a/eeschema/lib_item.cpp b/eeschema/lib_item.cpp index 4682b36f15..82a3f7d8fd 100644 --- a/eeschema/lib_item.cpp +++ b/eeschema/lib_item.cpp @@ -168,5 +168,3 @@ void LIB_ITEM::ViewGetLayers( int aLayers[], int& aCount ) const aLayers[1] = LAYER_DEVICE_BACKGROUND; aLayers[2] = LAYER_SELECTION_SHADOWS; } - - diff --git a/eeschema/lib_item.h b/eeschema/lib_item.h index 3f61330c70..6c9be0b960 100644 --- a/eeschema/lib_item.h +++ b/eeschema/lib_item.h @@ -204,6 +204,35 @@ public: return (LIB_SYMBOL*) m_parent; } + /** + * Return a measure of how likely the other object is to represent the same + * object. The scale runs from 0.0 (definitely different objects) to 1.0 (same) + * + * This is a pure virtual function. Derived classes must implement this. + */ + virtual double Similarity( const LIB_ITEM& aItem ) const = 0; + + /** + * Calculate the boilerplate similarity for all LIB_ITEMs without + * preventing the use above of a pure virtual function that catches at compile + * time when a new object has not been fully implemented + */ + double SimilarityBase( const LIB_ITEM& aItem ) const + { + double similarity = 1.0; + + if( m_unit != aItem.m_unit ) + similarity *= 0.9; + + if( m_convert != aItem.m_convert ) + similarity *= 0.9; + + if( m_private != aItem.m_private ) + similarity *= 0.9; + + return similarity; + } + void ViewGetLayers( int aLayers[], int& aCount ) const override; bool HitTest( const VECTOR2I& aPosition, int aAccuracy = 0 ) const override @@ -237,7 +266,7 @@ public: * @param aOther Object to test against. * @return True if object is identical to this object. */ - bool operator==( const LIB_ITEM& aOther ) const; + virtual bool operator==( const LIB_ITEM& aOther ) const; bool operator==( const LIB_ITEM* aOther ) const { return *this == *aOther; diff --git a/eeschema/lib_pin.cpp b/eeschema/lib_pin.cpp index e2eb5f7ad1..2b829239b6 100644 --- a/eeschema/lib_pin.cpp +++ b/eeschema/lib_pin.cpp @@ -1453,6 +1453,123 @@ wxString LIB_PIN::GetItemDescription( UNITS_PROVIDER* aUnitsProvider ) const } +bool LIB_PIN::operator==( const LIB_ITEM& aOther ) const +{ + if( aOther.Type() != LIB_PIN_T ) + return false; + + const LIB_PIN* other = static_cast( &aOther ); + + if( m_name != other->m_name ) + return false; + + if( m_number != other->m_number ) + return false; + + if( m_position != other->m_position ) + return false; + + if( m_length != other->m_length ) + return false; + + if( m_orientation != other->m_orientation ) + return false; + + if( m_shape != other->m_shape ) + return false; + + if( m_type != other->m_type ) + return false; + + if( m_attributes != other->m_attributes ) + return false; + + if( m_numTextSize != other->m_numTextSize ) + return false; + + if( m_nameTextSize != other->m_nameTextSize ) + return false; + + if( m_alternates.size() != other->m_alternates.size() ) + return false; + + auto lhsItem = m_alternates.begin(); + auto rhsItem = other->m_alternates.begin(); + + while( lhsItem != m_alternates.end() ) + { + if( rhsItem == other->m_alternates.end() ) + return false; + + const ALT& lhsAlt = lhsItem->second; + const ALT& rhsAlt = rhsItem->second; + + if( lhsAlt.m_Name != rhsAlt.m_Name ) + return false; + + if( lhsAlt.m_Type != rhsAlt.m_Type ) + return false; + + if( lhsAlt.m_Shape != rhsAlt.m_Shape ) + return false; + + ++lhsItem; + ++rhsItem; + } + + return rhsItem == other->m_alternates.end(); +} + + +double LIB_PIN::Similarity( const LIB_ITEM& aOther ) const +{ + if( aOther.m_Uuid == m_Uuid ) + return 1.0; + + if( aOther.Type() != LIB_PIN_T ) + return 0.0; + + const LIB_PIN* other = static_cast( &aOther ); + + double similarity = SimilarityBase( aOther ); + + if( m_name != other->m_name ) + similarity *= 0.9; + + if( m_number != other->m_number ) + similarity *= 0.9; + + if( m_position != other->m_position ) + similarity *= 0.9; + + if( m_length != other->m_length ) + similarity *= 0.9; + + if( m_orientation != other->m_orientation ) + similarity *= 0.9; + + if( m_shape != other->m_shape ) + similarity *= 0.9; + + if( m_type != other->m_type ) + similarity *= 0.9; + + if( m_attributes != other->m_attributes ) + similarity *= 0.9; + + if( m_numTextSize != other->m_numTextSize ) + similarity *= 0.9; + + if( m_nameTextSize != other->m_nameTextSize ) + similarity *= 0.9; + + if( m_alternates.size() != other->m_alternates.size() ) + similarity *= 0.9; + + return similarity; +} + + std::ostream& LIB_PIN::operator<<( std::ostream& aStream ) { aStream << "LIB_PIN:" << std::endl diff --git a/eeschema/lib_pin.h b/eeschema/lib_pin.h index 8dbd67c90e..c12a1ad50c 100644 --- a/eeschema/lib_pin.h +++ b/eeschema/lib_pin.h @@ -265,6 +265,13 @@ public: */ static const wxString GetCanonicalElectricalTypeName( ELECTRICAL_PINTYPE aType ); + double Similarity( const LIB_ITEM& aItem ) const override; + + bool operator==( const LIB_ITEM& aItem ) const override; + bool operator!=( const LIB_ITEM& aItem ) const { return !operator==( aItem ); } + bool operator<( const LIB_PIN& aRhs ) const { return compare( aRhs, EQUALITY ) < 0; } + bool operator>( const LIB_PIN& aRhs ) const { return compare( aRhs, EQUALITY ) > 0; } + protected: struct EXTENTS_CACHE { @@ -300,10 +307,6 @@ protected: */ void printPinElectricalTypeName( const RENDER_SETTINGS* aSettings, VECTOR2I& aPosition, PIN_ORIENTATION aOrientation, bool aDimmed ); - - bool operator==( const LIB_PIN& aRhs ) const { return compare( aRhs, EQUALITY ) == 0; } - bool operator<( const LIB_PIN& aRhs ) const { return compare( aRhs, EQUALITY ) < 0; } - bool operator>( const LIB_PIN& aRhs ) const { return compare( aRhs, EQUALITY ) > 0; } std::ostream& operator<<( std::ostream& aStream ); private: diff --git a/eeschema/lib_shape.cpp b/eeschema/lib_shape.cpp index e28aa78549..d5b9cd06ca 100644 --- a/eeschema/lib_shape.cpp +++ b/eeschema/lib_shape.cpp @@ -518,6 +518,33 @@ void LIB_SHAPE::AddPoint( const VECTOR2I& aPosition ) } +bool LIB_SHAPE::operator==( const LIB_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const LIB_SHAPE& other = static_cast( aOther ); + + return LIB_ITEM::operator==( aOther ) && EDA_SHAPE::operator==( other ); +} + + +double LIB_SHAPE::Similarity( const LIB_ITEM& aOther ) const +{ + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + if( aOther.Type() != Type() ) + return 0.0; + + const LIB_SHAPE& other = static_cast( aOther ); + + double similarity = SimilarityBase( other ); + + similarity *= EDA_SHAPE::Similarity( other ); + + return similarity; +} void LIB_SHAPE::ViewGetLayers( int aLayers[], int& aCount ) const diff --git a/eeschema/lib_shape.h b/eeschema/lib_shape.h index 9ae7065b3d..3d74ed3da4 100644 --- a/eeschema/lib_shape.h +++ b/eeschema/lib_shape.h @@ -120,6 +120,10 @@ public: void ViewGetLayers( int aLayers[], int& aCount ) const override; + double Similarity( const LIB_ITEM& aOther ) const override; + + bool operator==( const LIB_ITEM& aOther ) const override; + private: /** * @copydoc LIB_ITEM::compare() diff --git a/eeschema/lib_symbol.cpp b/eeschema/lib_symbol.cpp index 857df85513..706a83c9db 100644 --- a/eeschema/lib_symbol.cpp +++ b/eeschema/lib_symbol.cpp @@ -1912,3 +1912,139 @@ std::vector LIB_SYMBOL::GetUniqueUnits() return uniqueUnits; } + + +bool LIB_SYMBOL::operator==( const LIB_SYMBOL& aOther ) const +{ + if( m_libId != aOther.m_libId ) + return false; + + if( m_excludedFromBoard != aOther.m_excludedFromBoard ) + return false; + + if( m_excludedFromBOM != aOther.m_excludedFromBOM ) + return false; + + if( m_excludedFromSim != aOther.m_excludedFromSim ) + return false; + + if( m_flags != aOther.m_flags ) + return false; + + if( m_unitCount != aOther.m_unitCount ) + return false; + + if( m_pinNameOffset != aOther.m_pinNameOffset ) + return false; + + if( m_showPinNames != aOther.m_showPinNames ) + return false; + + if( m_showPinNumbers != aOther.m_showPinNumbers ) + return false; + + if( m_drawings.size() != aOther.m_drawings.size() ) + return false; + + for( auto it1 = m_drawings.begin(), it2 = aOther.m_drawings.begin(); + it1 != m_drawings.end(); ++it1, ++it2 ) + { + if( !( *it1 == *it2 ) ) + return false; + } + + const LIB_PINS thisPinList = GetAllLibPins(); + const LIB_PINS otherPinList = aOther.GetAllLibPins(); + + if( thisPinList.size() != otherPinList.size() ) + return false; + + for( auto it1 = thisPinList.begin(), it2 = otherPinList.begin(); + it1 != thisPinList.end(); ++it1, ++it2 ) + { + if( !( **it1 == **it2 ) ) + return false; + } + for( size_t ii = 0; ii < thisPinList.size(); ++ii ) + { + if( !( *thisPinList[ii] == *otherPinList[ii] ) ) + return false; + } + + return true; +} + + +double LIB_SYMBOL::Similarity( const LIB_SYMBOL& aOther ) const +{ + double similarity = 0.0; + int totalItems = 0; + + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + for( const LIB_ITEM& item : m_drawings ) + { + totalItems += 1; + double max_similarity = 0.0; + + for( const LIB_ITEM& otherItem : aOther.m_drawings ) + { + double temp_similarity = item.Similarity( otherItem ); + max_similarity = std::max( max_similarity, temp_similarity ); + + if( max_similarity == 1.0 ) + break; + } + + similarity += max_similarity; + } + + for( const LIB_PIN* pin : GetAllLibPins() ) + { + totalItems += 1; + double max_similarity = 0.0; + + for( const LIB_PIN* otherPin : aOther.GetAllLibPins() ) + { + double temp_similarity = pin->Similarity( *otherPin ); + max_similarity = std::max( max_similarity, temp_similarity ); + + if( max_similarity == 1.0 ) + break; + } + + similarity += max_similarity; + } + + if( totalItems == 0 ) + similarity = 0.0; + else + similarity /= totalItems; + + if( m_excludedFromBoard != aOther.m_excludedFromBoard ) + similarity *= 0.9; + + if( m_excludedFromBOM != aOther.m_excludedFromBOM ) + similarity *= 0.9; + + if( m_excludedFromSim != aOther.m_excludedFromSim ) + similarity *= 0.9; + + if( m_flags != aOther.m_flags ) + similarity *= 0.9; + + if( m_unitCount != aOther.m_unitCount ) + similarity *= 0.5; + + if( m_pinNameOffset != aOther.m_pinNameOffset ) + similarity *= 0.9; + + if( m_showPinNames != aOther.m_showPinNames ) + similarity *= 0.9; + + if( m_showPinNumbers != aOther.m_showPinNumbers ) + similarity *= 0.9; + + return similarity; +} \ No newline at end of file diff --git a/eeschema/lib_symbol.h b/eeschema/lib_symbol.h index 7b100623f5..db1223386d 100644 --- a/eeschema/lib_symbol.h +++ b/eeschema/lib_symbol.h @@ -711,11 +711,7 @@ public: REPORTER* aReporter = nullptr ) const; bool operator==( const LIB_SYMBOL* aSymbol ) const { return this == aSymbol; } - bool operator==( const LIB_SYMBOL& aSymbol ) const - { - return Compare( aSymbol, LIB_ITEM::COMPARE_FLAGS::EQUALITY ) == 0; - } - + bool operator==( const LIB_SYMBOL& aSymbol ) const; bool operator!=( const LIB_SYMBOL& aSymbol ) const { return Compare( aSymbol, LIB_ITEM::COMPARE_FLAGS::EQUALITY ) != 0; @@ -762,6 +758,13 @@ public: */ std::vector GetUnitDrawItems( int aUnit, int aConvert ); + /** + * Return a measure of similarity between this symbol and \a aSymbol. + * @param aSymbol is the symbol to compare to. + * + * @return a measure of similarity from 1.0 (identical) to 0.0 (no similarity). + */ + double Similarity( const LIB_SYMBOL& aSymbol ) const; #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/eeschema/lib_text.cpp b/eeschema/lib_text.cpp index a784cef711..5df27fedfc 100644 --- a/eeschema/lib_text.cpp +++ b/eeschema/lib_text.cpp @@ -500,6 +500,33 @@ void LIB_TEXT::CalcEdit( const VECTOR2I& aPosition ) } +bool LIB_TEXT::operator==( const LIB_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return false; + + const LIB_TEXT& other = static_cast( aOther ); + + return LIB_ITEM::operator==( aOther ) && EDA_TEXT::operator==( other ); +} + + +double LIB_TEXT::Similarity( const LIB_ITEM& aOther ) const +{ + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + if( aOther.Type() != Type() ) + return 0.0; + + const LIB_TEXT& other = static_cast( aOther ); + + double similarity = SimilarityBase( other ); + similarity *= EDA_TEXT::Similarity( other ); + + return similarity; +} + static struct LIB_TEXT_DESC { LIB_TEXT_DESC() diff --git a/eeschema/lib_text.h b/eeschema/lib_text.h index ea9622324c..65fd6970a4 100644 --- a/eeschema/lib_text.h +++ b/eeschema/lib_text.h @@ -113,6 +113,10 @@ public: EDA_ITEM* Clone() const override; + double Similarity( const LIB_ITEM& aOther ) const override; + + bool operator==( const LIB_ITEM& aOther ) const override; + private: /** * @copydoc LIB_ITEM::compare() diff --git a/eeschema/lib_textbox.cpp b/eeschema/lib_textbox.cpp index d162cd7257..55b5a6ad93 100644 --- a/eeschema/lib_textbox.cpp +++ b/eeschema/lib_textbox.cpp @@ -501,6 +501,35 @@ void LIB_TEXTBOX::GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector( aOther ); + + return LIB_SHAPE::operator==( other ) && EDA_TEXT::operator==( other ); +} + + +double LIB_TEXTBOX::Similarity( const LIB_ITEM& aOther ) const +{ + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + if( aOther.Type() != LIB_TEXTBOX_T ) + return 0.0; + + const LIB_TEXTBOX& other = static_cast( aOther ); + + double similarity = SimilarityBase( other ); + similarity *= LIB_SHAPE::Similarity( other ); + similarity *= EDA_TEXT::Similarity( other ); + + return similarity; +} + + void LIB_TEXTBOX::ViewGetLayers( int aLayers[], int& aCount ) const { aCount = 3; diff --git a/eeschema/lib_textbox.h b/eeschema/lib_textbox.h index be73336127..4df451e445 100644 --- a/eeschema/lib_textbox.h +++ b/eeschema/lib_textbox.h @@ -93,6 +93,10 @@ public: void ViewGetLayers( int aLayers[], int& aCount ) const override; + double Similarity( const LIB_ITEM& aOther ) const override; + + bool operator==( const LIB_ITEM& aOther ) const override; + protected: KIFONT::FONT* getDrawFont() const override; diff --git a/eeschema/sch_bitmap.cpp b/eeschema/sch_bitmap.cpp index 2dc8898f79..de9380e492 100644 --- a/eeschema/sch_bitmap.cpp +++ b/eeschema/sch_bitmap.cpp @@ -249,3 +249,45 @@ static struct SCH_BITMAP_DESC propMgr.InheritsAfter( TYPE_HASH( SCH_BITMAP ), TYPE_HASH( SCH_ITEM ) ); } } _SCH_BITMAP_DESC; + + +bool SCH_BITMAP::operator==( const SCH_ITEM& aItem ) const +{ + if( Type() != aItem.Type() ) + return false; + + const SCH_BITMAP* bitmap = static_cast( &aItem ); + + if( GetPosition() != bitmap->GetPosition() ) + return false; + + if( GetSize() != bitmap->GetSize() ) + return false; + + if( GetImage() != bitmap->GetImage() ) + return false; + + return true; +} + + +double SCH_BITMAP::Similarity( const SCH_ITEM& aItem ) const +{ + if( Type() != aItem.Type() ) + return 0.0; + + if( m_Uuid == aItem.m_Uuid ) + return 1.0; + + const SCH_BITMAP* bitmap = static_cast( &aItem ); + + if( GetImage() != bitmap->GetImage() ) + return 0.0; + + // If it is the same image but a different UUID and a different size, + // then it _might be different_. + if( GetSize() != bitmap->GetSize() ) + return 0.5; + + return 1.0; +} \ No newline at end of file diff --git a/eeschema/sch_bitmap.h b/eeschema/sch_bitmap.h index 678f1165cb..9fa782cfb6 100644 --- a/eeschema/sch_bitmap.h +++ b/eeschema/sch_bitmap.h @@ -154,6 +154,10 @@ public: EDA_ITEM* Clone() const override; + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_ITEM& aOther ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override; #endif diff --git a/eeschema/sch_bus_entry.cpp b/eeschema/sch_bus_entry.cpp index 24a692f592..41dbb4a799 100644 --- a/eeschema/sch_bus_entry.cpp +++ b/eeschema/sch_bus_entry.cpp @@ -577,6 +577,45 @@ bool SCH_BUS_WIRE_ENTRY::ConnectionPropagatesTo( const EDA_ITEM* aItem ) const return true; } +bool SCH_BUS_ENTRY_BASE::operator==( const SCH_ITEM& aItem ) const +{ + if( Type() != aItem.Type() ) + return false; + + const SCH_BUS_ENTRY_BASE* symbol = static_cast( &aItem ); + + if( GetLayer() != symbol->GetLayer() ) + return false; + + if( GetPosition() != symbol->GetPosition() ) + return false; + + if( GetEnd() != symbol->GetEnd() ) + return false; + + return true; +} + + +double SCH_BUS_ENTRY_BASE::Similarity( const SCH_ITEM& aItem ) const +{ + if( aItem.Type() != Type() ) + return 0.0; + + if( m_Uuid == aItem.m_Uuid ) + return 1.0; + + const SCH_BUS_ENTRY_BASE& other = static_cast( aItem ); + + if( GetLayer() != other.GetLayer() ) + return 0.0; + + if( GetPosition() != other.GetPosition() ) + return 0.0; + + return 1.0; +} + static struct SCH_BUS_ENTRY_DESC { diff --git a/eeschema/sch_bus_entry.h b/eeschema/sch_bus_entry.h index 6f105049aa..9322aee6b5 100644 --- a/eeschema/sch_bus_entry.h +++ b/eeschema/sch_bus_entry.h @@ -128,6 +128,10 @@ public: bool operator <( const SCH_ITEM& aItem ) const override; + double Similarity( const SCH_ITEM& aItem ) const override; + + bool operator==( const SCH_ITEM& aItem ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/eeschema/sch_field.cpp b/eeschema/sch_field.cpp index 760f29215b..49df6b3160 100644 --- a/eeschema/sch_field.cpp +++ b/eeschema/sch_field.cpp @@ -1294,6 +1294,74 @@ bool SCH_FIELD::operator <( const SCH_ITEM& aItem ) const return GetName() < field->GetName(); } +bool SCH_FIELD::operator==( const SCH_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return false; + + const SCH_FIELD& field = static_cast( aOther ); + + if( GetId() != field.GetId() ) + return false; + + if( GetPosition() != field.GetPosition() ) + return false; + + if( IsNamedVariable() != field.IsNamedVariable() ) + return false; + + if( IsNameShown() != field.IsNameShown() ) + return false; + + if( CanAutoplace() != field.CanAutoplace() ) + return false; + + if( GetText() != field.GetText() ) + return false; + + return true; +} + + +double SCH_FIELD::Similarity( const SCH_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return 0.0; + + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + const SCH_FIELD& field = static_cast( aOther ); + + double retval = 0.99; // The UUIDs are different, so we start with non-identity + + if( GetId() != field.GetId() ) + { + // We don't allow swapping of mandatory fields, so these cannot be the same item + if( GetId() < MANDATORY_FIELDS || field.GetId() < MANDATORY_FIELDS ) + return 0.0; + else + retval *= 0.5; + } + + if( GetPosition() != field.GetPosition() ) + retval *= 0.5; + + if( IsNamedVariable() != field.IsNamedVariable() ) + retval *= 0.5; + + if( IsNameShown() != field.IsNameShown() ) + retval *= 0.5; + + if( CanAutoplace() != field.CanAutoplace() ) + retval *= 0.5; + + if( GetText() != field.GetText() ) + retval *= Levenshtein( field ); + + return 1.0; +} + static struct SCH_FIELD_DESC { diff --git a/eeschema/sch_field.h b/eeschema/sch_field.h index 57d75e99d1..b6aa115373 100644 --- a/eeschema/sch_field.h +++ b/eeschema/sch_field.h @@ -275,6 +275,10 @@ public: bool operator <( const SCH_ITEM& aItem ) const override; + double Similarity( const SCH_ITEM& aItem ) const override; + + bool operator==( const SCH_ITEM& aItem ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/eeschema/sch_item.h b/eeschema/sch_item.h index f4ea44816f..d8e6b31cd4 100644 --- a/eeschema/sch_item.h +++ b/eeschema/sch_item.h @@ -279,6 +279,15 @@ public: bool RenderAsBitmap( double aWorldScale ) const override; + /** + * Return a measure of how likely the other object is to represent the same + * object. The scale runs from 0.0 (definitely different objects) to 1.0 (same) + * + * This is a pure virtual function. Derived classes must implement this. + */ + virtual double Similarity( const SCH_ITEM& aItem ) const = 0; + virtual bool operator==( const SCH_ITEM& aItem ) const = 0; + /** * Print a schematic item. * diff --git a/eeschema/sch_junction.cpp b/eeschema/sch_junction.cpp index 8fe4a37959..febd2ca444 100644 --- a/eeschema/sch_junction.cpp +++ b/eeschema/sch_junction.cpp @@ -307,6 +307,51 @@ void SCH_JUNCTION::GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector( aOther ); + + if( m_pos != other.m_pos ) + return false; + + if( m_diameter != other.m_diameter ) + return false; + + if( m_color != other.m_color ) + return false; + + return true; +} + + +double SCH_JUNCTION::Similarity( const SCH_ITEM& aOther ) const +{ + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + if( aOther.Type() != Type() ) + return 0.0; + + const SCH_JUNCTION& other = static_cast( aOther ); + + double similarity = 1.0; + + if( m_pos != other.m_pos ) + similarity *= 0.9; + + if( m_diameter != other.m_diameter ) + similarity *= 0.9; + + if( m_color != other.m_color ) + similarity *= 0.9; + + return similarity; +} + + static struct SCH_JUNCTION_DESC { SCH_JUNCTION_DESC() diff --git a/eeschema/sch_junction.h b/eeschema/sch_junction.h index 0a23e1cb70..1b229288ee 100644 --- a/eeschema/sch_junction.h +++ b/eeschema/sch_junction.h @@ -129,6 +129,10 @@ public: void GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector& aList ) override; + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_ITEM& aOther ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override; #endif diff --git a/eeschema/sch_label.cpp b/eeschema/sch_label.cpp index 29a92f4227..8f63a5c205 100644 --- a/eeschema/sch_label.cpp +++ b/eeschema/sch_label.cpp @@ -579,6 +579,69 @@ bool SCH_LABEL_BASE::IncrementLabel( int aIncrement ) } +bool SCH_LABEL_BASE::operator==( const SCH_ITEM& aOther ) const +{ + const SCH_LABEL_BASE* other = dynamic_cast( &aOther ); + + if( !other ) + return false; + + if( m_shape != other->m_shape ) + return false; + + if( m_connectionType != other->m_connectionType ) + return false; + + if( m_fields.size() != other->m_fields.size() ) + return false; + + for( size_t ii = 0; ii < m_fields.size(); ++ii ) + { + if( !( m_fields[ii] == other->m_fields[ii] ) ) + return false; + } + + return SCH_TEXT::operator==( aOther ); +} + + +double SCH_LABEL_BASE::Similarity( const SCH_ITEM& aOther ) const +{ + const SCH_LABEL_BASE* other = dynamic_cast( &aOther ); + + if( !other ) + return 0.0; + + if( m_Uuid == other->m_Uuid ) + return 1.0; + + double similarity = SCH_TEXT::Similarity( aOther ); + + if( typeid( *this ) != typeid( aOther ) ) + similarity *= 0.9; + + if( m_shape == other->m_shape ) + similarity *= 0.9; + + if( m_connectionType == other->m_connectionType ) + similarity *= 0.9; + + for( size_t ii = 0; ii < m_fields.size(); ++ii ) + { + if( ii >= other->m_fields.size() ) + break; + + similarity *= m_fields[ii].Similarity( other->m_fields[ii] ); + } + + int diff = std::abs( int( m_fields.size() ) - int( other->m_fields.size() ) ); + + similarity *= std::pow( 0.9, diff ); + + return similarity; +} + + void SCH_LABEL_BASE::AutoplaceFields( SCH_SCREEN* aScreen, bool aManual ) { int margin = GetTextOffset() * 2; diff --git a/eeschema/sch_label.h b/eeschema/sch_label.h index 55f355c53f..ae139c4fb2 100644 --- a/eeschema/sch_label.h +++ b/eeschema/sch_label.h @@ -311,6 +311,10 @@ public: */ virtual bool AutoRotateOnPlacementSupported() const = 0; + double Similarity( const SCH_ITEM& aItem ) const override; + + bool operator==( const SCH_ITEM& aItem ) const override; + protected: void cacheShownText() override; diff --git a/eeschema/sch_line.cpp b/eeschema/sch_line.cpp index 4cd7003c88..60dc411bf1 100644 --- a/eeschema/sch_line.cpp +++ b/eeschema/sch_line.cpp @@ -978,6 +978,69 @@ bool SCH_LINE::IsBus() const } +bool SCH_LINE::operator==( const SCH_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return false; + + const SCH_LINE& other = static_cast( aOther ); + + if( GetLayer() != other.GetLayer() ) + return false; + + if( m_start != other.m_start ) + return false; + + if( m_end != other.m_end ) + return false; + + if( m_stroke.GetWidth() != other.m_stroke.GetWidth() ) + return false; + + if( m_stroke.GetColor() != other.m_stroke.GetColor() ) + return false; + + if( m_stroke.GetPlotStyle() != other.m_stroke.GetPlotStyle() ) + return false; + + return true; +} + + +double SCH_LINE::Similarity( const SCH_ITEM& aOther ) const +{ + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + if( Type() != aOther.Type() ) + return 0.0; + + const SCH_LINE& other = static_cast( aOther ); + + if( GetLayer() != other.GetLayer() ) + return 0.0; + + double similarity = 1.0; + + if( m_start != other.m_start ) + similarity *= 0.9; + + if( m_end != other.m_end ) + similarity *= 0.9; + + if( m_stroke.GetWidth() != other.m_stroke.GetWidth() ) + similarity *= 0.9; + + if( m_stroke.GetColor() != other.m_stroke.GetColor() ) + similarity *= 0.9; + + if( m_stroke.GetPlotStyle() != other.m_stroke.GetPlotStyle() ) + similarity *= 0.9; + + return similarity; +} + + static struct SCH_LINE_DESC { SCH_LINE_DESC() diff --git a/eeschema/sch_line.h b/eeschema/sch_line.h index 5aad5046a2..2e1c6368bb 100644 --- a/eeschema/sch_line.h +++ b/eeschema/sch_line.h @@ -326,6 +326,10 @@ public: */ bool IsBus() const; + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_ITEM& aOther ) const override; + private: /** * @brief Recursively called function to travel through the connected wires and find a connected diff --git a/eeschema/sch_marker.h b/eeschema/sch_marker.h index 891738ac15..a3e9adf68c 100644 --- a/eeschema/sch_marker.h +++ b/eeschema/sch_marker.h @@ -120,6 +120,16 @@ public: */ bool IsLegacyMarker() const { return m_isLegacyMarker; } + double Similarity( const SCH_ITEM& aOther ) const override + { + return 0.0; + } + + bool operator==( const SCH_ITEM& aOther ) const override + { + return false; + } + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override; #endif diff --git a/eeschema/sch_no_connect.cpp b/eeschema/sch_no_connect.cpp index d92f2a0c84..0727798045 100644 --- a/eeschema/sch_no_connect.cpp +++ b/eeschema/sch_no_connect.cpp @@ -195,3 +195,34 @@ BITMAPS SCH_NO_CONNECT::GetMenuImage() const { return BITMAPS::noconn; } + + +bool SCH_NO_CONNECT::operator==( const SCH_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const SCH_NO_CONNECT* other = static_cast( &aOther ); + + if( m_pos != other->m_pos ) + return false; + + return true; +} + + +double SCH_NO_CONNECT::Similarity( const SCH_ITEM& aOther ) const +{ + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + if( aOther.Type() != Type() ) + return 0.0; + + const SCH_NO_CONNECT* other = static_cast( &aOther ); + + if( m_pos != other->m_pos ) + return 0.0; + + return 1.0; +} \ No newline at end of file diff --git a/eeschema/sch_no_connect.h b/eeschema/sch_no_connect.h index 6ad440c994..5d6d8d506a 100644 --- a/eeschema/sch_no_connect.h +++ b/eeschema/sch_no_connect.h @@ -109,6 +109,10 @@ public: EDA_ITEM* Clone() const override; + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_ITEM& aOther ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/eeschema/sch_pin.cpp b/eeschema/sch_pin.cpp index 445350e4b6..21b3877246 100644 --- a/eeschema/sch_pin.cpp +++ b/eeschema/sch_pin.cpp @@ -416,6 +416,43 @@ bool SCH_PIN::ConnectionPropagatesTo( const EDA_ITEM* aItem ) const } +bool SCH_PIN::operator==( const SCH_ITEM& aOther ) const +{ + if( aOther.Type() != SCH_PIN_T ) + return false; + + const SCH_PIN& other = static_cast( aOther ); + + if( m_number != other.m_number ) + return false; + + if( m_position != other.m_position ) + return false; + + return m_libPin == other.m_libPin; +} + + +double SCH_PIN::Similarity( const SCH_ITEM& aOther ) const +{ + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + if( aOther.Type() != SCH_PIN_T ) + return 0.0; + + const SCH_PIN& other = static_cast( aOther ); + + if( m_number != other.m_number ) + return 0.0; + + if( m_position != other.m_position ) + return 0.0; + + return m_libPin->Similarity( *other.m_libPin ); +} + + static struct SCH_PIN_DESC { SCH_PIN_DESC() diff --git a/eeschema/sch_pin.h b/eeschema/sch_pin.h index 2dc058874a..9867f95db8 100644 --- a/eeschema/sch_pin.h +++ b/eeschema/sch_pin.h @@ -164,6 +164,10 @@ public: const wxString& GetOperatingPoint() const { return m_operatingPoint; } void SetOperatingPoint( const wxString& aText ) { m_operatingPoint = aText; } + double Similarity( const SCH_ITEM& aItem ) const override; + + bool operator==( const SCH_ITEM& aItem ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override {} #endif diff --git a/eeschema/sch_shape.cpp b/eeschema/sch_shape.cpp index 3ed7aa01dd..b93ebca00c 100644 --- a/eeschema/sch_shape.cpp +++ b/eeschema/sch_shape.cpp @@ -502,6 +502,30 @@ void SCH_SHAPE::AddPoint( const VECTOR2I& aPosition ) } +bool SCH_SHAPE::operator==( const SCH_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const SCH_SHAPE& other = static_cast( aOther ); + + return EDA_SHAPE::operator==( other ); +} + + +double SCH_SHAPE::Similarity( const SCH_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const SCH_SHAPE& other = static_cast( aOther ); + + double similarity = EDA_SHAPE::Similarity( other ); + + return similarity; +} + + static struct SCH_SHAPE_DESC { SCH_SHAPE_DESC() diff --git a/eeschema/sch_shape.h b/eeschema/sch_shape.h index 6de173413c..9dc07004d9 100644 --- a/eeschema/sch_shape.h +++ b/eeschema/sch_shape.h @@ -107,6 +107,10 @@ public: void ViewGetLayers( int aLayers[], int& aCount ) const override; + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_ITEM& aOther ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/eeschema/sch_sheet.cpp b/eeschema/sch_sheet.cpp index fa0c138125..b45e3b99a1 100644 --- a/eeschema/sch_sheet.cpp +++ b/eeschema/sch_sheet.cpp @@ -1387,6 +1387,55 @@ int SCH_SHEET::ComparePageNum( const wxString& aPageNumberA, const wxString& aPa } +bool SCH_SHEET::operator==( const SCH_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return false; + + const SCH_SHEET* other = static_cast( &aOther ); + + if( m_pos != other->m_pos ) + return false; + + if( m_size != other->m_size ) + return false; + + if( GetBorderColor() != other->GetBorderColor() ) + return false; + + if( GetBackgroundColor() != other->GetBackgroundColor() ) + return false; + + if( GetBorderWidth() != other->GetBorderWidth() ) + return false; + + if( GetFields().size() != other->GetFields().size() ) + return false; + + for( size_t i = 0; i < GetFields().size(); ++i ) + { + if( !( GetFields()[i] == other->GetFields()[i] ) ) + return false; + } + + return true; +} + + +double SCH_SHEET::Similarity( const SCH_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return 0.0; + + const SCH_SHEET* other = static_cast( &aOther ); + + if( m_screen->GetFileName() == other->m_screen->GetFileName() ) + return 1.0; + + return 0.0; +} + + #if defined(DEBUG) void SCH_SHEET::Show( int nestLevel, std::ostream& os ) const diff --git a/eeschema/sch_sheet.h b/eeschema/sch_sheet.h index 43e6b098c9..7991b23135 100644 --- a/eeschema/sch_sheet.h +++ b/eeschema/sch_sheet.h @@ -418,6 +418,10 @@ public: */ static int ComparePageNum( const wxString& aPageNumberA, const wxString& aPageNumberB ); + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_ITEM& aOther ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override; #endif diff --git a/eeschema/sch_sheet_path.cpp b/eeschema/sch_sheet_path.cpp index 8bc75819ac..c90dcd46a4 100644 --- a/eeschema/sch_sheet_path.cpp +++ b/eeschema/sch_sheet_path.cpp @@ -80,6 +80,16 @@ public: void MirrorVertically( int aCenter ) override {} void Rotate( const VECTOR2I& aCenter ) override {} + double Similarity( const SCH_ITEM& aOther ) const override + { + return 0.0; + } + + bool operator==( const SCH_ITEM& aOther ) const override + { + return false; + } + #if defined(DEBUG) void Show( int , std::ostream& ) const override {} #endif diff --git a/eeschema/sch_sheet_pin.cpp b/eeschema/sch_sheet_pin.cpp index 1dddb167f3..cdbeb2244a 100644 --- a/eeschema/sch_sheet_pin.cpp +++ b/eeschema/sch_sheet_pin.cpp @@ -356,6 +356,39 @@ bool SCH_SHEET_PIN::HitTest( const VECTOR2I& aPoint, int aAccuracy ) const } +bool SCH_SHEET_PIN::operator==( const SCH_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const SCH_SHEET_PIN* other = static_cast( &aOther ); + + return m_edge == other->m_edge && m_number == other->m_number + && SCH_HIERLABEL::operator==( aOther ); +} + + +double SCH_SHEET_PIN::Similarity( const SCH_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const SCH_SHEET_PIN* other = static_cast( &aOther ); + + double similarity = 1.0; + + if( m_edge != other->m_edge ) + similarity *= 0.9; + + if( m_number != other->m_number ) + similarity *= 0.9; + + similarity *= SCH_HIERLABEL::Similarity( aOther ); + + return similarity; +} + + #if defined(DEBUG) void SCH_SHEET_PIN::Show( int nestLevel, std::ostream& os ) const diff --git a/eeschema/sch_sheet_pin.h b/eeschema/sch_sheet_pin.h index 9b90809c65..7b64223baa 100644 --- a/eeschema/sch_sheet_pin.h +++ b/eeschema/sch_sheet_pin.h @@ -200,6 +200,10 @@ public: EDA_ITEM* Clone() const override; + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_ITEM& aOther ) const override; + private: int m_number; ///< Label number use for saving sheet label to file. ///< Sheet label numbering begins at 2. diff --git a/eeschema/sch_symbol.cpp b/eeschema/sch_symbol.cpp index 70f999ceae..b76c2016ed 100644 --- a/eeschema/sch_symbol.cpp +++ b/eeschema/sch_symbol.cpp @@ -2519,6 +2519,61 @@ bool SCH_SYMBOL::IsPower() const } +bool SCH_SYMBOL::operator==( const SCH_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return false; + + auto symbol = static_cast( aOther ); + + if( GetLibId() != symbol.GetLibId() ) + return false; + + if( GetPosition() != symbol.GetPosition() ) + return false; + + if( GetUnit() != symbol.GetUnit() ) + return false; + + if( GetConvert() != symbol.GetConvert() ) + return false; + + if( GetTransform() != symbol.GetTransform() ) + return false; + + if( GetFields() != symbol.GetFields() ) + return false; + + if( m_pins.size() != symbol.m_pins.size() ) + return false; + + for( unsigned i = 0; i < m_pins.size(); ++i ) + { + if( !( *m_pins[i] == *symbol.m_pins[i] ) ) + return false; + } + + return true; +} + + +double SCH_SYMBOL::Similarity( const SCH_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return 0.0; + + auto symbol = static_cast( aOther ); + + if( GetLibId() != symbol.GetLibId() ) + return 0.0; + + if( GetPosition() == symbol.GetPosition() ) + return 1.0; + + return 0.0; +} + + static struct SCH_SYMBOL_DESC { SCH_SYMBOL_DESC() diff --git a/eeschema/sch_symbol.h b/eeschema/sch_symbol.h index d079676f21..1872c21eb3 100644 --- a/eeschema/sch_symbol.h +++ b/eeschema/sch_symbol.h @@ -758,6 +758,10 @@ public: bool IsPower() const; + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_ITEM& aOther ) const override; + private: BOX2I doGetBoundingBox( bool aIncludePins, bool aIncludeFields ) const; diff --git a/eeschema/sch_text.cpp b/eeschema/sch_text.cpp index ab3990b4af..173ab1e920 100644 --- a/eeschema/sch_text.cpp +++ b/eeschema/sch_text.cpp @@ -438,6 +438,43 @@ void SCH_TEXT::GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector( &aOther ); + + if( GetLayer() != other->GetLayer() ) + return false; + + if( GetExcludedFromSim() != other->GetExcludedFromSim() ) + return false; + + return EDA_TEXT::operator==( *other ); +} + + +double SCH_TEXT::Similarity( const SCH_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return 0.0; + + const SCH_TEXT* other = static_cast( &aOther ); + + double retval = 1.0; + + if( GetLayer() != other->GetLayer() ) + retval *= 0.9; + + if( GetExcludedFromSim() != other->GetExcludedFromSim() ) + retval *= 0.9; + + retval *= EDA_TEXT::Similarity( *other ); + + return retval; +} + #if defined(DEBUG) diff --git a/eeschema/sch_text.h b/eeschema/sch_text.h index f49e2c2381..ffb6b191d8 100644 --- a/eeschema/sch_text.h +++ b/eeschema/sch_text.h @@ -138,6 +138,10 @@ public: void GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector& aList ) override; + virtual double Similarity( const SCH_ITEM& aItem ) const override; + + virtual bool operator==( const SCH_ITEM& aItem ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override; #endif diff --git a/eeschema/sch_textbox.cpp b/eeschema/sch_textbox.cpp index 214f68f577..8f2905f9e6 100644 --- a/eeschema/sch_textbox.cpp +++ b/eeschema/sch_textbox.cpp @@ -466,6 +466,39 @@ void SCH_TEXTBOX::GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector( aOther ); + + if( m_excludedFromSim != other.m_excludedFromSim ) + return false; + + return SCH_SHAPE::operator==( aOther ) && EDA_TEXT::operator==( other ); +} + + +double SCH_TEXTBOX::Similarity( const SCH_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + auto other = static_cast( aOther ); + + double similarity = 1.0; + + if( m_excludedFromSim != other.m_excludedFromSim ) + similarity *= 0.9; + + similarity *= SCH_SHAPE::Similarity( aOther ); + similarity *= EDA_TEXT::Similarity( other ); + + return similarity; +} + + static struct SCH_TEXTBOX_DESC { SCH_TEXTBOX_DESC() diff --git a/eeschema/sch_textbox.h b/eeschema/sch_textbox.h index c116bbc728..4a8cc436cf 100644 --- a/eeschema/sch_textbox.h +++ b/eeschema/sch_textbox.h @@ -120,6 +120,10 @@ public: void GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector& aList ) override; + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_ITEM& aOther ) const override; + protected: KIFONT::FONT* getDrawFont() const override; diff --git a/eeschema/symbol_diff_frame.cpp b/eeschema/symbol_diff_frame.cpp new file mode 100644 index 0000000000..53d371e08f --- /dev/null +++ b/eeschema/symbol_diff_frame.cpp @@ -0,0 +1,211 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * https://www.gnu.org/licenses/gpl-3.0.html + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ +#include +#include + +#include + +BEGIN_EVENT_TABLE( SYMBOL_DIFF_FRAME, SCH_BASE_FRAME ) + // Window events + EVT_SIZE( SYMBOL_DIFF_FRAME::OnSize ) + EVT_ACTIVATE( SYMBOL_DIFF_FRAME::OnActivate ) + + // Toolbar events + // EVT_TOOL( ID_LIBVIEW_SELECT_PART, SYMBOL_DIFF_FRAME::OnSelectSymbol ) + // EVT_TOOL( ID_LIBVIEW_NEXT, SYMBOL_DIFF_FRAME::onSelectNextSymbol ) + // EVT_TOOL( ID_LIBVIEW_PREVIOUS, SYMBOL_DIFF_FRAME::onSelectPreviousSymbol ) + // EVT_CHOICE( ID_LIBVIEW_SELECT_UNIT_NUMBER, SYMBOL_DIFF_FRAME::onSelectSymbolUnit ) + + // listbox events + // EVT_TEXT( ID_LIBVIEW_LIB_FILTER, SYMBOL_DIFF_FRAME::OnLibFilter ) + // EVT_LISTBOX( ID_LIBVIEW_LIB_LIST, SYMBOL_DIFF_FRAME::ClickOnLibList ) + // EVT_TEXT( ID_LIBVIEW_SYM_FILTER, SYMBOL_DIFF_FRAME::OnSymFilter ) + // EVT_LISTBOX( ID_LIBVIEW_SYM_LIST, SYMBOL_DIFF_FRAME::ClickOnSymbolList ) + // EVT_LISTBOX_DCLICK( ID_LIBVIEW_SYM_LIST, SYMBOL_DIFF_FRAME::DClickOnSymbolList ) + + // Menu (and/or hotkey) events + EVT_MENU( wxID_CLOSE, SYMBOL_DIFF_FRAME::CloseLibraryViewer ) + + EVT_UPDATE_UI( ID_LIBVIEW_SELECT_UNIT_NUMBER, SYMBOL_DIFF_FRAME::onUpdateUnitChoice ) + +END_EVENT_TABLE() + +#define LIB_VIEW_STYLE ( KICAD_DEFAULT_DRAWFRAME_STYLE ) +#define LIB_VIEW_STYLE_MODAL ( KICAD_DEFAULT_DRAWFRAME_STYLE | wxFRAME_FLOAT_ON_PARENT ) + +SYMBOL_DIFF_FRAME::SYMBOL_DIFF_FRAME( KIWAY* aKiway, wxWindow* aParent, FRAME_T aFrameType, + const wxString& aLibraryName ) : + SCH_BASE_FRAME( aKiway, aParent, aFrameType, _( "Symbol Library Browser" ), + wxDefaultPosition, wxDefaultSize, wxS( "SymbolDiff" ) ) +{ + SetModal( true ); + + m_aboutTitle = _HKI( "KiCad Symbol Difference Viewer" ); + + // Give an icon + wxIcon icon; + icon.CopyFromBitmap( KiBitmap( BITMAPS::library_browser ) ); + SetIcon( icon ); + + + SetScreen( new SCH_SCREEN ); + GetScreen()->m_Center = true; // Axis origin centered on screen. + LoadSettings( config() ); + + // Ensure axis are always drawn (initial default display was not drawn) + KIGFX::GAL_DISPLAY_OPTIONS& gal_opts = GetGalDisplayOptions(); + gal_opts.m_axesEnabled = true; + gal_opts.m_gridMinSpacing = 10.0; + gal_opts.NotifyChanged(); + + GetRenderSettings()->LoadColors( GetColorSettings() ); + GetCanvas()->GetGAL()->SetAxesColor( m_colorSettings->GetColor( LAYER_SCHEMATIC_GRID_AXES ) ); + + GetRenderSettings()->SetDefaultPenWidth( DEFAULT_LINE_WIDTH_MILS * schIUScale.IU_PER_MILS ); + + setupTools(); + setupUIConditions(); + + ReCreateHToolbar(); + ReCreateVToolbar(); + ReCreateMenuBar(); + + wxPanel* libPanel = new wxPanel( this ); + wxSizer* libSizer = new wxBoxSizer( wxVERTICAL ); + + + wxPanel* symbolPanel = new wxPanel( this ); + wxSizer* symbolSizer = new wxBoxSizer( wxVERTICAL ); + + m_symbolFilter = new wxSearchCtrl( symbolPanel, ID_LIBVIEW_SYM_FILTER, wxEmptyString, + wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER ); + m_symbolFilter->SetDescriptiveText( _( "Filter" ) ); + m_symbolFilter->SetToolTip( + _( "Filter on symbol name, keywords, description and pin count.\n" + "Search terms are separated by spaces. All search terms must match.\n" + "A term which is a number will also match against the pin count." ) ); + symbolSizer->Add( m_symbolFilter, 0, wxEXPAND, 5 ); + +#ifdef __WXGTK__ + // wxSearchCtrl vertical height is not calculated correctly on some GTK setups + // See https://gitlab.com/kicad/code/kicad/-/issues/9019 + m_libFilter->SetMinSize( wxSize( -1, GetTextExtent( wxT( "qb" ) ).y + 10 ) ); + m_symbolFilter->SetMinSize( wxSize( -1, GetTextExtent( wxT( "qb" ) ).y + 10 ) ); +#endif + + m_symbolList = new WX_LISTBOX( symbolPanel, ID_LIBVIEW_SYM_LIST, wxDefaultPosition, wxDefaultSize, + 0, nullptr, wxLB_HSCROLL | wxNO_BORDER ); + symbolSizer->Add( m_symbolList, 1, wxEXPAND, 5 ); + + symbolPanel->SetSizer( symbolSizer ); + symbolPanel->Fit(); + + // Preload libraries + loadAllLibraries(); + + if( aLibraryName.empty() ) + { + ReCreateLibList(); + } + else + { + m_currentSymbol.SetLibNickname( aLibraryName ); + m_currentSymbol.SetLibItemName( "" ); + m_unit = 1; + m_convert = 1; + } + + m_selection_changed = false; + + DisplayLibInfos(); + + m_auimgr.SetManagedWindow( this ); + + CreateInfoBar(); + + // Manage main toolbar + m_auimgr.AddPane( m_mainToolBar, EDA_PANE().HToolbar().Name( "MainToolbar" ).Top().Layer(6) ); + m_auimgr.AddPane( m_messagePanel, EDA_PANE().Messages().Name( "MsgPanel" ) .Bottom().Layer(6) ); + + m_auimgr.AddPane( libPanel, EDA_PANE().Palette().Name( "Libraries" ).Left().Layer(2) + .CaptionVisible( false ).MinSize( 100, -1 ).BestSize( 200, -1 ) ); + m_auimgr.AddPane( symbolPanel, EDA_PANE().Palette().Name( "Symbols" ).Left().Layer(1) + .CaptionVisible( false ).MinSize( 100, -1 ).BestSize( 300, -1 ) ); + + m_auimgr.AddPane( GetCanvas(), EDA_PANE().Canvas().Name( "DrawFrame" ).Center() ); + + m_auimgr.GetPane( libPanel ).Show( aLibraryName.empty() ); + + m_auimgr.Update(); + + if( m_libListWidth > 0 ) + SetAuiPaneSize( m_auimgr, m_auimgr.GetPane( "Libraries" ), m_libListWidth, -1 ); + + if( m_symbolListWidth > 0 ) + SetAuiPaneSize( m_auimgr, m_auimgr.GetPane( "Symbols" ), m_symbolListWidth, -1 ); + + FinishAUIInitialization(); + + if( !IsModal() ) // For modal mode, calling ShowModal() will show this frame + { + Raise(); + Show( true ); + } + + SyncView(); + GetCanvas()->SetCanFocus( false ); + + setupUnits( config() ); + + // Set the working/draw area size to display a symbol to a reasonable value: + // A 450mm x 450mm with a origin at the area center looks like a large working area + double max_size_x = schIUScale.mmToIU( 450 ); + double max_size_y = schIUScale.mmToIU( 450 ); + BOX2D bbox; + bbox.SetOrigin( -max_size_x / 2, -max_size_y / 2 ); + bbox.SetSize( max_size_x, max_size_y ); + GetCanvas()->GetView()->SetBoundary( bbox ); + GetToolManager()->RunAction( ACTIONS::zoomFitScreen ); + + // If a symbol was previously selected in m_symbolList from a previous run, show it + wxString symbName = m_symbolList->GetStringSelection(); + + if( !symbName.IsEmpty() ) + { + SetSelectedSymbol( symbName ); + updatePreviewSymbol(); + } +} + + +SYMBOL_DIFF_FRAME::~SYMBOL_DIFF_FRAME() +{ + // Shutdown all running tools + if( m_toolManager ) + m_toolManager->ShutdownAllTools(); + + if( m_previewItem ) + { + GetCanvas()->GetView()->Remove( m_previewItem.get() ); + m_previewItem = nullptr; + } +} \ No newline at end of file diff --git a/eeschema/symbol_diff_frame.h b/eeschema/symbol_diff_frame.h new file mode 100644 index 0000000000..50961efd42 --- /dev/null +++ b/eeschema/symbol_diff_frame.h @@ -0,0 +1,97 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * https://www.gnu.org/licenses/gpl-3.0.html + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef __SYMBOL_DIFF_FRAME_H_ +#define __SYMBOL_DIFF_FRAME_H_ + +#include + +/** + * Symbol library viewer main window. + */ +class SYMBOL_DIFF_FRAME : public SCH_BASE_FRAME +{ +public: + + /** + * @param aKiway + * @param aParent is the parent frame of the viewer. + * @param aFrameType must be either #FRAME_SCH_LIB_VIEWER or #FRAME_SCH_LIB_VIEWER_MODAL. + * @param aLibrary is the library to open when starting (default = NULL). + */ + SYMBOL_DIFF_FRAME( KIWAY* aKiway, wxWindow* aParent, FRAME_T aFrameType, + const wxString& aLibraryName = wxEmptyString ); + + ~SYMBOL_DIFF_FRAME(); + + /** + * Runs the symbol viewer as a modal dialog. + * + * @param aSymbol an optional FPID string to initialize the viewer with and to + * return a selected footprint through. + */ + bool ShowModal( wxString* aSymbol, wxWindow* aParent ) override; + + /** + * Send the selected symbol back to the caller. + */ + void FinishModal(); + + void OnSize( wxSizeEvent& event ) override; + + void doCloseWindow() override; + void ReCreateHToolbar() override; + void ReCreateVToolbar() override; + void ReCreateOptToolbar() override {} + + void LoadSettings( APP_SETTINGS_BASE* aCfg ) override; + void SaveSettings( APP_SETTINGS_BASE* aCfg ) override; + + WINDOW_SETTINGS* GetWindowSettings( APP_SETTINGS_BASE* aCfg ) override; + + void CommonSettingsChanged( bool aEnvVarsChanged, bool aTextVarsChanged ) override; + + const BOX2I GetDocumentExtents( bool aIncludeAllVisible = true ) const override; + + SELECTION& GetCurrentSelection() override; + + void KiwayMailIn( KIWAY_EXPRESS& mail ) override; + +protected: + void setupUIConditions() override; + + void doReCreateMenuBar() override; + +private: + // Set up the tool framework. + void setupTools(); + + /** + * Called when the frame is activated to reload the libraries and symbol lists + * that can be changed by the schematic editor or the library editor. + */ + void OnActivate( wxActivateEvent& event ); + + + DECLARE_EVENT_TABLE() +}; +#endif \ No newline at end of file diff --git a/include/advanced_config.h b/include/advanced_config.h index 6886ad1c09..0967ce3b50 100644 --- a/include/advanced_config.h +++ b/include/advanced_config.h @@ -279,6 +279,11 @@ public: */ bool m_EnableGenerators; + /** + * When true, enable git integration + */ + bool m_EnableGit; + ///@} diff --git a/include/bitmaps/bitmaps_list.h b/include/bitmaps/bitmaps_list.h index 55dda436f5..b8f23dd0be 100644 --- a/include/bitmaps/bitmaps_list.h +++ b/include/bitmaps/bitmaps_list.h @@ -218,6 +218,13 @@ enum class BITMAPS : unsigned int gerber_file, gerbview_clear_layers, gerbview_show_negative_objects, + git_add, + git_changed_ahead, + git_modified, + git_conflict, + git_delete, + git_good_check, + git_out_of_date, go_down, go_up, grid, diff --git a/include/board_item.h b/include/board_item.h index 29e6aa1b5b..c3fb34d8d7 100644 --- a/include/board_item.h +++ b/include/board_item.h @@ -136,6 +136,15 @@ public: return false; } + /** + * Return a measure of how likely the other object is to represent the same + * object. The scale runs from 0.0 (definitely different objects) to 1.0 (same) + * + * This is a pure virtual function. Derived classes must implement this. + */ + virtual double Similarity( const BOARD_ITEM& aItem ) const = 0; + virtual bool operator==( const BOARD_ITEM& aItem ) const = 0; + /** * @return true if the object is on any copper layer, false otherwise. */ @@ -416,6 +425,16 @@ public: return item; } + double Similarity( const BOARD_ITEM& aItem ) const override + { + return ( this == &aItem ) ? 1.0 : 0.0; + } + + bool operator==( const BOARD_ITEM& aItem ) const override + { + return ( this == &aItem ); + } + #if defined(DEBUG) void Show( int , std::ostream& ) const override {} #endif diff --git a/include/eda_item.h b/include/eda_item.h index 1d34b851f1..26ba280c83 100644 --- a/include/eda_item.h +++ b/include/eda_item.h @@ -503,6 +503,22 @@ private: */ inline EDA_ITEM* new_clone( const EDA_ITEM& aItem ) { return aItem.Clone(); } +/** + * Comparison functor for sorting EDA_ITEM pointers by their UUID. + */ +struct CompareByUuid +{ + bool operator()(const EDA_ITEM* item1, const EDA_ITEM* item2) const + { + assert( item1 != nullptr && item2 != nullptr ); + + if( item1->m_Uuid == item2->m_Uuid ) + return item1 < item2; + + return item1->m_Uuid < item2->m_Uuid; + } +}; + /** * Define list of drawing items for screens. @@ -512,4 +528,6 @@ inline EDA_ITEM* new_clone( const EDA_ITEM& aItem ) { return aItem.Clone(); } */ typedef std::vector< EDA_ITEM* > EDA_ITEMS; +typedef std::set< EDA_ITEM*, CompareByUuid > EDA_ITEM_SET; + #endif // EDA_ITEM_H diff --git a/include/eda_shape.h b/include/eda_shape.h index 018b22f3b7..4388f7508d 100644 --- a/include/eda_shape.h +++ b/include/eda_shape.h @@ -331,6 +331,10 @@ public: int Compare( const EDA_SHAPE* aOther ) const; + double Similarity( const EDA_SHAPE& aOther ) const; + + bool operator==( const EDA_SHAPE& aOther ) const; + protected: void setPosition( const VECTOR2I& aPos ); VECTOR2I getPosition() const; diff --git a/include/eda_text.h b/include/eda_text.h index 2e37dc173e..fa83069dd0 100644 --- a/include/eda_text.h +++ b/include/eda_text.h @@ -303,6 +303,16 @@ public: */ void GetLinePositions( std::vector& aPositions, int aLineCount ) const; + /** + * Return the levenstein distance between two texts. + * + * Return a value of 0.0 - 1.0 where 1.0 is a perfect match. + */ + double Levenshtein( const EDA_TEXT& aOther ) const; + + + double Similarity( const EDA_TEXT& aOther ) const; + /** * Output the object to \a aFormatter in s-expression form. * diff --git a/include/frame_type.h b/include/frame_type.h index cb20489465..9970d58da6 100644 --- a/include/frame_type.h +++ b/include/frame_type.h @@ -36,6 +36,8 @@ enum FRAME_T FRAME_SCH_VIEWER, FRAME_SYMBOL_CHOOSER, FRAME_SIMULATOR, + FRAME_SCH_DIFF, + FRAME_SYM_DIFF, FRAME_PCB_EDITOR, FRAME_FOOTPRINT_EDITOR, @@ -44,6 +46,8 @@ enum FRAME_T FRAME_FOOTPRINT_WIZARD, FRAME_PCB_DISPLAY3D, FRAME_FOOTPRINT_PREVIEW, + FRAME_PCB_DIFF, + FRAME_FOOTPRINT_DIFF, FRAME_CVPCB, FRAME_CVPCB_DISPLAY, diff --git a/include/gestfich.h b/include/gestfich.h index ce488518ed..52d558d485 100644 --- a/include/gestfich.h +++ b/include/gestfich.h @@ -92,4 +92,11 @@ wxString FindKicadFile( const wxString& shortname ); */ extern wxString QuoteFullPath( wxFileName& fn, wxPathFormat format = wxPATH_NATIVE ); + +/** + * Removes the directory \a aDirName and all its contents including + * subdirectories and their files + */ +bool RmDirRecursive( const wxString& aDirName, wxString* aErrors = nullptr ); + #endif /* GESTFICH_H */ diff --git a/include/pcb_group.h b/include/pcb_group.h index dbe08c11e2..d277edbfdc 100644 --- a/include/pcb_group.h +++ b/include/pcb_group.h @@ -102,6 +102,10 @@ public: static bool WithinScope( BOARD_ITEM* aItem, PCB_GROUP* aScope, bool isFootprintEditor ); + double Similarity( const BOARD_ITEM& aOther ) const override; + + bool operator==( const BOARD_ITEM& aOther ) const override; + #if defined( DEBUG ) void Show( int nestLevel, std::ostream& os ) const override { diff --git a/include/project/project_local_settings.h b/include/project/project_local_settings.h index 77ba3c37c0..73505429e4 100644 --- a/include/project/project_local_settings.h +++ b/include/project/project_local_settings.h @@ -135,6 +135,12 @@ public: /// State of the selection filter widget SELECTION_FILTER_OPTIONS m_SelectionFilter; + // Upstream git repo info + wxString m_GitRepoUsername; + wxString m_GitRepoPassword; + wxString m_GitRepoType; + wxString m_GitSSHKey; + private: /// A link to the owning project PROJECT* m_project; diff --git a/include/scoped_set_reset.h b/include/scoped_set_reset.h index ff84795e8b..be02be67be 100644 --- a/include/scoped_set_reset.h +++ b/include/scoped_set_reset.h @@ -24,6 +24,8 @@ #ifndef __SCOPED_SET_RESET_H #define __SCOPED_SET_RESET_H +#include + /** * RAII class that sets an value at construction and resets it to the original value * at destruction. @@ -65,4 +67,30 @@ private: VAL_TYPE& m_target; }; + +/** + * RAII class that executes a function at construction and another at destruction. + * + * Useful to ensure cleanup code is executed even if an exception is thrown. +*/ +template +class SCOPED_EXECUTION +{ +public: + SCOPED_EXECUTION(Func initFunc, Func destroyFunc) : + m_initFunc(initFunc), m_destroyFunc(destroyFunc) + { + m_initFunc(); + } + + ~SCOPED_EXECUTION() + { + m_destroyFunc(); + } + +private: + Func m_initFunc; + Func m_destroyFunc; +}; + #endif // __SCOPED_SET_RESET_H diff --git a/include/settings/common_settings.h b/include/settings/common_settings.h index e2194d7319..90c9c97821 100644 --- a/include/settings/common_settings.h +++ b/include/settings/common_settings.h @@ -145,6 +145,25 @@ public: int sash_pos; }; + struct GIT_REPOSITORY + { + wxString name; + wxString path; + wxString authType; + wxString username; + wxString ssh_path; + bool active; + bool checkValid; + }; + + struct GIT + { + std::vector repositories; + bool useDefaultAuthor; + wxString authorName; + wxString authorEmail; + }; + COMMON_SETTINGS(); virtual ~COMMON_SETTINGS() {} @@ -193,6 +212,8 @@ public: NETCLASS_PANEL m_NetclassPanel; PACKAGE_MANAGER m_PackageManager; + + GIT m_Git; }; #endif diff --git a/kicad/kicad.cpp b/kicad/kicad.cpp index fde8d69688..05e0353848 100644 --- a/kicad/kicad.cpp +++ b/kicad/kicad.cpp @@ -47,6 +47,7 @@ #include #include +#include #include #include "pgm_kicad.h" @@ -105,6 +106,9 @@ bool PGM_KICAD::OnPgmInit() } #endif + // Initialize the git library before trying to initialize individual programs + git_libgit2_init(); + static const wxCmdLineEntryDesc desc[] = { { wxCMD_LINE_OPTION, "f", "frame", "Frame to load", wxCMD_LINE_VAL_STRING, 0 }, { wxCMD_LINE_PARAM, nullptr, nullptr, "File to load", wxCMD_LINE_VAL_STRING, @@ -378,6 +382,7 @@ void PGM_KICAD::OnPgmExit() // especially wxSingleInstanceCheckerImpl earlier than wxApp and earlier // than static destruction would. Destroy(); + git_libgit2_shutdown(); } diff --git a/kicad/kicad_id.h b/kicad/kicad_id.h index 326a723aa2..60e6cd8228 100644 --- a/kicad/kicad_id.h +++ b/kicad/kicad_id.h @@ -73,6 +73,27 @@ enum id_kicad_frm { ID_IMPORT_EASYEDA_PROJECT, ID_IMPORT_EASYEDAPRO_PROJECT, + ID_GIT_INITIALIZE_PROJECT, // Initialize a new git repository in an existing project + ID_GIT_CLONE_PROJECT, // Clone a project from a remote repository + ID_GIT_COMMIT_PROJECT, // Commit all files in the project + ID_GIT_COMMIT_FILE, // Commit a single file + ID_GIT_SYNC_PROJECT, // Sync the project with the remote repository (pull and push -- same as Update) + ID_GIT_FETCH, // Fetch the remote repository (without merging -- this is the same as Refresh) + ID_GIT_PUSH, // Push the local repository to the remote repository + ID_GIT_PULL, // Pull the remote repository to the local repository + ID_GIT_RESOLVE_CONFLICT, // Present the user with a resolve conflicts dialog (ours/theirs/merge) + ID_GIT_REVERT_LOCAL, // Revert the local repository to the last commit + ID_GIT_COMPARE, // Compare the current project to a different branch or commit in the git repository + ID_GIT_REMOVE_VCS, // Remove the git repository data from the project directory (rm .git) + ID_GIT_ADD_TO_INDEX, // Add a file to the git index + ID_GIT_REMOVE_FROM_INDEX, // Remove a file from the git index + ID_GIT_SWITCH_BRANCH, // Switch the local repository to a different branch + ID_GIT_SWITCH_QUICK1, // Switch the local repository to the first quick branch + ID_GIT_SWITCH_QUICK2, // Switch the local repository to the second quick branch + ID_GIT_SWITCH_QUICK3, // Switch the local repository to the third quick branch + ID_GIT_SWITCH_QUICK4, // Switch the local repository to the fourth quick branch + ID_GIT_SWITCH_QUICK5, // Switch the local repository to the fifth quick branch + // Please, verify: the number of items in this list should be // less than ROOM_FOR_KICADMANAGER (see id.h) ID_KICADMANAGER_END_LIST diff --git a/kicad/menubar.cpp b/kicad/menubar.cpp index 0515ac5bc7..6114abfd33 100644 --- a/kicad/menubar.cpp +++ b/kicad/menubar.cpp @@ -74,6 +74,7 @@ void KICAD_MANAGER_FRAME::doReCreateMenuBar() fileMenu->Add( KICAD_MANAGER_ACTIONS::newProject ); fileMenu->Add( KICAD_MANAGER_ACTIONS::newFromTemplate ); + fileMenu->Add( KICAD_MANAGER_ACTIONS::newFromRepository ); if( wxDir::Exists( PATHS::GetStockDemosPath() ) ) { diff --git a/kicad/project_tree.cpp b/kicad/project_tree.cpp index 148c306b56..3d6f215048 100644 --- a/kicad/project_tree.cpp +++ b/kicad/project_tree.cpp @@ -24,6 +24,7 @@ #include +#include #include #include "project_tree_item.h" @@ -48,6 +49,7 @@ PROJECT_TREE::PROJECT_TREE( PROJECT_TREE_PANE* parent ) : m_imageList( nullptr ) { m_projectTreePane = parent; + m_gitCommon = new KIGIT_COMMON( nullptr ); // Make sure the GUI font scales properly on GTK SetFont( KIUI::GetControlFont( this ) ); @@ -65,6 +67,7 @@ PROJECT_TREE::~PROJECT_TREE() void PROJECT_TREE::LoadIcons() { delete m_imageList; + delete m_statusImageList; // icons size is not know (depending on they are built) // so get it: @@ -107,6 +110,28 @@ void PROJECT_TREE::LoadIcons() m_imageList->Add( KiBitmap( BITMAPS::zip ) ); // ZIP_ARCHIVE SetImageList( m_imageList ); + + // Make an image list containing small icons + dummy = KiBitmap( BITMAPS::git_add ); + iconsize.x = dummy.GetWidth(); + iconsize.y = dummy.GetHeight(); + + wxBitmap blank_bitmap( iconsize.x, iconsize.y ); + + m_statusImageList = new wxImageList( iconsize.x, iconsize.y, true, + static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_LAST ) ); + + m_statusImageList->Add( blank_bitmap ); // GIT_STATUS_UNTRACKED + m_statusImageList->Add( KiBitmap( BITMAPS::git_good_check ) ); // GIT_STATUS_CURRENT + m_statusImageList->Add( KiBitmap( BITMAPS::git_modified ) ); // GIT_STATUS_MODIFIED + m_statusImageList->Add( KiBitmap( BITMAPS::git_add ) ); // GIT_STATUS_ADDED + m_statusImageList->Add( KiBitmap( BITMAPS::git_delete ) ); // GIT_STATUS_DELETED + m_statusImageList->Add( KiBitmap( BITMAPS::git_out_of_date ) ); // GIT_STATUS_BEHIND + m_statusImageList->Add( KiBitmap( BITMAPS::git_changed_ahead ) );// GIT_STATUS_AHEAD + m_statusImageList->Add( KiBitmap( BITMAPS::git_conflict ) ); // GIT_STATUS_CONFLICTED + + SetStateImageList( m_statusImageList ); + } diff --git a/kicad/project_tree.h b/kicad/project_tree.h index 67864a407f..80c5a98303 100644 --- a/kicad/project_tree.h +++ b/kicad/project_tree.h @@ -25,12 +25,14 @@ #ifndef PROJECT_TREE_H #define PROJECT_TREE_H +#include #include #include "tree_file_type.h" class PROJECT_TREE_PANE; +struct git_repository; /** PROJECT_TREE * This is the class to show (as a tree) the files in the project directory @@ -42,6 +44,8 @@ class PROJECT_TREE : public wxTreeCtrl private: PROJECT_TREE_PANE* m_projectTreePane; wxImageList* m_imageList; + wxImageList* m_statusImageList; + KIGIT_COMMON* m_gitCommon; public: PROJECT_TREE_PANE* GetProjectTreePane() const { return m_projectTreePane; } @@ -51,6 +55,11 @@ public: void LoadIcons(); + void SetGitRepo( git_repository* aRepo ) { m_gitCommon->SetRepo( aRepo ); } + git_repository* GetGitRepo() const { return m_gitCommon->GetRepo(); } + + KIGIT_COMMON* GitCommon() const { return m_gitCommon; } + private: /* overridden sort function */ int OnCompareItems( const wxTreeItemId& item1, const wxTreeItemId& item2 ) override; diff --git a/kicad/project_tree_pane.cpp b/kicad/project_tree_pane.cpp index cbe648aad1..3cebfb60b1 100644 --- a/kicad/project_tree_pane.cpp +++ b/kicad/project_tree_pane.cpp @@ -24,6 +24,7 @@ */ #include +#include #include #include @@ -31,8 +32,12 @@ #include #include +#include #include #include +#include +#include +#include #include #include #include @@ -40,11 +45,29 @@ #include #include #include +#include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include + #include "project_tree_item.h" #include "project_tree.h" #include "pgm_kicad.h" @@ -126,6 +149,23 @@ BEGIN_EVENT_TABLE( PROJECT_TREE_PANE, wxSashLayoutWindow ) EVT_MENU( ID_PROJECT_OPEN_DIR, PROJECT_TREE_PANE::onOpenDirectory ) EVT_MENU( ID_PROJECT_DELETE, PROJECT_TREE_PANE::onDeleteFile ) EVT_MENU( ID_PROJECT_RENAME, PROJECT_TREE_PANE::onRenameFile ) + + EVT_MENU( ID_GIT_INITIALIZE_PROJECT, PROJECT_TREE_PANE::onGitInitializeProject ) + EVT_MENU( ID_GIT_COMMIT_PROJECT, PROJECT_TREE_PANE::onGitCommit ) + EVT_MENU( ID_GIT_COMMIT_FILE, PROJECT_TREE_PANE::onGitCommit ) + EVT_MENU( ID_GIT_SYNC_PROJECT, PROJECT_TREE_PANE::onGitSyncProject ) + EVT_MENU( ID_GIT_FETCH, PROJECT_TREE_PANE::onGitFetch ) + EVT_MENU( ID_GIT_PUSH, PROJECT_TREE_PANE::onGitPushProject ) + EVT_MENU( ID_GIT_PULL, PROJECT_TREE_PANE::onGitPullProject ) + EVT_MENU( ID_GIT_RESOLVE_CONFLICT, PROJECT_TREE_PANE::onGitResolveConflict ) + EVT_MENU( ID_GIT_REVERT_LOCAL, PROJECT_TREE_PANE::onGitRevertLocal ) + EVT_MENU( ID_GIT_SWITCH_BRANCH, PROJECT_TREE_PANE::onGitSwitchBranch ) + EVT_MENU( ID_GIT_COMPARE, PROJECT_TREE_PANE::onGitCompare ) + EVT_MENU( ID_GIT_REMOVE_VCS, PROJECT_TREE_PANE::onGitRemoveVCS ) + EVT_MENU( ID_GIT_ADD_TO_INDEX, PROJECT_TREE_PANE::onGitAddToIndex ) + EVT_MENU( ID_GIT_REMOVE_FROM_INDEX, PROJECT_TREE_PANE::onGitRemoveFromIndex ) + + EVT_IDLE( PROJECT_TREE_PANE::onIdle ) EVT_PAINT( PROJECT_TREE_PANE::onPaint ) END_EVENT_TABLE() @@ -140,6 +180,7 @@ PROJECT_TREE_PANE::PROJECT_TREE_PANE( KICAD_MANAGER_FRAME* parent ) : m_isRenaming = false; m_selectedItem = nullptr; m_watcherNeedReset = false; + m_lastGitStatusUpdate = wxDateTime::Now(); m_watcher = nullptr; Connect( wxEVT_FSWATCHER, @@ -158,6 +199,7 @@ PROJECT_TREE_PANE::PROJECT_TREE_PANE( KICAD_MANAGER_FRAME* parent ) : m_filters.emplace_back( wxT( "^no KiCad files found" ) ); ReCreateTreePrj(); + } @@ -304,6 +346,30 @@ std::vector getProjects( const wxDir& dir ) return projects; } +static git_repository* get_git_repository_for_file( const char* filename ) +{ + git_repository* repo = nullptr; + git_buf repo_path = GIT_BUF_INIT; + + // Find the repository path for the given file + if( git_repository_discover( &repo_path, filename, 0, NULL ) ) + { + printf( "Error %s\n", git_error_last()->message ); + return nullptr; + } + + if( git_repository_open( &repo, repo_path.ptr ) ) + { + git_buf_free( &repo_path ); + return nullptr; + } + + // Free the git_buf memory + git_buf_free( &repo_path ); + + return repo; +} + wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName, const wxTreeItemId& aParent, @@ -468,7 +534,9 @@ wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName, // Mark root files (files which have the same aName as the project) wxString fileName = currfile.GetName().Lower(); wxString projName = project.GetName().Lower(); - data->SetRootFile( fileName == projName || fileName.StartsWith( projName + "-" ) ); + + if( fileName == projName || fileName.StartsWith( projName + "-" ) ) + data->SetRootFile( true ); #ifndef __WINDOWS__ bool subdir_populated = false; @@ -527,6 +595,12 @@ void PROJECT_TREE_PANE::ReCreateTreePrj() if( !pro_dir ) // This is empty from PROJECT_TREE_PANE constructor return; + if( m_TreeProject->GetGitRepo() ) + { + git_repository_free( m_TreeProject->GetGitRepo() ); + m_TreeProject->SetGitRepo( nullptr ); + } + wxFileName fn = pro_dir; bool prjReset = false; @@ -541,6 +615,21 @@ void PROJECT_TREE_PANE::ReCreateTreePrj() bool prjOpened = fn.FileExists(); + // Bind the git repository to the project tree (if it exists) + m_TreeProject->SetGitRepo( get_git_repository_for_file( fn.GetPath().c_str() ) ); + m_TreeProject->GitCommon()->SetPassword( Prj().GetLocalSettings().m_GitRepoPassword ); + m_TreeProject->GitCommon()->SetUsername( Prj().GetLocalSettings().m_GitRepoUsername ); + m_TreeProject->GitCommon()->SetSSHKey( Prj().GetLocalSettings().m_GitSSHKey ); + + wxString conn_type = Prj().GetLocalSettings().m_GitRepoType; + + if( conn_type == "https" ) + m_TreeProject->GitCommon()->SetConnType( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ); + else if( conn_type == "ssh" ) + m_TreeProject->GitCommon()->SetConnType( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ); + else + m_TreeProject->GitCommon()->SetConnType( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ); + // We may have opened a legacy project, in which case GetProjectFileName will return the // name of the migrated (new format) file, which may not have been saved to disk yet. if( !prjOpened && !prjReset ) @@ -558,8 +647,10 @@ void PROJECT_TREE_PANE::ReCreateTreePrj() m_TreeProject->SetItemBold( m_root, true ); // The main project file is now a JSON file - m_TreeProject->SetItemData( m_root, new PROJECT_TREE_ITEM( TREE_FILE_TYPE::JSON_PROJECT, - fn.GetFullPath(), m_TreeProject ) ); + PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( TREE_FILE_TYPE::JSON_PROJECT, + fn.GetFullPath(), m_TreeProject ); + + m_TreeProject->SetItemData( m_root, data ); // Now adding all current files if available if( prjOpened ) @@ -596,6 +687,31 @@ void PROJECT_TREE_PANE::ReCreateTreePrj() // Sort filenames by alphabetic order m_TreeProject->SortChildren( m_root ); + updateGitStatusIcons(); +} + + +bool PROJECT_TREE_PANE::hasChangedFiles() +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return false; + + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX + | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY; + + git_status_list* status_list = nullptr; + int error = git_status_list_new( &status_list, repo, &opts ); + + if( error != GIT_OK ) + return false; + + bool has_changed_files = git_status_list_entrycount( status_list ) > 0; + git_status_list_free( status_list ); + return has_changed_files; } @@ -615,6 +731,22 @@ void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event ) bool can_rename = true; bool can_delete = true; + bool vcs_has_repo = m_TreeProject->GetGitRepo() != nullptr; + bool vcs_can_commit = hasChangedFiles(); + bool vcs_can_init = !vcs_has_repo; + bool vcs_can_remove = vcs_has_repo; + bool vcs_can_fetch = vcs_has_repo && m_TreeProject->GitCommon()->HasPushAndPullRemote(); + bool vcs_can_push = vcs_can_fetch && m_TreeProject->GitCommon()->HasLocalCommits(); + bool vcs_can_pull = vcs_can_fetch; + bool vcs_can_switch = vcs_has_repo; + bool vcs_menu = ADVANCED_CFG::GetCfg().m_EnableGit; + + // Check if the libgit2 library has been successfully initialized + int major, minor, rev; + bool libgit_init = ( git_libgit2_version( &major, &minor, &rev ) == GIT_OK ); + + vcs_menu &= libgit_init; + if( selection.size() == 0 ) return; @@ -642,8 +774,8 @@ void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event ) switch( item->GetType() ) { - case TREE_FILE_TYPE::LEGACY_PROJECT: case TREE_FILE_TYPE::JSON_PROJECT: + case TREE_FILE_TYPE::LEGACY_PROJECT: can_rename = false; if( item->GetId() == m_TreeProject->GetRootItem() ) @@ -662,6 +794,10 @@ void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event ) can_edit = false; break; + case TREE_FILE_TYPE::SEXPR_SCHEMATIC: + case TREE_FILE_TYPE::SEXPR_PCB: + KI_FALLTHROUGH; + default: can_switch_to_project = false; can_create_new_directory = false; @@ -769,6 +905,68 @@ void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event ) #endif } + if( vcs_menu ) + { + wxMenu* vcs_submenu = new wxMenu(); + wxMenu* branch_submenu = new wxMenu(); + wxMenuItem* vcs_menuitem = nullptr; + + vcs_menuitem = vcs_submenu->Append( ID_GIT_INITIALIZE_PROJECT, + _( "Add Project to Version Control..." ), + _( "Initialize a new repository" ) ); + vcs_menuitem->Enable( vcs_can_init ); + + + vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_PROJECT, _( "Commit Project..." ), + _( "Commit changes to the local repository" ) ); + vcs_menuitem->Enable( vcs_can_commit ); + + vcs_menuitem = vcs_submenu->Append( ID_GIT_PUSH, _( "Push" ), + _( "Push committed local changes to remote repository" ) ); + vcs_menuitem->Enable( vcs_can_push ); + + vcs_menuitem = vcs_submenu->Append( ID_GIT_PULL, _( "Pull" ), + _( "Pull changes from remote repository into local" ) ); + vcs_menuitem->Enable( vcs_can_pull ); + + vcs_submenu->AppendSeparator(); + + vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_FILE, _( "Commit File..." ), + _( "Commit changes to the local repository" ) ); + vcs_menuitem->Enable( vcs_can_commit ); + + vcs_submenu->AppendSeparator(); + + // vcs_menuitem = vcs_submenu->Append( ID_GIT_COMPARE, _( "Diff" ), + // _( "Show changes between the repository and working tree" ) ); + // vcs_menuitem->Enable( vcs_can_diff ); + + std::vector branchNames = m_TreeProject->GitCommon()->GetBranchNames(); + + // Skip the first one (that is the current branch) + for( size_t ii = 1; ii < branchNames.size() && ii < 6; ++ii ) + { + wxString msg = _( "Switch to branch " ) + branchNames[ii]; + vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH + ii, branchNames[ii], msg ); + vcs_menuitem->Enable( vcs_can_switch ); + } + + vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Other..." ), + _( "Switch to a different branch" ) ); + vcs_menuitem->Enable( vcs_can_switch ); + + vcs_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Switch to Branch" ), branch_submenu ); + + vcs_submenu->AppendSeparator(); + + vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Remove Version Control" ), + _( "Delete all version control files from the project directory." ) ); + vcs_menuitem->Enable( vcs_can_remove ); + + popup_menu.AppendSeparator(); + popup_menu.AppendSubMenu( vcs_submenu, _( "Version Control" ) ); + } + if( popup_menu.GetMenuItemCount() > 0 ) PopupMenu( &popup_menu ); } @@ -872,6 +1070,9 @@ void PROJECT_TREE_PANE::onIdle( wxIdleEvent& aEvent ) item->Activate( this ); } + + // Inside this routine, we rate limit to once per 2 seconds + updateGitStatusIcons(); } @@ -1042,9 +1243,12 @@ void PROJECT_TREE_PANE::onFileSystemEvent( wxFileSystemWatcherEvent& event ) case wxFSW_EVENT_DELETE: case wxFSW_EVENT_CREATE: case wxFSW_EVENT_RENAME: + CallAfter( &PROJECT_TREE_PANE::updateGitStatusIcons ); break; case wxFSW_EVENT_MODIFY: + CallAfter( &PROJECT_TREE_PANE::updateGitStatusIcons ); + KI_FALLTHROUGH; case wxFSW_EVENT_ACCESS: default: return; @@ -1288,3 +1492,769 @@ void KICAD_MANAGER_FRAME::OnChangeWatchedPaths( wxCommandEvent& aEvent ) { m_leftWin->FileWatcherReset(); } + + +void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent ) +{ + PROJECT_TREE_ITEM* tree_data = GetItemIdData( m_TreeProject->GetRootItem() ); + + wxString dir = tree_data->GetDir(); + + if( dir.empty() ) + { + wxLogError( "Failed to initialize git project: project directory is empty." ); + return; + } + + // Check if the directory is already a git repository + git_repository* repo = nullptr; + int error = git_repository_open(&repo, dir.mb_str()); + + if( error == 0 ) + { + // Directory is already a git repository + + DisplayInfoMessage( this, _( "The selected directory is already a git project." ) ); + git_repository_free( repo ); + return; + } + else + { + // Directory is not a git repository + error = git_repository_init( &repo, dir.mb_str(), 0 ); + + if( error != 0 ) + { + git_repository_free( repo ); + DisplayErrorMessage( this, _( "Failed to initialize git project." ), + git_error_last()->message ); + return; + } + else + { + m_TreeProject->SetGitRepo( repo ); + } + } + + DIALOG_GIT_REPOSITORY dlg( this, repo ); + + dlg.SetTitle( _( "Set default remote" ) ); + + if( dlg.ShowModal() != wxID_OK ) + return; + + //Set up the git remote + + m_TreeProject->GitCommon()->SetConnType( dlg.GetRepoType() ); + m_TreeProject->GitCommon()->SetPassword( dlg.GetPassword() ); + m_TreeProject->GitCommon()->SetUsername( dlg.GetUsername() ); + m_TreeProject->GitCommon()->SetSSHKey( dlg.GetRepoSSHPath() ); + + git_remote* remote = nullptr; + wxString fullURL; + + if( dlg.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) + { + fullURL = dlg.GetUsername() + "@" + dlg.GetRepoURL(); + } + else if( dlg.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) + { + fullURL = dlg.GetRepoURL().StartsWith( "https" ) ? "https://" : "http://"; + + if( !dlg.GetUsername().empty() ) + { + fullURL.append( dlg.GetUsername() ); + + if( !dlg.GetPassword().empty() ) + { + fullURL.append( wxS( ":" ) ); + fullURL.append( dlg.GetPassword() ); + } + + fullURL.append( wxS( "@" ) ); + } + + fullURL.append( dlg.GetBareRepoURL() ); + } + else + { + fullURL = dlg.GetRepoURL(); + } + + + error = git_remote_create_with_fetchspec( &remote, repo, "origin", + fullURL.ToStdString().c_str(), + "+refs/heads/*:refs/remotes/origin/*" ); + + if( error != GIT_OK ) + { + DisplayErrorMessage( this, _( "Failed to set default remote." ), + git_error_last()->message ); + return; + } + + GIT_PULL_HANDLER handler( repo ); + + handler.SetConnType( m_TreeProject->GitCommon()->GetConnType() ); + handler.SetUsername( m_TreeProject->GitCommon()->GetUsername() ); + handler.SetPassword( m_TreeProject->GitCommon()->GetPassword() ); + handler.SetSSHKey( m_TreeProject->GitCommon()->GetSSHKey() ); + + handler.SetProgressReporter( std::make_unique( this, _( "Fetching Remote" ), 1 ) ); + + handler.PerformFetch(); + + Prj().GetLocalSettings().m_GitRepoPassword = dlg.GetPassword(); + Prj().GetLocalSettings().m_GitRepoUsername = dlg.GetUsername(); + Prj().GetLocalSettings().m_GitSSHKey = dlg.GetRepoSSHPath(); + + if( dlg.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) + Prj().GetLocalSettings().m_GitRepoType = "ssh"; + else if( dlg.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) + Prj().GetLocalSettings().m_GitRepoType = "https"; + else + Prj().GetLocalSettings().m_GitRepoType = "local"; +} + + +void PROJECT_TREE_PANE::onGitCompare( wxCommandEvent& aEvent ) +{ + +} + + +void PROJECT_TREE_PANE::onGitPullProject( wxCommandEvent& aEvent ) +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return; + + GIT_PULL_HANDLER handler( repo ); + + handler.SetConnType( m_TreeProject->GitCommon()->GetConnType() ); + handler.SetUsername( m_TreeProject->GitCommon()->GetUsername() ); + handler.SetPassword( m_TreeProject->GitCommon()->GetPassword() ); + handler.SetSSHKey( m_TreeProject->GitCommon()->GetSSHKey() ); + + handler.SetProgressReporter( std::make_unique( this, _( "Fetching Remote" ), 1 ) ); + + handler.PerformPull(); +} + + +void PROJECT_TREE_PANE::onGitPushProject( wxCommandEvent& aEvent ) +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return; + + GIT_PUSH_HANDLER handler( repo ); + + handler.SetConnType( m_TreeProject->GitCommon()->GetConnType() ); + handler.SetUsername( m_TreeProject->GitCommon()->GetUsername() ); + handler.SetPassword( m_TreeProject->GitCommon()->GetPassword() ); + handler.SetSSHKey( m_TreeProject->GitCommon()->GetSSHKey() ); + + handler.SetProgressReporter( std::make_unique( this, _( "Fetching Remote" ), 1 ) ); + + if( handler.PerformPush() != PushResult::Success ) + { + wxString errorMessage = handler.GetErrorString(); + + DisplayErrorMessage( this, _( "Failed to push project" ), errorMessage ); + } +} + + +static int git_create_branch( git_repository* aRepo, wxString& aBranchName ) +{ + git_oid head_oid; + + if( int error = git_reference_name_to_id( &head_oid, aRepo, "HEAD" ) != 0 ) + { + wxLogError( "Failed to lookup HEAD reference" ); + return error; + } + + // Lookup the current commit object + git_commit* commit = nullptr; + if( int error = git_commit_lookup( &commit, aRepo, &head_oid ) != GIT_OK ) + { + wxLogError( "Failed to lookup commit" ); + return error; + } + + git_reference* branchRef = nullptr; + + if( git_branch_create( &branchRef, aRepo, aBranchName.mb_str(), commit, 0 ) != 0 ) + { + wxLogError( "Failed to create branch" ); + git_commit_free( commit ); + return -1; + } + + git_commit_free( commit ); + git_reference_free( branchRef ); + + return 0; +} + + +void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent ) +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return; + + DIALOG_GIT_SWITCH dlg( this, repo ); + + int retval = dlg.ShowModal(); + wxString branchName = dlg.GetBranchName(); + + if( retval == wxID_ADD ) + git_create_branch( repo, branchName); + else if( retval != wxID_OK ) + return; + + // Retrieve the reference to the existing branch using libgit2 + git_reference* branchRef = nullptr; + + if( git_reference_lookup( &branchRef, repo, branchName.mb_str() ) != GIT_OK && + git_reference_dwim( &branchRef, repo, branchName.mb_str() ) != GIT_OK ) + { + wxString errorMessage = wxString::Format( _( "Failed to lookup branch '%s': %s" ), branchName, giterr_last()->message ); + DisplayError( this, errorMessage ); + return; + } + + const char* branchRefName = git_reference_name( branchRef ); + + git_object* branchObj = nullptr; + + if( git_revparse_single( &branchObj, repo, branchName.mb_str() ) != 0 ) + { + wxString errorMessage = + wxString::Format( _( "Failed to find branch head for '%s'" ), branchName ); + DisplayError( this, errorMessage ); + git_reference_free( branchRef ); + return; + } + + + // Switch to the branch + if( git_checkout_tree( repo, branchObj, nullptr ) != 0 ) + { + wxString errorMessage = + wxString::Format( _( "Failed to switch to branch '%s'" ), branchName ); + DisplayError( this, errorMessage ); + git_reference_free( branchRef ); + git_object_free( branchObj ); + return; + } + + // Update the HEAD reference + if( git_repository_set_head( repo, branchRefName ) != 0 ) + { + wxString errorMessage = wxString::Format( + _( "Failed to update HEAD reference for branch '%s'" ), branchName ); + DisplayError( this, errorMessage ); + git_reference_free( branchRef ); + git_object_free( branchObj ); + return; + } + + // Free resources + git_reference_free( branchRef ); + git_object_free( branchObj ); +} + + +void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent ) +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo + || !IsOK( this, _( "Are you sure you want to remove git tracking from this project?" ) ) ) + { + return; + } + + // Remove the VCS (git) from the project directory + git_repository_free( repo ); + m_TreeProject->SetGitRepo( nullptr ); + + // Remove the .git directory + wxFileName fn( m_Parent->GetProjectFileName() ); + fn.AppendDir( ".git" ); + + wxString errors; + + if( !RmDirRecursive( fn.GetPath(), &errors ) ) + { + DisplayErrorMessage( this, _( "Failed to remove git directory" ), errors ); + } + + // Clear all item states + + for( wxTreeItemId cookie = m_TreeProject->GetRootItem(); + cookie != nullptr; + cookie = m_TreeProject->GetNext( cookie ) ) + { + m_TreeProject->SetItemState( cookie, wxTREE_ITEMSTATE_NONE ); + } +} + + +void PROJECT_TREE_PANE::updateGitStatusIcons() +{ + if( ADVANCED_CFG::GetCfg().m_EnableGit == false ) + return; + + wxTimeSpan timeSinceLastUpdate = wxDateTime::Now() - m_lastGitStatusUpdate; + + if( timeSinceLastUpdate.Abs() < wxTimeSpan::Seconds( 2 ) ) + return; + + m_lastGitStatusUpdate = wxDateTime::Now(); + + wxTreeItemId kid = m_TreeProject->GetRootItem(); + + if( !kid.IsOk() ) + return; + + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return; + + // Get Current Branch + + git_reference* currentBranchReference = nullptr; + git_repository_head( ¤tBranchReference, repo ); + + // Get the current branch name + if( currentBranchReference ) + { + PROJECT_TREE_ITEM* rootItem = GetItemIdData( kid ); + wxString filename = wxFileNameFromPath( rootItem->GetFileName() ); + wxString branchName = git_reference_shorthand( currentBranchReference ); + + m_TreeProject->SetItemText( kid, filename + " [" + branchName + "]" ); + git_reference_free( currentBranchReference ); + } + else + { + wxLogError( "Failed to lookup current branch: %s", giterr_last()->message ); + } + + // Collect a map to easily set the state of each item + std::map branchMap; + { + while( kid.IsOk() ) + { + PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid ); + + branchMap[nextItem->GetFileName()] = kid; + kid = m_TreeProject->GetNext( kid ); + } + } + + git_status_options status_options = GIT_STATUS_OPTIONS_INIT; + status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_INCLUDE_UNMODIFIED; + + git_index* index = nullptr; + + if( git_repository_index( &index, repo ) != GIT_OK ) + { + wxLogDebug( "Failed to get git index: %s", giterr_last()->message ); + return; + } + + git_status_list* status_list = nullptr; + + if( git_status_list_new( &status_list, repo, &status_options ) != GIT_OK ) + { + wxLogDebug( "Failed to get git status list: %s", giterr_last()->message ); + git_index_free( index ); + return; + } + + auto [ localChanges, remoteChanges ] = m_TreeProject->GitCommon()->GetDifferentFiles(); + + size_t count = git_status_list_entrycount( status_list ); + + for( size_t ii = 0; ii < count; ++ii ) + { + const git_status_entry* entry = git_status_byindex( status_list, ii ); + std::string path( entry->head_to_index? entry->head_to_index->old_file.path + : entry->index_to_workdir->old_file.path ); + wxFileName fn( path ); + fn.MakeAbsolute( git_repository_workdir( repo ) ); + + auto iter = branchMap.find( fn.GetFullPath() ); + + if( iter == branchMap.end() ) + continue; + + // If we are current, don't continue because we still need to check to see if the + // current commit is ahead/behind the remote. If the file is modified/added/deleted, + // that is the main status we want to show. + if( entry->status == GIT_STATUS_CURRENT ) + { + m_TreeProject->SetItemState( + iter->second, + static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT ) ); + } + else if( entry->status & ( GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_MODIFIED ) ) + { + m_TreeProject->SetItemState( + iter->second, + static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_MODIFIED ) ); + continue; + } + else if( entry->status & ( GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_NEW ) ) + { + m_TreeProject->SetItemState( + iter->second, + static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED ) ); + continue; + } + else if( entry->status & ( GIT_STATUS_INDEX_DELETED | GIT_STATUS_WT_DELETED ) ) + { + m_TreeProject->SetItemState( + iter->second, + static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_DELETED ) ); + continue; + } + + // Check if file is up to date with the remote + if( localChanges.count( path ) ) + { + m_TreeProject->SetItemState( + iter->second, + static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD ) ); + continue; + } + else if( remoteChanges.count( path ) ) + { + m_TreeProject->SetItemState( + iter->second, + static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND ) ); + continue; + } + else + { + m_TreeProject->SetItemState( + iter->second, + static_cast( KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT ) ); + continue; + } + } + + git_status_list_free( status_list ); + git_index_free( index ); +} + + +void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent ) +{ + std::vector tree_data = GetSelectedData(); + + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( repo == nullptr ) + { + wxMessageBox( "The selected directory is not a git project." ); + return; + } + + git_config* config = nullptr; + git_repository_config( &config, repo ); + + // Read relevant data from the git config + const char* authorName = nullptr; + const char* authorEmail = nullptr; + + // Read author name + git_config_entry* name_c = nullptr; + git_config_entry* email_c = nullptr; + int authorNameError = git_config_get_entry( &name_c, config, "user.name" ); + + if( authorNameError != 0 || name_c == nullptr ) + { + authorName = Pgm().GetCommonSettings()->m_Git.authorName; + } + else + { + authorName = name_c->value; + git_config_entry_free( name_c ); + } + + // Read author email + int authorEmailError = git_config_get_entry( &email_c, config, "user.email" ); + + if( authorEmailError != 0 || email_c == nullptr ) + { + authorEmail = Pgm().GetCommonSettings()->m_Git.authorEmail; + } + else + { + authorEmail = email_c->value; + git_config_entry_free( email_c ); + } + + // Free the config object + git_config_free( config ); + + // Collect modified files in the repository + git_status_options status_options = GIT_STATUS_OPTIONS_INIT; + status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED; + + git_status_list* status_list = nullptr; + git_status_list_new( &status_list, repo, &status_options ); + + std::map modifiedFiles; + + size_t count = git_status_list_entrycount( status_list ); + + std::set selected_files; + + for( PROJECT_TREE_ITEM* item : tree_data ) + { + if( item->GetType() != TREE_FILE_TYPE::DIRECTORY ) + selected_files.emplace( item->GetFileName() ); + } + + for( size_t i = 0; i < count; ++i ) + { + const git_status_entry* entry = git_status_byindex( status_list, i ); + + // Check if the file is modified (index or workdir changes) + if( entry->status == GIT_STATUS_CURRENT + || ( entry->status & ( GIT_STATUS_CONFLICTED | GIT_STATUS_IGNORED ) ) ) + { + continue; + } + + wxFileName fn( entry->index_to_workdir->old_file.path ); + fn.MakeAbsolute( git_repository_workdir( repo ) ); + + wxString filePath( entry->index_to_workdir->old_file.path, wxConvUTF8 ); + + if( aEvent.GetId() == ID_GIT_COMMIT_PROJECT ) + { + modifiedFiles.emplace( filePath, entry->status ); + } + else if( selected_files.count( fn.GetFullPath() ) ) + { + modifiedFiles.emplace( filePath, entry->status ); + } + } + + git_status_list_free( status_list ); + + // Create a commit dialog + DIALOG_GIT_COMMIT dlg( this, repo, authorName, authorEmail, modifiedFiles ); + auto ret = dlg.ShowModal(); + + if( ret == wxID_OK ) + { + // Commit the changes + git_oid tree_id; + git_tree* tree = nullptr; + git_commit* parent = nullptr; + git_index* index = nullptr; + + std::vector files = dlg.GetSelectedFiles(); + + if( dlg.GetCommitMessage().IsEmpty() ) + { + wxMessageBox( _( "Discarding commit due to empty commit message." ) ); + return; + } + + if( files.empty() ) + { + wxMessageBox( _( "Discarding commit due to empty file selection." ) ); + return; + } + + if( git_repository_index( &index, repo ) != 0 ) + { + wxMessageBox( _( "Failed to get repository index: %s" ), giterr_last()->message ); + return; + } + + for( wxString& file :files ) + { + if( git_index_add_bypath( index, file.mb_str() ) != 0 ) + { + wxMessageBox( _( "Failed to add file to index: %s" ), giterr_last()->message ); + git_index_free( index ); + return; + } + } + + if( git_index_write( index ) != 0 ) + { + wxMessageBox( _( "Failed to write index: %s" ), giterr_last()->message ); + git_index_free( index ); + return; + } + + if (git_index_write_tree( &tree_id, index ) != 0) + { + wxMessageBox( _( "Failed to write tree: %s" ), giterr_last()->message ); + git_index_free( index ); + return; + } + + git_index_free( index ); + + if( git_tree_lookup( &tree, repo, &tree_id ) != 0 ) + { + wxMessageBox( _( "Failed to lookup tree: %s" ), giterr_last()->message ); + return; + } + + git_reference* headRef = nullptr; + + if( git_repository_head( &headRef, repo ) != 0 ) + { + wxMessageBox( _( "Failed to get HEAD reference: %s" ), giterr_last()->message ); + git_index_free( index ); + return; + } + + if( git_reference_peel( (git_object**) &parent, headRef, GIT_OBJECT_COMMIT ) != 0 ) + { + wxMessageBox( _( "Failed to get commit: %s" ), giterr_last()->message ); + git_reference_free( headRef ); + git_index_free( index ); + return; + } + + git_reference_free( headRef ); + + const wxString& commit_msg = dlg.GetCommitMessage(); + const wxString& author_name = dlg.GetAuthorName(); + const wxString& author_email = dlg.GetAuthorEmail(); + + git_signature* author = nullptr; + + if( git_signature_now( &author, author_name.mb_str(), author_email.mb_str() ) != 0 ) + { + wxMessageBox( _( "Failed to create author signature: %s" ), giterr_last()->message ); + return; + } + + git_oid oid; + const git_commit* parents[1] = { parent }; + + if( git_commit_create( &oid, repo, "HEAD", author, author, nullptr, commit_msg.mb_str(), tree, + 1, parents ) != 0 ) + { + wxMessageBox( _( "Failed to create commit: %s" ), giterr_last()->message ); + return; + } + + git_signature_free( author ); + git_commit_free( parent ); + git_tree_free( tree ); + } +} + +void PROJECT_TREE_PANE::onGitAddToIndex( wxCommandEvent& aEvent ) +{ + +} + + +bool PROJECT_TREE_PANE::canFileBeAddedToVCS( const wxString& aFile ) +{ + git_index *index; + size_t entry_pos; + + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return false; + + if( git_repository_index( &index, repo ) != 0 ) + return false; + + // If we successfully find the file in the index, we may not add it to the VCS + if( git_index_find( &entry_pos, index, aFile.mb_str() ) == 0 ) + { + git_index_free( index ); + return false; + } + + git_index_free( index ); + return true; +} + + +void PROJECT_TREE_PANE::onGitSyncProject( wxCommandEvent& aEvent ) +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return; + + GIT_SYNC_HANDLER handler( repo ); + handler.PerformSync(); +} + + +void PROJECT_TREE_PANE::onGitFetch( wxCommandEvent& aEvent ) +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return; + + GIT_PULL_HANDLER handler( repo ); + handler.PerformFetch(); +} + +void PROJECT_TREE_PANE::onGitResolveConflict( wxCommandEvent& aEvent ) +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return; + + GIT_RESOLVE_CONFLICT_HANDLER handler( repo ); + handler.PerformResolveConflict(); +} + +void PROJECT_TREE_PANE::onGitRevertLocal( wxCommandEvent& aEvent ) +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return; + + GIT_REVERT_HANDLER handler( repo ); + handler.PerformRevert(); +} + +void PROJECT_TREE_PANE::onGitRemoveFromIndex( wxCommandEvent& aEvent ) +{ + git_repository* repo = m_TreeProject->GetGitRepo(); + + if( !repo ) + return; + + GIT_REMOVE_FROM_INDEX_HANDLER handler( repo ); + handler.PerformRemoveFromIndex(); +} + + + diff --git a/kicad/project_tree_pane.h b/kicad/project_tree_pane.h index 2ce7f2b7c7..9a845c630e 100644 --- a/kicad/project_tree_pane.h +++ b/kicad/project_tree_pane.h @@ -31,6 +31,7 @@ #define TREEPRJ_FRAME_H #include +#include #include #include #include @@ -154,6 +155,87 @@ private: */ void onPaint( wxPaintEvent& aEvent ); + /** + * Initialize a new git repository in the current project directory + */ + void onGitInitializeProject( wxCommandEvent& event ); + + /** + * Commit the current project saved changes to the git repository + */ + void onGitCommit( wxCommandEvent& event ); + + /** + * Pull the latest changes from the git repository + */ + void onGitPullProject( wxCommandEvent& event ); + + /** + * Push the current project changes to the git repository + */ + void onGitPushProject( wxCommandEvent& event ); + + /** + * Switch to a different branch in the git repository + */ + void onGitSwitchBranch( wxCommandEvent& event ); + + /** + * Compare the current project to a different branch in the git repository + */ + void onGitCompare( wxCommandEvent& event ); + + /** + * Remove the git repository from the current project directory + */ + void onGitRemoveVCS( wxCommandEvent& event ); + + /** + * Add a file to the git index + */ + void onGitAddToIndex( wxCommandEvent& event ); + + /** + * Remove a file from the git index + */ + void onGitRemoveFromIndex( wxCommandEvent& event ); + + /** + * Sync the current project with the git repository + */ + void onGitSyncProject( wxCommandEvent& event ); + + /** + * Fetch the latest changes from the git repository + */ + void onGitFetch( wxCommandEvent& event ); + + /** + * Resolve conflicts in the git repository + */ + void onGitResolveConflict( wxCommandEvent& event ); + + /** + * Revert the local repository to the last commit + */ + void onGitRevertLocal( wxCommandEvent& event ); + + /** + * Updates the icons shown in the tree project to reflect the current git status + */ + void updateGitStatusIcons(); + + /** + * Returns true if the current project has any uncommitted changes + */ + bool hasChangedFiles(); + + /** + * Returns true if the current project has local commits that have not been pushed to the + * remote repository + */ + bool hasLocalCommits(); + /** * Shutdown the file watcher. Used when closing to prevent post-free access into the project * tree. (Using the destructor doesn't work as wxWidgets defers destruction in some cases.) @@ -190,6 +272,12 @@ private: void onThemeChanged( wxSysColourChangedEvent &aEvent ); + /** + * Returns true if the file has already been added to the repository or + * false if it has not been added yet. + */ + bool canFileBeAddedToVCS( const wxString& aFilePath ); + public: KICAD_MANAGER_FRAME* m_Parent; PROJECT_TREE* m_TreeProject; @@ -203,6 +291,7 @@ private: bool m_watcherNeedReset; // true if FileWatcherReset() must be called // (during an idle time for instance) after // the main loop event handler is started + wxDateTime m_lastGitStatusUpdate; DECLARE_EVENT_TABLE() }; diff --git a/kicad/tools/kicad_manager_actions.cpp b/kicad/tools/kicad_manager_actions.cpp index 1a58e2d9c4..06083b1d27 100644 --- a/kicad/tools/kicad_manager_actions.cpp +++ b/kicad/tools/kicad_manager_actions.cpp @@ -53,6 +53,14 @@ TOOL_ACTION KICAD_MANAGER_ACTIONS::newFromTemplate( TOOL_ACTION_ARGS() .Tooltip( _( "Create new project from template" ) ) .Icon( BITMAPS::new_project_from_template ) ); +TOOL_ACTION KICAD_MANAGER_ACTIONS::newFromRepository( TOOL_ACTION_ARGS() + .Name( "kicad.Control.newFromRepository" ) + .Scope( AS_GLOBAL ) + .LegacyHotkeyName( "Clone Project From Repository" ) + .MenuText( _( "Clone Project from Repository..." ) ) + .Tooltip( _( "Clone a project from an existing repository" ) ) + .Icon( BITMAPS::new_project_from_template ) ); + TOOL_ACTION KICAD_MANAGER_ACTIONS::openDemoProject( TOOL_ACTION_ARGS() .Name( "kicad.Control.openDemoProject" ) .Scope( AS_GLOBAL ) diff --git a/kicad/tools/kicad_manager_actions.h b/kicad/tools/kicad_manager_actions.h index b72cbfc0e9..040013ec8b 100644 --- a/kicad/tools/kicad_manager_actions.h +++ b/kicad/tools/kicad_manager_actions.h @@ -33,6 +33,7 @@ class KICAD_MANAGER_ACTIONS : public ACTIONS public: static TOOL_ACTION newProject; static TOOL_ACTION newFromTemplate; + static TOOL_ACTION newFromRepository; static TOOL_ACTION openDemoProject; static TOOL_ACTION openProject; static TOOL_ACTION closeProject; diff --git a/kicad/tools/kicad_manager_control.cpp b/kicad/tools/kicad_manager_control.cpp index 84b115fec6..650dc521ae 100644 --- a/kicad/tools/kicad_manager_control.cpp +++ b/kicad/tools/kicad_manager_control.cpp @@ -36,6 +36,8 @@ #include #include #include +#include +#include #include #include #include @@ -62,10 +64,12 @@ void KICAD_MANAGER_CONTROL::Reset( RESET_REASON aReason ) } -int KICAD_MANAGER_CONTROL::NewProject( const TOOL_EVENT& aEvent ) +wxFileName KICAD_MANAGER_CONTROL::newProjectDirectory( wxString* aFileName ) { + wxString default_filename = aFileName ? *aFileName : wxString(); + wxString default_dir = m_frame->GetMruPath(); - wxFileDialog dlg( m_frame, _( "Create New Project" ), default_dir, wxEmptyString, + wxFileDialog dlg( m_frame, _( "Create New Project" ), default_dir, default_filename, ProjectFileWildcard(), wxFD_SAVE | wxFD_OVERWRITE_PROMPT ); // Add a "Create a new directory" checkbox @@ -73,7 +77,7 @@ int KICAD_MANAGER_CONTROL::NewProject( const TOOL_EVENT& aEvent ) dlg.SetCustomizeHook( newProjectHook ); if( dlg.ShowModal() == wxID_CANCEL ) - return -1; + return wxFileName(); wxFileName pro( dlg.GetPath() ); @@ -107,7 +111,7 @@ int KICAD_MANAGER_CONTROL::NewProject( const TOOL_EVENT& aEvent ) "Make sure you have write permissions and try again." ), pro.GetPath() ); DisplayErrorMessage( m_frame, msg ); - return -1; + return wxFileName(); } } else if( directory.HasFiles() ) @@ -117,9 +121,21 @@ int KICAD_MANAGER_CONTROL::NewProject( const TOOL_EVENT& aEvent ) "Do you want to continue?" ); if( !IsOK( m_frame, msg ) ) - return -1; + return wxFileName(); } + return pro; +} + + +int KICAD_MANAGER_CONTROL::NewProject( const TOOL_EVENT& aEvent ) +{ + + wxFileName pro = newProjectDirectory(); + + if( !pro.IsOk() ) + return -1; + m_frame->CreateNewProject( pro ); m_frame->LoadProject( pro ); @@ -127,6 +143,68 @@ int KICAD_MANAGER_CONTROL::NewProject( const TOOL_EVENT& aEvent ) } +int KICAD_MANAGER_CONTROL::NewFromRepository( const TOOL_EVENT& aEvent ) +{ + DIALOG_GIT_REPOSITORY dlg( m_frame, nullptr ); + + dlg.SetTitle( _( "Clone Project from Git Repository" ) ); + + int ret = dlg.ShowModal(); + + if( ret != wxID_OK ) + return -1; + + wxString repodest = dlg.GetRepoName(); + wxFileName pro = newProjectDirectory( &repodest ); + + if( !pro.IsOk() ) + return -1; + + GIT_CLONE_HANDLER cloneHandler; + + cloneHandler.SetURL( dlg.GetRepoURL() ); + cloneHandler.SetClonePath( pro.GetPath() ); + cloneHandler.SetConnType( dlg.GetRepoType() ); + cloneHandler.SetUsername( dlg.GetUsername() ); + cloneHandler.SetPassword( dlg.GetPassword() ); + cloneHandler.SetSSHKey( dlg.GetRepoSSHPath() ); + + cloneHandler.SetProgressReporter( std::make_unique( m_frame, _( "Cloning Repository" ), 1 ) ); + + if( !cloneHandler.PerformClone() ) + { + DisplayErrorMessage( m_frame, cloneHandler.GetErrorString() ); + return -1; + } + + std::vector projects = cloneHandler.GetProjectDirs(); + + if( projects.empty() ) + { + DisplayErrorMessage( m_frame, _( "No project files were found in the repository." ) ); + return -1; + } + + // Currently, we pick the first project file we find in the repository. + // TODO: Look into spare checkout to allow the user to pick a partial repository + wxString dest = pro.GetPath() + wxFileName::GetPathSeparator() + projects.front(); + m_frame->LoadProject( dest ); + + Prj().GetLocalSettings().m_GitRepoPassword = dlg.GetPassword(); + Prj().GetLocalSettings().m_GitRepoUsername = dlg.GetUsername(); + Prj().GetLocalSettings().m_GitSSHKey = dlg.GetRepoSSHPath(); + + if( dlg.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) + Prj().GetLocalSettings().m_GitRepoType = "ssh"; + else if( dlg.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) + Prj().GetLocalSettings().m_GitRepoType = "https"; + else + Prj().GetLocalSettings().m_GitRepoType = "local"; + + return 0; +} + + int KICAD_MANAGER_CONTROL::NewFromTemplate( const TOOL_EVENT& aEvent ) { DIALOG_TEMPLATE_SELECTOR* ps = new DIALOG_TEMPLATE_SELECTOR( m_frame ); @@ -874,6 +952,7 @@ void KICAD_MANAGER_CONTROL::setTransitions() { Go( &KICAD_MANAGER_CONTROL::NewProject, KICAD_MANAGER_ACTIONS::newProject.MakeEvent() ); Go( &KICAD_MANAGER_CONTROL::NewFromTemplate, KICAD_MANAGER_ACTIONS::newFromTemplate.MakeEvent() ); + Go( &KICAD_MANAGER_CONTROL::NewFromRepository, KICAD_MANAGER_ACTIONS::newFromRepository.MakeEvent() ); Go( &KICAD_MANAGER_CONTROL::OpenDemoProject, KICAD_MANAGER_ACTIONS::openDemoProject.MakeEvent() ); Go( &KICAD_MANAGER_CONTROL::OpenProject, KICAD_MANAGER_ACTIONS::openProject.MakeEvent() ); Go( &KICAD_MANAGER_CONTROL::CloseProject, KICAD_MANAGER_ACTIONS::closeProject.MakeEvent() ); diff --git a/kicad/tools/kicad_manager_control.h b/kicad/tools/kicad_manager_control.h index badbfd4434..1ab7964bb5 100644 --- a/kicad/tools/kicad_manager_control.h +++ b/kicad/tools/kicad_manager_control.h @@ -46,6 +46,7 @@ public: int NewProject( const TOOL_EVENT& aEvent ); int NewFromTemplate( const TOOL_EVENT& aEvent ); + int NewFromRepository( const TOOL_EVENT& aEvent ); int OpenProject( const TOOL_EVENT& aEvent ); int OpenDemoProject( const TOOL_EVENT& aEvent ); int CloseProject( const TOOL_EVENT& aEvent ); @@ -77,6 +78,9 @@ private: std::mutex m_loading; int openProject( const wxString& aDefaultDir ); + + wxFileName newProjectDirectory( wxString* aFileName = nullptr ); + }; #endif diff --git a/libs/core/include/core/kicad_algo.h b/libs/core/include/core/kicad_algo.h index 428d94e81e..fc8f08a556 100644 --- a/libs/core/include/core/kicad_algo.h +++ b/libs/core/include/core/kicad_algo.h @@ -206,6 +206,37 @@ T clamp( T min, T value, T max ) return std::max( min, std::min( value, max ) ); } +/** + * @brief Returns the length of the longest common subset of values between two containers. +*/ +template +size_t longest_common_subset( const _Container& __c1, const _Container& __c2 ) +{ + size_t __c1_size = __c1.size(); + size_t __c2_size = __c2.size(); + + if( __c1_size == 0 || __c2_size == 0 ) + return 0; + + // Create a 2D table to store the lengths of common subsets + std::vector> table( __c1_size + 1, std::vector( __c2_size + 1, 0 ) ); + + size_t longest = 0; + + for( size_t i = 1; i <= __c1_size; ++i ) + { + for( size_t j = 1; j <= __c2_size; ++j ) + { + if( __c1[i - 1] == __c2[j - 1] ) + { + table[i][j] = table[i - 1][j - 1] + 1; + longest = std::max( longest, static_cast( table[i][j] ) ); + } + } + } + + return longest; +} } // namespace alg diff --git a/libs/kiplatform/CMakeLists.txt b/libs/kiplatform/CMakeLists.txt index 60219bfecf..eda4aabdb9 100644 --- a/libs/kiplatform/CMakeLists.txt +++ b/libs/kiplatform/CMakeLists.txt @@ -14,6 +14,7 @@ if( APPLE ) osx/environment.mm osx/io.mm osx/policy.mm + osx/secrets.mm osx/ui.mm ) @@ -30,6 +31,7 @@ elseif( WIN32 ) msw/environment.cpp msw/io.cpp msw/policy.cpp + msw/secrets.cpp msw/ui.cpp ) @@ -46,6 +48,7 @@ elseif( UNIX ) gtk/environment.cpp gtk/io.cpp gtk/policy.cpp + gtk/secrets.cpp gtk/ui.cpp ) @@ -61,6 +64,11 @@ elseif( UNIX ) link_directories( ${GTK3_LIBRARY_DIRS} ) add_definitions( ${GTK3_CFLAGS_OTHER} ) + # Detect the secret library and configure it + pkg_check_modules(secret REQUIRED libsecret-1) + include_directories( SYSTEM ${secret_INCLUDE_DIRS} ) + list( APPEND PLATFORM_LIBS ${secret_LIBRARIES} ) + if( KICAD_WAYLAND ) find_package(Wayland COMPONENTS Client REQUIRED) diff --git a/libs/kiplatform/gtk/secrets.cpp b/libs/kiplatform/gtk/secrets.cpp new file mode 100644 index 0000000000..eab664d2bb --- /dev/null +++ b/libs/kiplatform/gtk/secrets.cpp @@ -0,0 +1,84 @@ +/* +* This program source code file is part of KiCad, a free EDA CAD application. +* +* Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors. +* +* This program is free software: you can redistribute it and/or modify it +* under the terms of the GNU General Public License as published by the +* Free Software Foundation, either version 3 of the License, or (at your +* option) any later version. +* +* This program is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License along +* with this program. If not, see . +*/ + +#include + +#include + +namespace KIPLATFORM +{ + namespace SECRETS + { + static const SecretSchema schema = + { + "org.kicad.kicad", SECRET_SCHEMA_NONE, + { + { "service", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { "key", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { nullptr, SECRET_SCHEMA_ATTRIBUTE_STRING } + } + }; + + bool StoreSecret( const wxString& aService, const wxString& aKey, const wxString& aSecret ) + { + GError* error = nullptr; + wxString display = aService + ":" + aKey; + + secret_password_store_sync( &schema, + SECRET_COLLECTION_DEFAULT, + display.mb_str(), // Display name + aSecret.mb_str(), // Secret value + nullptr, + &error, + "service", aService.ToStdString().c_str(), + "key", aKey.ToStdString().c_str(), + nullptr ); + + if( error ) + { + g_error_free( error ); + return false; + } + + return true; + } + + bool GetSecret( const wxString& aService, const wxString& aKey, wxString& aSecret ) + { + GError* error = nullptr; + gchar* secret = secret_password_lookup_sync( &schema, + nullptr, + &error, + "service", aService.ToStdString().c_str(), + "key", aKey.ToStdString().c_str(), + nullptr ); + + if( error ) + { + g_error_free( error ); + return false; + } + + aSecret = secret; + g_free( secret ); + + return true; + } + } +} \ No newline at end of file diff --git a/libs/kiplatform/include/kiplatform/secrets.h b/libs/kiplatform/include/kiplatform/secrets.h new file mode 100644 index 0000000000..cabdf31496 --- /dev/null +++ b/libs/kiplatform/include/kiplatform/secrets.h @@ -0,0 +1,37 @@ +/* +* This program source code file is part of KiCad, a free EDA CAD application. +* +* Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors. +* +* This program is free software: you can redistribute it and/or modify it +* under the terms of the GNU General Public License as published by the +* Free Software Foundation, either version 3 of the License, or (at your +* option) any later version. +* +* This program is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License along +* with this program. If not, see . +*/ + +#ifndef KIPLATFORM_SECRETS_H_ +#define KIPLATFORM_SECRETS_H_ + +#include + +namespace KIPLATFORM +{ + namespace SECRETS + { + + bool StoreSecret( const wxString& aService, const wxString& aKey, const wxString& aSecret ); + + bool GetSecret( const wxString& aService, const wxString& aKey, wxString& aSecret ); + + } +} + +#endif // KIPLATFORM_SECRETS_H_ \ No newline at end of file diff --git a/libs/kiplatform/msw/secrets.cpp b/libs/kiplatform/msw/secrets.cpp new file mode 100644 index 0000000000..0e87d4f0fa --- /dev/null +++ b/libs/kiplatform/msw/secrets.cpp @@ -0,0 +1,60 @@ +/* +* This program source code file is part of KiCad, a free EDA CAD application. +* +* Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors. +* +* This program is free software: you can redistribute it and/or modify it +* under the terms of the GNU General Public License as published by the +* Free Software Foundation, either version 3 of the License, or (at your +* option) any later version. +* +* This program is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License along +* with this program. If not, see . +*/ + +#include + +#include +#include +#include + +namespace KIPLATFORM +{ + namespace SECRETS + { + bool StoreSecret( const wxString& aService, const wxString& aKey, const wxString& aSecret ) + { + wxString display = aService + L":" + aKey; + + CREDENTIALW cred = { 0 }; + cred.Type = CRED_TYPE_GENERIC; + cred.TargetName = (LPWSTR)display.wc_str(); + cred.CredentialBlobSize = (DWORD)aSecret.size(); + cred.CredentialBlob = (LPBYTE)aSecret.c_str(); + cred.Persist = CRED_PERSIST_USER; + + return CredWriteW( &cred, 0 ); + } + + bool GetSecret( const wxString& aService, const wxString& aKey, wxString& aSecret ) + { + wxString display = aService + ":" + aKey; + + CREDENTIALW* cred = nullptr; + bool result = CredReadW( display.wc_str(), CRED_TYPE_GENERIC, 0, &cred ); + + if( result ) + { + aSecret = wxString( (char*)cred->CredentialBlob, cred->CredentialBlobSize ); + CredFree( cred ); + } + + return result; + } + } +} \ No newline at end of file diff --git a/libs/kiplatform/osx/secrets.mm b/libs/kiplatform/osx/secrets.mm new file mode 100644 index 0000000000..111652af4d --- /dev/null +++ b/libs/kiplatform/osx/secrets.mm @@ -0,0 +1,75 @@ +/* +* This program source code file is part of KiCad, a free EDA CAD application. +* +* Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors. +* +* This program is free software: you can redistribute it and/or modify it +* under the terms of the GNU General Public License as published by the +* Free Software Foundation, either version 3 of the License, or (at your +* option) any later version. +* +* This program is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License along +* with this program. If not, see . +*/ + +#include + +#import + +bool KIPLATFORM::SECRETS::StoreSecret( const wxString& aService, const wxString& aKey, const wxString& aValue ) +{ + SecKeychainItemRef itemRef = NULL; + + OSStatus status = SecKeychainFindGenericPassword( NULL, aService.length(), aService.utf8_str(), + aKey.length(), aKey.utf8_str(), + NULL, NULL, &itemRef ); + + if( status == errSecItemNotFound ) + { + status = SecKeychainAddGenericPassword( NULL, aService.length(), aService.utf8_str(), + aKey.length(), aKey.utf8_str(), + aValue.length(), aValue.utf8_str(), + NULL ); + + CFRelease( itemRef ); + } + else if( status == errSecSuccess ) + { + status = SecKeychainItemModifyAttributesAndData( itemRef, NULL, aValue.length(), aValue.utf8_str() ); + } + + + return status == errSecSuccess; +} + +bool KIPLATFORM::SECRETS::GetSecret( const wxString& aService, const wxString& aKey, wxString& aValue ) +{ + SecKeychainItemRef itemRef = NULL; + + OSStatus status = SecKeychainFindGenericPassword( NULL, aService.length(), aService.utf8_str(), + aKey.length(), aKey.utf8_str(), + NULL, NULL, &itemRef ); + + if( status == errSecSuccess ) + { + UInt32 length; + char* data; + + status = SecKeychainItemCopyAttributesAndData( itemRef, NULL, NULL, NULL, &length, (void**)&data ); + + if( status == errSecSuccess ) + { + aValue = wxString::FromUTF8( data, length ); + SecKeychainItemFreeAttributesAndData( NULL, data ); + } + + CFRelease( itemRef ); + } + + return status == errSecSuccess; +} \ No newline at end of file diff --git a/pcbnew/CMakeLists.txt b/pcbnew/CMakeLists.txt index 45b92c97e7..4256e4eb28 100644 --- a/pcbnew/CMakeLists.txt +++ b/pcbnew/CMakeLists.txt @@ -378,10 +378,15 @@ set( PCBNEW_CLASS_SRCS ) +set( PCBNEW_GIT_SRCS + git/kigit_pcb_merge.cpp + ) + set( PCBNEW_SRCS ${PCBNEW_MICROWAVE_SRCS} ${PCBNEW_CLASS_SRCS} ${PCBNEW_DIALOGS} + ${PCBNEW_GIT_SRCS} ) # extra sources from common @@ -644,6 +649,7 @@ target_link_libraries( pcbnew_kiface_objects rectpack2d gzip-hpp Boost::boost + git2 ZLIB::ZLIB ${OCC_LIBRARIES} ) diff --git a/pcbnew/board.cpp b/pcbnew/board.cpp index 07930e75d2..c8374a0a1d 100644 --- a/pcbnew/board.cpp +++ b/pcbnew/board.cpp @@ -2584,21 +2584,85 @@ void BOARD::ConvertBrdLayerToPolygonalContours( PCB_LAYER_ID aLayer, } -const std::set BOARD::GetItemSet() +const BOARD_ITEM_SET BOARD::GetItemSet() { - std::set items; + BOARD_ITEM_SET items; -#define INSERT_ITEMS( collection ) \ - for( BOARD_ITEM * item : collection ) \ - items.insert( item ); - - INSERT_ITEMS( m_tracks ) - INSERT_ITEMS( m_footprints ) - INSERT_ITEMS( m_drawings ) - INSERT_ITEMS( m_zones ) - INSERT_ITEMS( m_markers ) - -#undef INSERT_ITEMS + std::copy( m_tracks.begin(), m_tracks.end(), std::inserter( items, items.end() ) ); + std::copy( m_zones.begin(), m_zones.end(), std::inserter( items, items.end() ) ); + std::copy( m_footprints.begin(), m_footprints.end(), std::inserter( items, items.end() ) ); + std::copy( m_drawings.begin(), m_drawings.end(), std::inserter( items, items.end() ) ); + std::copy( m_markers.begin(), m_markers.end(), std::inserter( items, items.end() ) ); + std::copy( m_groups.begin(), m_groups.end(), std::inserter( items, items.end() ) ); return items; } + + +bool BOARD::operator==( const BOARD_ITEM& aItem ) const +{ + if( aItem.Type() != Type() ) + return false; + + const BOARD& other = static_cast( aItem ); + + if( m_designSettings != other.m_designSettings ) + return false; + + if( m_NetInfo.GetNetCount() != other.m_NetInfo.GetNetCount() ) + return false; + + const NETNAMES_MAP& thisNetNames = m_NetInfo.NetsByName(); + const NETNAMES_MAP& otherNetNames = other.m_NetInfo.NetsByName(); + + for( auto it1 = thisNetNames.begin(), it2 = otherNetNames.begin(); + it1 != thisNetNames.end() && it2 != otherNetNames.end(); ++it1, ++it2 ) + { + // We only compare the names in order here, not the index values + // as the index values are auto-generated and the names are not. + if( it1->first != it2->first ) + return false; + } + + if( m_properties.size() != other.m_properties.size() ) + return false; + + for( auto it1 = m_properties.begin(), it2 = other.m_properties.begin(); + it1 != m_properties.end() && it2 != other.m_properties.end(); ++it1, ++it2 ) + { + if( *it1 != *it2 ) + return false; + } + + if( m_paper.GetCustomHeightMils() != other.m_paper.GetCustomHeightMils() ) + return false; + + if( m_paper.GetCustomWidthMils() != other.m_paper.GetCustomWidthMils() ) + return false; + + if( m_paper.GetSizeMils() != other.m_paper.GetSizeMils() ) + return false; + + if( m_paper.GetPaperId() != other.m_paper.GetPaperId() ) + return false; + + if( m_paper.GetWxOrientation() != other.m_paper.GetWxOrientation() ) + return false; + + for( int ii = 0; !m_titles.GetComment( ii ).empty(); ++ii ) + { + if( m_titles.GetComment( ii ) != other.m_titles.GetComment( ii ) ) + return false; + } + + wxArrayString ourVars; + m_titles.GetContextualTextVars( &ourVars ); + + wxArrayString otherVars; + other.m_titles.GetContextualTextVars( &otherVars ); + + if( ourVars != otherVars ) + return false; + + return true; +} \ No newline at end of file diff --git a/pcbnew/board.h b/pcbnew/board.h index 8ba1a2b50b..1f4b3104b9 100644 --- a/pcbnew/board.h +++ b/pcbnew/board.h @@ -254,6 +254,10 @@ public: virtual void OnBoardRatsnestChanged( BOARD& aBoard ) { } }; +/** + * Set of BOARD_ITEMs ordered by UUID. + */ +typedef std::set BOARD_ITEM_SET; /** * Flags to specify how the board is being used. @@ -326,7 +330,7 @@ public: MARKERS& Markers() { return m_markers; } const MARKERS& Markers() const { return m_markers; } - const std::set GetItemSet(); + const BOARD_ITEM_SET GetItemSet(); /** * The groups must maintain the following invariants. These are checked by @@ -936,6 +940,16 @@ public: */ void SynchronizeProperties(); + /** + * Return the Similarity. Because we compare board to board, we just return 1.0 here + */ + double Similarity( const BOARD_ITEM& aOther ) const override + { + return 1.0; + } + + bool operator==( const BOARD_ITEM& aOther ) const override; + wxString GetClass() const override { return wxT( "BOARD" ); diff --git a/pcbnew/footprint.cpp b/pcbnew/footprint.cpp index b7790b75d1..621ad05de5 100644 --- a/pcbnew/footprint.cpp +++ b/pcbnew/footprint.cpp @@ -2899,6 +2899,77 @@ bool FOOTPRINT::HasThroughHolePads() const } +bool FOOTPRINT::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != PCB_FOOTPRINT_T ) + return false; + + const FOOTPRINT& other = static_cast( aOther ); + + if( m_pads.size() != other.m_pads.size() ) + return false; + + for( size_t ii = 0; ii < m_pads.size(); ++ii ) + { + if( !( *m_pads[ii] == *other.m_pads[ii] ) ) + return false; + } + + if( m_drawings.size() != other.m_drawings.size() ) + return false; + + for( size_t ii = 0; ii < m_drawings.size(); ++ii ) + { + if( !( *m_drawings[ii] == *other.m_drawings[ii] ) ) + return false; + } + + if( m_zones.size() != other.m_zones.size() ) + return false; + + for( size_t ii = 0; ii < m_zones.size(); ++ii ) + { + if( !( *m_zones[ii] == *other.m_zones[ii] ) ) + return false; + } + + if( m_fields.size() != other.m_fields.size() ) + return false; + + for( size_t ii = 0; ii < m_fields.size(); ++ii ) + { + if( !( *m_fields[ii] == *other.m_fields[ii] ) ) + return false; + } + + return true; +} + + +double FOOTPRINT::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != PCB_FOOTPRINT_T ) + return 0.0; + + const FOOTPRINT& other = static_cast( aOther ); + + double similarity = 1.0; + + for( size_t ii = 0; ii < m_pads.size(); ++ii ) + { + const PAD* pad = m_pads[ii]; + const PAD* otherPad = other.FindPadByNumber( pad->GetNumber() ); + + if( !otherPad ) + continue; + + similarity *= pad->Similarity( *otherPad ); + } + + return similarity; +} + + #define TEST( a, b ) { if( a != b ) return a < b; } #define TEST_PT( a, b ) { if( a.x != b.x ) return a.x < b.x; if( a.y != b.y ) return a.y < b.y; } diff --git a/pcbnew/footprint.h b/pcbnew/footprint.h index 2c74b4ba20..d33bd44c5e 100644 --- a/pcbnew/footprint.h +++ b/pcbnew/footprint.h @@ -919,6 +919,10 @@ public: std::shared_ptr GetEffectiveShape( PCB_LAYER_ID aLayer = UNDEFINED_LAYER, FLASHING aFlash = FLASHING::DEFAULT ) const override; + double Similarity( const BOARD_ITEM& aOther ) const override; + + bool operator==( const BOARD_ITEM& aOther ) const override; + #if defined(DEBUG) virtual void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/pcbnew/git/kigit_pcb_merge.cpp b/pcbnew/git/kigit_pcb_merge.cpp new file mode 100644 index 0000000000..94506f3534 --- /dev/null +++ b/pcbnew/git/kigit_pcb_merge.cpp @@ -0,0 +1,180 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "kigit_pcb_merge.h" + +#include +#include +#include + +#include + +#include + +void KIGIT_PCB_MERGE::findSetDifferences( const BOARD_ITEM_SET& aAncestorSet, const BOARD_ITEM_SET& aOtherSet, + std::vector& aAdded, std::vector& aRemoved, + std::vector& aChanged ) +{ + auto it1 = aAncestorSet.begin(); + auto it2 = aOtherSet.begin(); + + while( it1 != aAncestorSet.end() && it2 != aOtherSet.end() ) + { + BOARD_ITEM* item1 = *it1; + BOARD_ITEM* item2 = *it2; + + if( item1->m_Uuid < item2->m_Uuid ) + { + aRemoved.push_back( item1 ); + ++it1; + } + else if( item1->m_Uuid > item2->m_Uuid ) + { + aAdded.push_back( item2 ); + ++it2; + } + else + { + if( !( *item1 == *item2 ) ) + aChanged.push_back( item1 ); + + ++it1; + ++it2; + } + } +} +KIGIT_PCB_MERGE_DIFFERENCES KIGIT_PCB_MERGE::compareBoards( BOARD* aAncestor, BOARD* aOther ) +{ + KIGIT_PCB_MERGE_DIFFERENCES differences; + + const auto ancestor_set = aAncestor->GetItemSet(); + const auto other_set = aOther->GetItemSet(); + + findSetDifferences( ancestor_set, other_set, differences.m_added, differences.m_removed, differences.m_changed ); + + return differences; +} + +int KIGIT_PCB_MERGE::Merge() +{ + git_repository* repo = git_merge_driver_source_repo( m_mergeDriver ); + const git_index_entry* ancestor = git_merge_driver_source_ancestor( m_mergeDriver ); + const git_index_entry* ours = git_merge_driver_source_ours( m_mergeDriver ); + const git_index_entry* theirs = git_merge_driver_source_theirs( m_mergeDriver ); + + // Read ancestor index entry into a buffer + + git_blob* ancestor_blob; + git_blob* ours_blob; + git_blob* theirs_blob; + + if( git_blob_lookup( &ancestor_blob, repo, &ancestor->id ) != 0 ) + { + return GIT_ENOTFOUND; + } + + if( git_blob_lookup( &ours_blob, repo, &ours->id ) != 0 ) + { + git_blob_free( ancestor_blob ); + return GIT_ENOTFOUND; + } + + if( git_blob_lookup( &theirs_blob, repo, &theirs->id ) != 0 ) + { + git_blob_free( ancestor_blob ); + git_blob_free( ours_blob ); + return GIT_ENOTFOUND; + } + + // Get the raw data from the blobs + BLOB_READER ancestor_reader( ancestor_blob ); + PCB_PARSER ancestor_parser( &ancestor_reader, nullptr, nullptr ); + BLOB_READER ours_reader( ours_blob ); + PCB_PARSER ours_parser( &ours_reader, nullptr, nullptr ); + BLOB_READER theirs_reader( theirs_blob ); + PCB_PARSER theirs_parser( &theirs_reader, nullptr, nullptr ); + + std::unique_ptr ancestor_board; + std::unique_ptr ours_board; + std::unique_ptr theirs_board; + + try + { + ancestor_board.reset( static_cast( ancestor_parser.Parse() ) ); + ours_board.reset( static_cast( ours_parser.Parse() ) ); + theirs_board.reset( static_cast( theirs_parser.Parse() ) ); + } + catch(const IO_ERROR&) + { + git_blob_free( ancestor_blob ); + git_blob_free( ours_blob ); + git_blob_free( theirs_blob ); + return GIT_EUSER; + } + + git_blob_free( ancestor_blob ); + git_blob_free( ours_blob ); + git_blob_free( theirs_blob ); + + // Parse the differences between the ancestor and ours + KIGIT_PCB_MERGE_DIFFERENCES ancestor_ours_differences = compareBoards( ancestor_board.get(), ours_board.get() ); + KIGIT_PCB_MERGE_DIFFERENCES ancestor_theirs_differences = compareBoards( ancestor_board.get(), theirs_board.get() ); + + // Find the items that we modified and they deleted + std::set_intersection( ancestor_ours_differences.m_changed.begin(), ancestor_ours_differences.m_changed.end(), + ancestor_theirs_differences.m_removed.begin(), ancestor_theirs_differences.m_removed.end(), + std::inserter( we_modified_they_deleted, we_modified_they_deleted.begin() ) ); + + // Find the items that they modified and we deleted + std::set_intersection( ancestor_theirs_differences.m_changed.begin(), ancestor_theirs_differences.m_changed.end(), + ancestor_ours_differences.m_removed.begin(), ancestor_ours_differences.m_removed.end(), + std::inserter( they_modified_we_deleted, they_modified_we_deleted.begin() ) ); + + // Find the items that both we and they modified + std::set_intersection( ancestor_ours_differences.m_changed.begin(), ancestor_ours_differences.m_changed.end(), + ancestor_theirs_differences.m_changed.begin(), ancestor_theirs_differences.m_changed.end(), + std::inserter( both_modified, both_modified.begin() ) ); + + return 0; +} + + +std::unique_ptr readBoard( wxString& aFilename ) +{ + // Take input from stdin + FILE_LINE_READER reader( aFilename ); + + PCB_PARSER parser( &reader, nullptr, nullptr ); + std::unique_ptr board; + + try + { + board.reset( static_cast( parser.Parse() ) ); + } + catch( const IO_ERROR& ) + { + } + + return board; +} + diff --git a/pcbnew/git/kigit_pcb_merge.h b/pcbnew/git/kigit_pcb_merge.h new file mode 100644 index 0000000000..46a07c02f9 --- /dev/null +++ b/pcbnew/git/kigit_pcb_merge.h @@ -0,0 +1,85 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef KIGIT_PCB_MERGE_H +#define KIGIT_PCB_MERGE_H +#include +#include + +#include +#include + +#include + +typedef struct KIGIT_PCB_MERGE_DIFFERENCES +{ + std::vector m_added; + std::vector m_removed; + std::vector m_changed; +} KIGIT_PCB_MERGE_DIFFERENCES; + +class KIGIT_PCB_MERGE +{ + public: + KIGIT_PCB_MERGE( git_merge_driver_source* aSource, git_buf* aBuf ) : m_mergeDriver( aSource ), m_result( aBuf ) + {} + + virtual ~KIGIT_PCB_MERGE(); + + int Merge(); + + std::set& GetWeModifiedTheyDeleted() + { + return we_modified_they_deleted; + } + + std::set& GetTheyModifiedWeDeleted() + { + return they_modified_we_deleted; + } + + std::set& GetBothModified() + { + return both_modified; + } + + protected: + std::unique_ptr readBoard( wxString& aFilename ); + KIGIT_PCB_MERGE_DIFFERENCES compareBoards( BOARD* aAncestor, BOARD* aOther ); + void findSetDifferences( const BOARD_ITEM_SET& aAncestorSet, const BOARD_ITEM_SET& aOtherSet, + std::vector& aAdded, std::vector& aRemoved, + std::vector& aChanged ); + + private: + + git_merge_driver_source* m_mergeDriver; + git_buf* m_result; + + std::set we_modified_they_deleted; + std::set they_modified_we_deleted; + std::set both_modified; +}; + + + +#endif // KIGIT_PCB_MERGE_H \ No newline at end of file diff --git a/pcbnew/netinfo.h b/pcbnew/netinfo.h index f86ae1da53..045804cd6e 100644 --- a/pcbnew/netinfo.h +++ b/pcbnew/netinfo.h @@ -171,6 +171,16 @@ public: bool Matches( const EDA_SEARCH_DATA& aSearchData, void* aAuxData ) const override; + double Similarity( const BOARD_ITEM& aBoardItem ) const override + { + return 0.0; + } + + bool operator==( const BOARD_ITEM& aBoardItem ) const override + { + return 0.0; + } + private: friend class NETINFO_LIST; diff --git a/pcbnew/pad.cpp b/pcbnew/pad.cpp index 249be9b65e..98dab89f2f 100644 --- a/pcbnew/pad.cpp +++ b/pcbnew/pad.cpp @@ -1684,6 +1684,186 @@ void PAD::TransformShapeToPolygon( SHAPE_POLY_SET& aBuffer, PCB_LAYER_ID aLayer, } +bool PAD::operator==( const BOARD_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return false; + + if( m_parent->m_Uuid != aOther.GetParent()->m_Uuid ) + return false; + + const PAD& other = static_cast( aOther ); + + if( GetShape() != other.GetShape() ) + return false; + + if( GetPosition() != other.GetPosition() ) + return false; + + if( GetAttribute() != other.GetAttribute() ) + return false; + + if( GetSize() != other.GetSize() ) + return false; + + if( GetOffset() != other.GetOffset() ) + return false; + + if( GetDrillSize() != other.GetDrillSize() ) + return false; + + if( GetDrillShape() != other.GetDrillShape() ) + return false; + + if( GetRoundRectRadiusRatio() != other.GetRoundRectRadiusRatio() ) + return false; + + if( GetChamferRectRatio() != other.GetChamferRectRatio() ) + return false; + + if( GetChamferPositions() != other.GetChamferPositions() ) + return false; + + if( GetOrientation() != other.GetOrientation() ) + return false; + + if( GetZoneConnection() != other.GetZoneConnection() ) + return false; + + if( GetThermalSpokeWidth() != other.GetThermalSpokeWidth() ) + return false; + + if( GetThermalSpokeAngle() != other.GetThermalSpokeAngle() ) + return false; + + if( GetThermalGap() != other.GetThermalGap() ) + return false; + + if( GetCustomShapeInZoneOpt() != other.GetCustomShapeInZoneOpt() ) + return false; + + if( GetPrimitives().size() != other.GetPrimitives().size() ) + return false; + + for( size_t ii = 0; ii < GetPrimitives().size(); ii++ ) + { + if( GetPrimitives()[ii] != other.GetPrimitives()[ii] ) + return false; + } + + if( GetAnchorPadShape() != other.GetAnchorPadShape() ) + return false; + + if( GetLocalClearance() != other.GetLocalClearance() ) + return false; + + if( GetLocalSolderMaskMargin() != other.GetLocalSolderMaskMargin() ) + return false; + + if( GetLocalSolderPasteMargin() != other.GetLocalSolderPasteMargin() ) + return false; + + if( GetLocalSolderPasteMarginRatio() != other.GetLocalSolderPasteMarginRatio() ) + return false; + + if( GetLocalSpokeWidthOverride() != other.GetLocalSpokeWidthOverride() ) + return false; + + if( GetLayerSet() != other.GetLayerSet() ) + return false; + + return true; +} + + +double PAD::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + if( m_parent->m_Uuid != aOther.GetParent()->m_Uuid ) + return 0.0; + + const PAD& other = static_cast( aOther ); + + double similarity = 1.0; + + if( GetShape() != other.GetShape() ) + similarity *= 0.9; + + if( GetPosition() != other.GetPosition() ) + similarity *= 0.9; + + if( GetAttribute() != other.GetAttribute() ) + similarity *= 0.9; + + if( GetSize() != other.GetSize() ) + similarity *= 0.9; + + if( GetOffset() != other.GetOffset() ) + similarity *= 0.9; + + if( GetDrillSize() != other.GetDrillSize() ) + similarity *= 0.9; + + if( GetDrillShape() != other.GetDrillShape() ) + similarity *= 0.9; + + if( GetRoundRectRadiusRatio() != other.GetRoundRectRadiusRatio() ) + similarity *= 0.9; + + if( GetChamferRectRatio() != other.GetChamferRectRatio() ) + similarity *= 0.9; + + if( GetChamferPositions() != other.GetChamferPositions() ) + similarity *= 0.9; + + if( GetOrientation() != other.GetOrientation() ) + similarity *= 0.9; + + if( GetZoneConnection() != other.GetZoneConnection() ) + similarity *= 0.9; + + if( GetThermalSpokeWidth() != other.GetThermalSpokeWidth() ) + similarity *= 0.9; + + if( GetThermalSpokeAngle() != other.GetThermalSpokeAngle() ) + similarity *= 0.9; + + if( GetThermalGap() != other.GetThermalGap() ) + similarity *= 0.9; + + if( GetCustomShapeInZoneOpt() != other.GetCustomShapeInZoneOpt() ) + similarity *= 0.9; + + if( GetPrimitives().size() != other.GetPrimitives().size() ) + similarity *= 0.9; + + if( GetAnchorPadShape() != other.GetAnchorPadShape() ) + similarity *= 0.9; + + if( GetLocalClearance() != other.GetLocalClearance() ) + similarity *= 0.9; + + if( GetLocalSolderMaskMargin() != other.GetLocalSolderMaskMargin() ) + similarity *= 0.9; + + if( GetLocalSolderPasteMargin() != other.GetLocalSolderPasteMargin() ) + similarity *= 0.9; + + if( GetLocalSolderPasteMarginRatio() != other.GetLocalSolderPasteMarginRatio() ) + similarity *= 0.9; + + if( GetLocalSpokeWidthOverride() != other.GetLocalSpokeWidthOverride() ) + similarity *= 0.9; + + if( GetLayerSet() != other.GetLayerSet() ) + similarity *= 0.9; + + return similarity; +} + + static struct PAD_DESC { PAD_DESC() diff --git a/pcbnew/pad.h b/pcbnew/pad.h index 11c197414b..3ea37faa08 100644 --- a/pcbnew/pad.h +++ b/pcbnew/pad.h @@ -720,6 +720,10 @@ public: m_zoneLayerOverrides.at( aLayer ) = aOverride; } + double Similarity( const BOARD_ITEM& aOther ) const override; + + bool operator==( const BOARD_ITEM& aOther ) const override; + #if defined(DEBUG) virtual void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/pcbnew/pcb_bitmap.cpp b/pcbnew/pcb_bitmap.cpp index 8f76879696..82cca11a61 100644 --- a/pcbnew/pcb_bitmap.cpp +++ b/pcbnew/pcb_bitmap.cpp @@ -255,6 +255,72 @@ void PCB_BITMAP::ViewGetLayers( int aLayers[], int& aCount ) const } +bool PCB_BITMAP::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const PCB_BITMAP& other = static_cast( aOther ); + + if( m_layer != other.m_layer ) + return false; + + if( m_pos != other.m_pos ) + return false; + + if( m_bitmapBase->GetSize() != other.m_bitmapBase->GetSize() ) + return false; + + if( m_bitmapBase->GetPPI() != other.m_bitmapBase->GetPPI() ) + return false; + + if( m_bitmapBase->GetScale() != other.m_bitmapBase->GetScale() ) + return false; + + if( m_bitmapBase->GetImageID() != other.m_bitmapBase->GetImageID() ) + return false; + + if( m_bitmapBase->GetImageData() != other.m_bitmapBase->GetImageData() ) + return false; + + return true; +} + + +double PCB_BITMAP::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const PCB_BITMAP& other = static_cast( aOther ); + + double similarity = 1.0; + + if( m_layer != other.m_layer ) + similarity *= 0.9; + + if( m_pos != other.m_pos ) + similarity *= 0.9; + + if( m_bitmapBase->GetSize() != other.m_bitmapBase->GetSize() ) + similarity *= 0.9; + + if( m_bitmapBase->GetPPI() != other.m_bitmapBase->GetPPI() ) + similarity *= 0.9; + + if( m_bitmapBase->GetScale() != other.m_bitmapBase->GetScale() ) + similarity *= 0.9; + + if( m_bitmapBase->GetImageID() != other.m_bitmapBase->GetImageID() ) + similarity *= 0.9; + + if( m_bitmapBase->GetImageData() != other.m_bitmapBase->GetImageData() ) + similarity *= 0.9; + + return similarity; +} + + static struct PCB_BITMAP_DESC { PCB_BITMAP_DESC() diff --git a/pcbnew/pcb_bitmap.h b/pcbnew/pcb_bitmap.h index 68236809ac..fe3e15d16b 100644 --- a/pcbnew/pcb_bitmap.h +++ b/pcbnew/pcb_bitmap.h @@ -141,6 +141,10 @@ public: EDA_ITEM* Clone() const override; + double Similarity( const BOARD_ITEM& aBoardItem ) const override; + + bool operator==( const BOARD_ITEM& aBoardItem ) const override; + #if defined( DEBUG ) void Show( int nestLevel, std::ostream& os ) const override; #endif diff --git a/pcbnew/pcb_dimension.cpp b/pcbnew/pcb_dimension.cpp index 47a54a2fd0..81ef56e52f 100644 --- a/pcbnew/pcb_dimension.cpp +++ b/pcbnew/pcb_dimension.cpp @@ -62,6 +62,101 @@ PCB_DIMENSION_BASE::PCB_DIMENSION_BASE( BOARD_ITEM* aParent, KICAD_T aType ) : } +bool PCB_DIMENSION_BASE::operator==( const BOARD_ITEM& aOther ) const +{ + if( Type() != aOther.Type() ) + return false; + + const PCB_DIMENSION_BASE& other = static_cast( aOther ); + + if( m_textPosition != other.m_textPosition ) + return false; + + if( m_keepTextAligned != other.m_keepTextAligned ) + return false; + + if( m_units != other.m_units ) + return false; + + if( m_autoUnits != other.m_autoUnits ) + return false; + + if( m_unitsFormat != other.m_unitsFormat ) + return false; + + if( m_precision != other.m_precision ) + return false; + + if( m_suppressZeroes != other.m_suppressZeroes ) + return false; + + if( m_lineThickness != other.m_lineThickness ) + return false; + + if( m_arrowLength != other.m_arrowLength ) + return false; + + if( m_extensionOffset != other.m_extensionOffset ) + return false; + + if( m_measuredValue != other.m_measuredValue ) + return false; + + return EDA_TEXT::operator==( other ); +} + + +double PCB_DIMENSION_BASE::Similarity( const BOARD_ITEM& aOther ) const +{ + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + if( Type() != aOther.Type() ) + return 0.0; + + const PCB_DIMENSION_BASE& other = static_cast( aOther ); + + double similarity = 1.0; + + if( m_textPosition != other.m_textPosition ) + similarity *= 0.9; + + if( m_keepTextAligned != other.m_keepTextAligned ) + similarity *= 0.9; + + if( m_units != other.m_units ) + similarity *= 0.9; + + if( m_autoUnits != other.m_autoUnits ) + similarity *= 0.9; + + if( m_unitsFormat != other.m_unitsFormat ) + similarity *= 0.9; + + if( m_precision != other.m_precision ) + similarity *= 0.9; + + if( m_suppressZeroes != other.m_suppressZeroes ) + similarity *= 0.9; + + if( m_lineThickness != other.m_lineThickness ) + similarity *= 0.9; + + if( m_arrowLength != other.m_arrowLength ) + similarity *= 0.9; + + if( m_extensionOffset != other.m_extensionOffset ) + similarity *= 0.9; + + if( m_measuredValue != other.m_measuredValue ) + similarity *= 0.9; + + similarity *= EDA_TEXT::Similarity( other ); + + return similarity; +} + + void PCB_DIMENSION_BASE::updateText() { wxString text = m_overrideTextEnabled ? m_valueString : GetValueText(); diff --git a/pcbnew/pcb_dimension.h b/pcbnew/pcb_dimension.h index 0386f4e7e3..9989a13884 100644 --- a/pcbnew/pcb_dimension.h +++ b/pcbnew/pcb_dimension.h @@ -278,6 +278,10 @@ public: int aError, ERROR_LOC aErrorLoc, bool aIgnoreLineWidth = false ) const override; + double Similarity( const BOARD_ITEM& aOther ) const override; + + bool operator==( const BOARD_ITEM& aOther ) const override; + #if defined(DEBUG) virtual void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/pcbnew/pcb_field.cpp b/pcbnew/pcb_field.cpp index 22a90f64bf..ca9e6f60ef 100644 --- a/pcbnew/pcb_field.cpp +++ b/pcbnew/pcb_field.cpp @@ -116,7 +116,7 @@ wxString PCB_FIELD::GetItemDescription( UNITS_PROVIDER* aUnitsProvider ) const KIUI::EllipsizeMenuText( GetText() ), GetParentFootprint()->GetReference() ); - default: + default: break; // avoid unreachable code / missing return statement warnings } @@ -158,6 +158,41 @@ EDA_ITEM* PCB_FIELD::Clone() const } +bool PCB_FIELD::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const PCB_FIELD& other = static_cast( aOther ); + + return m_id == other.m_id && m_name == other.m_name && EDA_TEXT::operator==( other ); +} + + +double PCB_FIELD::Similarity( const BOARD_ITEM& aOther ) const +{ + if( m_Uuid == aOther.m_Uuid ) + return 1.0; + + if( aOther.Type() != Type() ) + return 0.0; + + const PCB_FIELD& other = static_cast( aOther ); + + if( m_id < MANDATORY_FIELDS || other.m_id < MANDATORY_FIELDS ) + { + if( m_id == other.m_id ) + return 1.0; + else + return 0.0; + } + + if( m_name == other.m_name ) + return 1.0; + + return EDA_TEXT::Similarity( other ); +} + static struct PCB_FIELD_DESC { PCB_FIELD_DESC() diff --git a/pcbnew/pcb_field.h b/pcbnew/pcb_field.h index c4a54987b6..90e190549d 100644 --- a/pcbnew/pcb_field.h +++ b/pcbnew/pcb_field.h @@ -111,6 +111,10 @@ public: int GetId() const { return m_id; } + double Similarity( const BOARD_ITEM& aOther ) const override; + + bool operator==( const BOARD_ITEM& aOther ) const override; + private: int m_id; ///< Field index, @see enum MANDATORY_FIELD_T diff --git a/pcbnew/pcb_group.cpp b/pcbnew/pcb_group.cpp index 33051c04df..155f35c04b 100644 --- a/pcbnew/pcb_group.cpp +++ b/pcbnew/pcb_group.cpp @@ -434,3 +434,50 @@ void PCB_GROUP::RunOnDescendants( const std::function& aFun wxFAIL_MSG( wxT( "Error calling function in PCB_GROUP::RunOnDescendants" ) ); } } + +bool PCB_GROUP::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const PCB_GROUP& other = static_cast( aOther ); + + if( m_items.size() != other.m_items.size() ) + return false; + + // The items in groups are in unordered sets hashed by the pointer value, so we need to + // order them by UUID (EDA_ITEM_SET) to compare + EDA_ITEM_SET itemSet( m_items.begin(), m_items.end() ); + EDA_ITEM_SET otherItemSet( other.m_items.begin(), other.m_items.end() ); + + for( auto it1 = itemSet.begin(), it2 = otherItemSet.begin(); it1 != itemSet.end(); ++it1, ++it2 ) + { + // Compare UUID instead of the items themselves because we only care if the contents + // of the group has changed, not which elements in the group have changed + if( ( *it1 )->m_Uuid != ( *it2 )->m_Uuid ) + return false; + } + + return true; +} + + +double PCB_GROUP::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const PCB_GROUP& other = static_cast( aOther ); + + double similarity = 0.0; + + for( BOARD_ITEM* item : m_items ) + { + for( BOARD_ITEM* otherItem : other.m_items ) + { + similarity += item->Similarity( *otherItem ); + } + } + + return similarity / m_items.size(); +} diff --git a/pcbnew/pcb_marker.h b/pcbnew/pcb_marker.h index b881bd9948..5e1011c9eb 100644 --- a/pcbnew/pcb_marker.h +++ b/pcbnew/pcb_marker.h @@ -112,6 +112,16 @@ public: SEVERITY GetSeverity() const override; + double Similarity( const BOARD_ITEM& aBoardItem ) const override + { + return 0.0; + } + + bool operator==( const BOARD_ITEM& aBoardItem ) const override + { + return false; + } + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/pcbnew/pcb_shape.cpp b/pcbnew/pcb_shape.cpp index 5615d4e52a..a373e9e8cd 100644 --- a/pcbnew/pcb_shape.cpp +++ b/pcbnew/pcb_shape.cpp @@ -573,6 +573,68 @@ void PCB_SHAPE::TransformShapeToPolygon( SHAPE_POLY_SET& aBuffer, PCB_LAYER_ID a } +bool PCB_SHAPE::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const PCB_SHAPE& other = static_cast( aOther ); + + if( m_layer != other.m_layer ) + return false; + + if( m_isKnockout != other.m_isKnockout ) + return false; + + if( m_isLocked != other.m_isLocked ) + return false; + + if( m_flags != other.m_flags ) + return false; + + if( m_forceVisible != other.m_forceVisible ) + return false; + + if( m_netinfo->GetNetCode() != other.m_netinfo->GetNetCode() ) + return false; + + return EDA_SHAPE::operator==( other ); +} + + +double PCB_SHAPE::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const PCB_SHAPE& other = static_cast( aOther ); + + double similarity = 1.0; + + if( GetLayer() != other.GetLayer() ) + similarity *= 0.9; + + if( m_isKnockout != other.m_isKnockout ) + similarity *= 0.9; + + if( m_isLocked != other.m_isLocked ) + similarity *= 0.9; + + if( m_flags != other.m_flags ) + similarity *= 0.9; + + if( m_forceVisible != other.m_forceVisible ) + similarity *= 0.9; + + if( m_netinfo->GetNetCode() != other.m_netinfo->GetNetCode() ) + similarity *= 0.9; + + similarity *= EDA_SHAPE::Similarity( other ); + + return similarity; +} + + static struct PCB_SHAPE_DESC { PCB_SHAPE_DESC() diff --git a/pcbnew/pcb_shape.h b/pcbnew/pcb_shape.h index 1a5a224831..cfec12a4e5 100644 --- a/pcbnew/pcb_shape.h +++ b/pcbnew/pcb_shape.h @@ -160,6 +160,10 @@ public: ///< @copydoc VIEW_ITEM::ViewGetLOD double ViewGetLOD( int aLayer, KIGFX::VIEW* aView ) const override; + double Similarity( const BOARD_ITEM& aBoardItem ) const override; + + bool operator==( const BOARD_ITEM& aBoardItem ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/pcbnew/pcb_target.cpp b/pcbnew/pcb_target.cpp index 0386f7c242..ad13e865c3 100644 --- a/pcbnew/pcb_target.cpp +++ b/pcbnew/pcb_target.cpp @@ -189,6 +189,45 @@ void PCB_TARGET::TransformShapeToPolygon( SHAPE_POLY_SET& aBuffer, PCB_LAYER_ID } +bool PCB_TARGET::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const PCB_TARGET& other = static_cast( aOther ); + + return m_shape == other.m_shape && m_size == other.m_size && m_lineWidth == other.m_lineWidth + && m_layer == other.m_layer && m_pos == other.m_pos; +} + + +double PCB_TARGET::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const PCB_TARGET& other = static_cast( aOther ); + + double similarity = 1.0; + + if( GetShape() != other.GetShape() ) + similarity *= 0.9; + + if( GetSize() != other.GetSize() ) + similarity *= 0.9; + + if( GetWidth() != other.GetWidth() ) + similarity *= 0.9; + + if( GetLayer() != other.GetLayer() ) + similarity *= 0.9; + + if( GetPosition() != other.GetPosition() ) + similarity *= 0.9; + + return 1.0; +} + static struct PCB_TARGET_DESC { PCB_TARGET_DESC() diff --git a/pcbnew/pcb_target.h b/pcbnew/pcb_target.h index 90bf4362b6..ace5b06e79 100644 --- a/pcbnew/pcb_target.h +++ b/pcbnew/pcb_target.h @@ -110,6 +110,10 @@ public: int aError, ERROR_LOC aErrorLoc, bool ignoreLineWidth = false ) const override; + double Similarity( const BOARD_ITEM& aBoardItem ) const override; + + bool operator==( const BOARD_ITEM& aBoardItem ) const override; + #if defined(DEBUG) void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/pcbnew/pcb_text.cpp b/pcbnew/pcb_text.cpp index 27e6d7f429..33e846a15b 100644 --- a/pcbnew/pcb_text.cpp +++ b/pcbnew/pcb_text.cpp @@ -527,6 +527,28 @@ void PCB_TEXT::TransformShapeToPolygon( SHAPE_POLY_SET& aBuffer, PCB_LAYER_ID aL } +bool PCB_TEXT::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const PCB_TEXT& other = static_cast( aOther ); + + return EDA_TEXT::operator==( other ); +} + + +double PCB_TEXT::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const PCB_TEXT& other = static_cast( aOther ); + + return EDA_TEXT::Similarity( other ); +} + + static struct PCB_TEXT_DESC { PCB_TEXT_DESC() diff --git a/pcbnew/pcb_text.h b/pcbnew/pcb_text.h index bada23a393..dcf647ed10 100644 --- a/pcbnew/pcb_text.h +++ b/pcbnew/pcb_text.h @@ -161,6 +161,10 @@ public: EDA_ITEM* Clone() const override; + double Similarity( const BOARD_ITEM& aBoardItem ) const override; + + bool operator==( const BOARD_ITEM& aBoardItem ) const override; + #if defined(DEBUG) virtual void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } #endif diff --git a/pcbnew/pcb_textbox.cpp b/pcbnew/pcb_textbox.cpp index fabcbd4789..d09085b52c 100644 --- a/pcbnew/pcb_textbox.cpp +++ b/pcbnew/pcb_textbox.cpp @@ -584,6 +584,36 @@ void PCB_TEXTBOX::SetBorderWidth( const int aSize ) } + +bool PCB_TEXTBOX::operator==( const BOARD_ITEM& aBoardItem ) const +{ + if( aBoardItem.Type() != Type() ) + return false; + + const PCB_TEXTBOX& other = static_cast( aBoardItem ); + + return m_borderEnabled == other.m_borderEnabled && EDA_TEXT::operator==( other ); +} + + +double PCB_TEXTBOX::Similarity( const BOARD_ITEM& aBoardItem ) const +{ + if( aBoardItem.Type() != Type() ) + return 0.0; + + const PCB_TEXTBOX& other = static_cast( aBoardItem ); + + double similarity = 1.0; + + if( m_borderEnabled != other.m_borderEnabled ) + similarity *= 0.9; + + similarity *= EDA_TEXT::Similarity( other ); + + return similarity; +} + + static struct PCB_TEXTBOX_DESC { PCB_TEXTBOX_DESC() diff --git a/pcbnew/pcb_textbox.h b/pcbnew/pcb_textbox.h index ce70348b3f..65424dc7e9 100644 --- a/pcbnew/pcb_textbox.h +++ b/pcbnew/pcb_textbox.h @@ -149,6 +149,10 @@ public: void SetBorderWidth( const int aSize ); int GetBorderWidth() const { return m_stroke.GetWidth(); } + double Similarity( const BOARD_ITEM& aBoardItem ) const override; + + bool operator==( const BOARD_ITEM& aBoardItem ) const override; + protected: bool m_borderEnabled; ///< Controls drawing the border (as defined by the stroke members) diff --git a/pcbnew/pcb_track.cpp b/pcbnew/pcb_track.cpp index 69283f0c29..571e53825e 100644 --- a/pcbnew/pcb_track.cpp +++ b/pcbnew/pcb_track.cpp @@ -151,6 +151,142 @@ BITMAPS PCB_VIA::GetMenuImage() const } +bool PCB_TRACK::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const PCB_TRACK& other = static_cast( aOther ); + + return m_Start == other.m_Start && m_End == other.m_End && m_layer == other.m_layer && + m_Width == other.m_Width; +} + + +double PCB_TRACK::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const PCB_TRACK& other = static_cast( aOther ); + + double similarity = 1.0; + + if( m_layer != other.m_layer ) + similarity *= 0.9; + + if( m_Width != other.m_Width ) + similarity *= 0.9; + + if( m_Start != other.m_Start ) + similarity *= 0.9; + + if( m_End != other.m_End ) + similarity *= 0.9; + + return similarity; +} + + +bool PCB_ARC::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const PCB_ARC& other = static_cast( aOther ); + + return m_Start == other.m_Start && m_End == other.m_End && m_Mid == other.m_Mid && + m_layer == other.m_layer && m_Width == other.m_Width; +} + + +double PCB_ARC::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const PCB_ARC& other = static_cast( aOther ); + + double similarity = 1.0; + + if( m_layer != other.m_layer ) + similarity *= 0.9; + + if( m_Width != other.m_Width ) + similarity *= 0.9; + + if( m_Start != other.m_Start ) + similarity *= 0.9; + + if( m_End != other.m_End ) + similarity *= 0.9; + + if( m_Mid != other.m_Mid ) + similarity *= 0.9; + + return similarity; +} + + +bool PCB_VIA::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const PCB_VIA& other = static_cast( aOther ); + + return m_Start == other.m_Start && m_End == other.m_End && m_layer == other.m_layer && + m_bottomLayer == other.m_bottomLayer && m_Width == other.m_Width && + m_viaType == other.m_viaType && m_drill == other.m_drill && + m_removeUnconnectedLayer == other.m_removeUnconnectedLayer && + m_keepStartEndLayer == other.m_keepStartEndLayer && + m_zoneLayerOverrides == other.m_zoneLayerOverrides; +} + + +double PCB_VIA::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const PCB_VIA& other = static_cast( aOther ); + + double similarity = 1.0; + + if( m_layer != other.m_layer ) + similarity *= 0.9; + + if( m_Width != other.m_Width ) + similarity *= 0.9; + + if( m_Start != other.m_Start ) + similarity *= 0.9; + + if( m_End != other.m_End ) + similarity *= 0.9; + + if( m_bottomLayer != other.m_bottomLayer ) + similarity *= 0.9; + + if( m_viaType != other.m_viaType ) + similarity *= 0.9; + + if( m_drill != other.m_drill ) + similarity *= 0.9; + + if( m_removeUnconnectedLayer != other.m_removeUnconnectedLayer ) + similarity *= 0.9; + + if( m_keepStartEndLayer != other.m_keepStartEndLayer ) + similarity *= 0.9; + + if( m_zoneLayerOverrides != other.m_zoneLayerOverrides ) + similarity *= 0.9; + + return similarity; +} + + bool PCB_TRACK::ApproxCollinear( const PCB_TRACK& aTrack ) { SEG a( m_Start, m_End ); diff --git a/pcbnew/pcb_track.h b/pcbnew/pcb_track.h index 06681433ff..a7491db6e3 100644 --- a/pcbnew/pcb_track.h +++ b/pcbnew/pcb_track.h @@ -249,6 +249,10 @@ public: return m_CachedScale; } + virtual double Similarity( const BOARD_ITEM& aOther ) const override; + + virtual bool operator==( const BOARD_ITEM& aOther ) const override; + /** * Set the cached scale. * @@ -360,6 +364,10 @@ public: */ bool IsDegenerated( int aThreshold = 5 ) const; + double Similarity( const BOARD_ITEM& aOther ) const override; + + bool operator==( const BOARD_ITEM& aOther ) const override; + protected: virtual void swapData( BOARD_ITEM* aImage ) override; @@ -579,6 +587,10 @@ public: m_zoneLayerOverrides.at( aLayer ) = aOverride; } + double Similarity( const BOARD_ITEM& aOther ) const override; + + bool operator==( const BOARD_ITEM& aOther ) const override; + protected: void swapData( BOARD_ITEM* aImage ) override; diff --git a/pcbnew/python/scripting/pcbnew_action_plugins.cpp b/pcbnew/python/scripting/pcbnew_action_plugins.cpp index d7385fdc1b..58ffe009c0 100644 --- a/pcbnew/python/scripting/pcbnew_action_plugins.cpp +++ b/pcbnew/python/scripting/pcbnew_action_plugins.cpp @@ -333,7 +333,7 @@ void PCB_EDIT_FRAME::RunActionPlugin( ACTION_PLUGIN* aActionPlugin ) PICKED_ITEMS_LIST deletedItemsList; // The list of existing items after running the action script - const std::set currItemList = currentPcb->GetItemSet(); + const auto currItemList = currentPcb->GetItemSet(); // Found deleted items for( unsigned int i = 0; i < oldBuffer->GetCount(); i++ ) @@ -418,7 +418,7 @@ void PCB_EDIT_FRAME::RunActionPlugin( ACTION_PLUGIN* aActionPlugin ) void PCB_EDIT_FRAME::RebuildAndRefresh() { // The list of existing items after running the action script - const std::set items = GetBoard()->GetItemSet(); + const BOARD_ITEM_SET items = GetBoard()->GetItemSet(); // Sync selection with items selection state SELECTION& selection = GetCurrentSelection(); diff --git a/pcbnew/teardrop/teardrop_parameters.h b/pcbnew/teardrop/teardrop_parameters.h index c9cdba3f0c..85bfc29479 100644 --- a/pcbnew/teardrop/teardrop_parameters.h +++ b/pcbnew/teardrop/teardrop_parameters.h @@ -92,6 +92,24 @@ public: bool IsCurved() const { return m_CurveSegCount > 2; } + bool operator== ( const TEARDROP_PARAMETERS& aOther ) const + { + return m_Enabled == aOther.m_Enabled && + m_AllowUseTwoTracks == aOther.m_AllowUseTwoTracks && + m_TdMaxLen == aOther.m_TdMaxLen && + m_TdMaxWidth == aOther.m_TdMaxWidth && + m_BestLengthRatio == aOther.m_BestLengthRatio && + m_BestWidthRatio == aOther.m_BestWidthRatio && + m_CurveSegCount == aOther.m_CurveSegCount && + m_WidthtoSizeFilterRatio == aOther.m_WidthtoSizeFilterRatio && + m_TdOnPadsInZones == aOther.m_TdOnPadsInZones; + } + + bool operator!= ( const TEARDROP_PARAMETERS& aOther ) const + { + return !( *this == aOther ); + } + public: bool m_Enabled; /// True to create teardrops using 2 track segments if the first in too small diff --git a/pcbnew/zone.cpp b/pcbnew/zone.cpp index 0a42889731..f148d3e889 100644 --- a/pcbnew/zone.cpp +++ b/pcbnew/zone.cpp @@ -1393,6 +1393,140 @@ void ZONE::TransformSolidAreasShapesToPolygon( PCB_LAYER_ID aLayer, SHAPE_POLY_S } +bool ZONE::operator==( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return false; + + const ZONE& other = static_cast( aOther ); + + if( GetIsRuleArea() != other.GetIsRuleArea() ) + return false; + + if( GetLayerSet() != other.GetLayerSet() ) + return false; + + if( GetNetCode() != other.GetNetCode() ) + return false; + + if( GetIsRuleArea() ) + { + if( GetDoNotAllowCopperPour() != other.GetDoNotAllowCopperPour() ) + return false; + if( GetDoNotAllowTracks() != other.GetDoNotAllowTracks() ) + return false; + if( GetDoNotAllowVias() != other.GetDoNotAllowVias() ) + return false; + if( GetDoNotAllowFootprints() != other.GetDoNotAllowFootprints() ) + return false; + if( GetDoNotAllowPads() != other.GetDoNotAllowPads() ) + return false; + } + else + { + if( GetAssignedPriority() != other.GetAssignedPriority() ) + return false; + + if( GetMinThickness() != other.GetMinThickness() ) + return false; + + if( GetCornerSmoothingType() != other.GetCornerSmoothingType() ) + return false; + + if( GetCornerRadius() != other.GetCornerRadius() ) + return false; + + if( GetTeardropParams() != other.GetTeardropParams() ) + return false; + } + + if( GetNumCorners() != other.GetNumCorners() ) + return false; + + for( int ii = 0; ii < GetNumCorners(); ii++ ) + { + if( GetCornerPosition( ii ) != other.GetCornerPosition( ii ) ) + return false; + } + + return true; +} + + +double ZONE::Similarity( const BOARD_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const ZONE& other = static_cast( aOther ); + + if( GetIsRuleArea() != other.GetIsRuleArea() ) + return 0.0; + + double similarity = 1.0; + + if( GetLayerSet() != other.GetLayerSet() ) + similarity *= 0.9; + + if( GetNetCode() != other.GetNetCode() ) + similarity *= 0.9; + + if( !GetIsRuleArea() ) + { + if( GetAssignedPriority() != other.GetAssignedPriority() ) + similarity *= 0.9; + + if( GetMinThickness() != other.GetMinThickness() ) + similarity *= 0.9; + + if( GetCornerSmoothingType() != other.GetCornerSmoothingType() ) + similarity *= 0.9; + + if( GetCornerRadius() != other.GetCornerRadius() ) + similarity *= 0.9; + + if( GetTeardropParams() != other.GetTeardropParams() ) + similarity *= 0.9; + } + else + { + if( GetDoNotAllowCopperPour() != other.GetDoNotAllowCopperPour() ) + similarity *= 0.9; + if( GetDoNotAllowTracks() != other.GetDoNotAllowTracks() ) + similarity *= 0.9; + if( GetDoNotAllowVias() != other.GetDoNotAllowVias() ) + similarity *= 0.9; + if( GetDoNotAllowFootprints() != other.GetDoNotAllowFootprints() ) + similarity *= 0.9; + if( GetDoNotAllowPads() != other.GetDoNotAllowPads() ) + similarity *= 0.9; + } + + std::vector corners; + std::vector otherCorners; + VECTOR2I lastCorner( 0, 0 ); + + for( int ii = 0; ii < GetNumCorners(); ii++ ) + { + corners.push_back( lastCorner - GetCornerPosition( ii ) ); + lastCorner = GetCornerPosition( ii ); + } + + lastCorner = VECTOR2I( 0, 0 ); + for( int ii = 0; ii < other.GetNumCorners(); ii++ ) + { + otherCorners.push_back( lastCorner - other.GetCornerPosition( ii ) ); + lastCorner = other.GetCornerPosition( ii ); + } + + size_t longest = alg::longest_common_subset( corners, otherCorners ); + + similarity *= std::pow( 0.9, GetNumCorners() + other.GetNumCorners() - 2 * longest ); + + return similarity; +} + + static struct ZONE_DESC { ZONE_DESC() diff --git a/pcbnew/zone.h b/pcbnew/zone.h index 7d1c157d4c..b8cce01c7e 100644 --- a/pcbnew/zone.h +++ b/pcbnew/zone.h @@ -784,6 +784,10 @@ public: */ MD5_HASH GetHashValue( PCB_LAYER_ID aLayer ); + double Similarity( const BOARD_ITEM& aOther ) const override; + + bool operator==( const BOARD_ITEM& aOther ) const override; + #if defined(DEBUG) virtual void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } diff --git a/resources/bitmaps_png/CMakeLists.txt b/resources/bitmaps_png/CMakeLists.txt index ec06956bd2..1a41fe81a4 100644 --- a/resources/bitmaps_png/CMakeLists.txt +++ b/resources/bitmaps_png/CMakeLists.txt @@ -72,6 +72,13 @@ set( BMAPS_SMALL e_48 e_96 e_192 + git_add + git_changed_ahead + git_conflict + git_delete + git_good_check + git_modified + git_out_of_date icon_bitmap2component_16 icon_eeschema_16 icon_gerbview_16 diff --git a/resources/bitmaps_png/png/git_add_16.png b/resources/bitmaps_png/png/git_add_16.png new file mode 100644 index 0000000000000000000000000000000000000000..c044cd21ba243155e6d6ba0c76bf21ebda116d6f GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`_MR?|Ar_~TfBgS%lu|PUu z=hLUrr*+rg3rIMy`oDrr3s1wZpZ1J@*!mfPVkrjf&B}QH2hbvY2j)3_0yj558Hgkd<~-n2^J+W=6eBCCbYSwF){2r@l5yJ|58n$ OF$|urelF{r5}E*cx+RbR literal 0 HcmV?d00001 diff --git a/resources/bitmaps_png/png/git_changed_ahead_16.png b/resources/bitmaps_png/png/git_changed_ahead_16.png new file mode 100644 index 0000000000000000000000000000000000000000..c3ac0a4594930d5c1bf1ab92419214e5cfae6906 GIT binary patch literal 412 zcmV;N0b~A&P)1(Sx`J^@rgu})Jt{sEfj3+-$Bs2&SWPg z-K`bz@Ufej$v5A8lTwG9je&BeR;_a^@d<{A*PJ=! zQ0jN?M)@w{1?S)7|6!h>lV1_~(f2k$_=rI5;P;Ju@ChENpcdaW@+6gWy|{X&mI&m@ zN^CZm?4f9)(3iixJY4Bj!9-dKNwRMMSM}57K$v~386$cjTvw(Z5-06LqE(UGE@x$D zo$2z}Z!)WcZ(Nt*&b_?Cf=nJql1aQBgt@1go$R>;f5kUDsernVC>jL-0000@YYJ6tBR<~q^co@8kb}uJDW_+djFd$9-o@&QlBuc4=hP9 zU&j<-KNMlI+q_qA&QZK=ay;5GwZbSt?fHa2s8pz6>MH(15l z5}y||;0o0pYmr<~2RB&VQ5~lNhg57ukzA04WN>3uWC-SkrL7rApQ+Z8&5%M@a2U{L})wyZeVC&f*SAx zDR$~`=LHoQ12E0V!eVJ-j~pik8>005a$m|OU0dw~D|002ovPDHLkV1gNY Be8>O* literal 0 HcmV?d00001 diff --git a/resources/bitmaps_png/png/git_conflict_dark_16.png b/resources/bitmaps_png/png/git_conflict_dark_16.png new file mode 100644 index 0000000000000000000000000000000000000000..2247a589d85a553a14ba222f6b731fc46d381239 GIT binary patch literal 312 zcmV-80muG{P)fDHk<1g04V9!y;XHgun=V-vyvHOC7e z!2_Dk|6gp}g)rpbKNQUmXD>(cmeV<813tqf&<(+^84{q#2B3uegDDG9bpu_39426k zfZ>2_zr_Z~$G?ZwbAaY2b4 zXjpvMujU|vl8``&2y0RX8FEn5`6ISu3^D{{ zGqPr^nF*A_K#Amly5m`B^nM1hL4Jlg8?OPRVgQck!4lnHU%3DP002ovPDHLkV1kTK Bvts}N literal 0 HcmV?d00001 diff --git a/resources/bitmaps_png/png/git_delete_dark_16.png b/resources/bitmaps_png/png/git_delete_dark_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0daf03a425e0596f3f82752b2993e054d1127a74 GIT binary patch literal 385 zcmV-{0e=38P)cK=0Ae*jU!I!kVdJEALsGzPWKIkqgir{X=2R(*OgqkgzaVEBD(#8dG zU?8OVzs&rZb^xIBpKNW&tz;!*d>3;n82_Zi^(5=YJM?WKKz8{$3Irznl0vd%EeI&Z zTAhy}-#cuHzfP_p-#@0cb|JQGqO0K;Ow8wa_{fUY>CMl+){q|{B;uLe*j}~(>RCiF z6i=^UgCJOm6_=sTMh1$SfdwEjHMjK=m_4NRX}r-BU)J-!4XSrqsEXYBPwk-pdB;mCU8zE0`I z2ktNVgM`B&HV3+xytNG@W3du-bzcqAYIu|lB*}tfj1aon6^DN&oG=;^$@p{mypq<` fb5-_2z+dwlZuSq?y;{Ci00000NkvXXu0mjfdSkEJ literal 0 HcmV?d00001 diff --git a/resources/bitmaps_png/png/git_good_check_16.png b/resources/bitmaps_png/png/git_good_check_16.png new file mode 100644 index 0000000000000000000000000000000000000000..3c109da9be3b9566f0be7d3e8f022f6034636004 GIT binary patch literal 236 zcmVN6%^-Zvd5W_z zMVg870zS=f1AJ%tasee~f)t|%nCCRlTQJQ)_8ISK-hwEx0MN6%^-Zvd5W_z zMVg870zS=f1AJ%tasee~f)t|%nCCRlTQJQ)_8ISK-hwEx0M?09+ujE>gOVH literal 0 HcmV?d00001 diff --git a/resources/bitmaps_png/png/git_modified_dark_16.png b/resources/bitmaps_png/png/git_modified_dark_16.png new file mode 100644 index 0000000000000000000000000000000000000000..9bf84efd67ac247d4c187a20b4cd525b64f0558e GIT binary patch literal 336 zcmV-W0k8gvP)A@cJLyraYEY{rQe{GZ$J(Zn`4WaO&r?+lTx*Zku1kEJF~k9 zlv2T8aCIS{^T&$uH()IoKN9F%h1si|CZk>f8S)7T0#IH=ypj{i3HNc|3+7b5`DrnTLvd0aXej1x#>3iGI{}X_nPc5(`oA*SJjDJ`GEQx6%8g)wmq=ji>lr8NTF%q(5 z#>mon#|h4wY{n?!eArziEXTznl}+9{bLrfFqu7&wl}W@p@nP?h{XY}p(2`J-K#(*c ioL7{&wcRW5Tbuzy8pa0jONuT40000m~lmFl^s0R_;&0|5?{}FG#zCcfcXm%$V5pf@}I_ZQw zmWX&LW~O_(rn-8NBnke5M!;TSHS%y8`M8gLd^sr$)xSE@mNebEe@|c$oDEaX191C^1%sWaFFV6ov*rX*p_aY4YqqXb3vXBTVSKcyrduIoySi_MgP#+C^z@t$82BA%b+BE9+M zI!NK+)%LKhXIm!-D=&$0KE6s55R6E&!*wV=lnEZ>f)rJH?qklJVaZ%(cm~lmFl^s0R_;&0|5?{}FG#zCcfcXm%$V5pf@}I_ZQw zmWX&LW~O_(rn-8NBnke5M!;TSHS%y8`M8gLd^sr$)xSE@mNebEe@|c$oDEaX191C^1%sWaFFV6ov*rX*p_aY4YqqXb3vXBTVSKcyrduIoySi_MgP#+C^z@t$82BA%b+BE9+M zI!NK+)%LKhXIm!-D=&$0KE6s55R6E&!*wV=lnEZ>f)rJH?qklJVaZ%(c +image/svg+xml diff --git a/resources/bitmaps_png/sources/dark/git_changed_ahead.svg b/resources/bitmaps_png/sources/dark/git_changed_ahead.svg new file mode 100644 index 0000000000..8553cb0cd0 --- /dev/null +++ b/resources/bitmaps_png/sources/dark/git_changed_ahead.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/dark/git_conflict.svg b/resources/bitmaps_png/sources/dark/git_conflict.svg new file mode 100644 index 0000000000..d61e65cb15 --- /dev/null +++ b/resources/bitmaps_png/sources/dark/git_conflict.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/dark/git_delete.svg b/resources/bitmaps_png/sources/dark/git_delete.svg new file mode 100644 index 0000000000..fb4daad52d --- /dev/null +++ b/resources/bitmaps_png/sources/dark/git_delete.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/dark/git_good_check.svg b/resources/bitmaps_png/sources/dark/git_good_check.svg new file mode 100644 index 0000000000..9f87f19944 --- /dev/null +++ b/resources/bitmaps_png/sources/dark/git_good_check.svg @@ -0,0 +1,84 @@ + + + + + + + + Layer 1 + + + + + + + + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/dark/git_modified.svg b/resources/bitmaps_png/sources/dark/git_modified.svg new file mode 100644 index 0000000000..3037931200 --- /dev/null +++ b/resources/bitmaps_png/sources/dark/git_modified.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/dark/git_out_of_date.svg b/resources/bitmaps_png/sources/dark/git_out_of_date.svg new file mode 100644 index 0000000000..a7ed67f47e --- /dev/null +++ b/resources/bitmaps_png/sources/dark/git_out_of_date.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/light/git_add.svg b/resources/bitmaps_png/sources/light/git_add.svg new file mode 100644 index 0000000000..35f6bd0b68 --- /dev/null +++ b/resources/bitmaps_png/sources/light/git_add.svg @@ -0,0 +1,82 @@ + +image/svg+xml diff --git a/resources/bitmaps_png/sources/light/git_changed_ahead.svg b/resources/bitmaps_png/sources/light/git_changed_ahead.svg new file mode 100644 index 0000000000..a45a4368a3 --- /dev/null +++ b/resources/bitmaps_png/sources/light/git_changed_ahead.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/light/git_conflict.svg b/resources/bitmaps_png/sources/light/git_conflict.svg new file mode 100644 index 0000000000..eb99b1debb --- /dev/null +++ b/resources/bitmaps_png/sources/light/git_conflict.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/light/git_delete.svg b/resources/bitmaps_png/sources/light/git_delete.svg new file mode 100644 index 0000000000..39d339c4ec --- /dev/null +++ b/resources/bitmaps_png/sources/light/git_delete.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/light/git_good_check.svg b/resources/bitmaps_png/sources/light/git_good_check.svg new file mode 100644 index 0000000000..9f87f19944 --- /dev/null +++ b/resources/bitmaps_png/sources/light/git_good_check.svg @@ -0,0 +1,84 @@ + + + + + + + + Layer 1 + + + + + + + + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/light/git_modified.svg b/resources/bitmaps_png/sources/light/git_modified.svg new file mode 100644 index 0000000000..7cd76e34bd --- /dev/null +++ b/resources/bitmaps_png/sources/light/git_modified.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/bitmaps_png/sources/light/git_out_of_date.svg b/resources/bitmaps_png/sources/light/git_out_of_date.svg new file mode 100644 index 0000000000..4240cbfefc --- /dev/null +++ b/resources/bitmaps_png/sources/light/git_out_of_date.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + +