├── .gitignore ├── README.md ├── app.go ├── build ├── README.md ├── appicon.png ├── darwin │ ├── Info.dev.plist │ └── Info.plist └── windows │ ├── icon.ico │ ├── info.json │ ├── installer │ ├── project.nsi │ └── wails_tools.nsh │ └── wails.exe.manifest ├── common.go ├── frontend ├── index.html ├── package-lock.json ├── package.json ├── package.json.md5 ├── src │ ├── .DS_Store │ ├── App.css │ ├── App.tsx │ ├── assets │ │ ├── .DS_Store │ │ └── fonts │ │ │ ├── OFL.txt │ │ │ └── nunito-v16-latin-regular.woff2 │ ├── components │ │ ├── .DS_Store │ │ ├── block │ │ │ ├── Index.tsx │ │ │ └── index.scss │ │ ├── branch │ │ │ ├── Index.tsx │ │ │ ├── Item.tsx │ │ │ ├── mergeDialog.tsx │ │ │ └── style.scss │ │ ├── changes │ │ │ ├── FileState.tsx │ │ │ ├── Index.tsx │ │ │ ├── SubmitView.tsx │ │ │ └── style.scss │ │ ├── dialog │ │ │ ├── About.tsx │ │ │ ├── Input.tsx │ │ │ └── Theme.tsx │ │ ├── diff │ │ │ ├── DiffCommitView.tsx │ │ │ ├── DiffWorkView.tsx │ │ │ ├── FailedInfo.tsx │ │ │ └── style.scss │ │ ├── history │ │ │ ├── Index.tsx │ │ │ └── Item.tsx │ │ ├── repoSetting │ │ │ ├── Action.tsx │ │ │ ├── Card.tsx │ │ │ ├── Index.tsx │ │ │ └── index.scss │ │ ├── sides │ │ │ ├── Sides.tsx │ │ │ └── SidesTop.tsx │ │ ├── tag │ │ │ ├── Index.tsx │ │ │ ├── Item.tsx │ │ │ └── style.scss │ │ └── topBar │ │ │ ├── TopBar.tsx │ │ │ ├── action │ │ │ └── Action.tsx │ │ │ ├── breadcrumb │ │ │ └── Nav.tsx │ │ │ └── index.scss │ ├── config │ │ ├── app.ts │ │ └── gitCmd.ts │ ├── main.tsx │ ├── nativeMenuEvents.ts │ ├── store │ │ ├── index.ts │ │ ├── sliceCategory.ts │ │ ├── sliceCommitDiff.ts │ │ ├── sliceMain.ts │ │ ├── sliceSetting.ts │ │ └── sliceWorkDiff.ts │ ├── style.css │ ├── utils │ │ ├── common.ts │ │ ├── notification.tsx │ │ └── repo.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts ├── wailsjs │ ├── .DS_Store │ ├── go │ │ ├── main │ │ │ ├── App.d.ts │ │ │ └── App.js │ │ ├── models.ts │ │ └── repository │ │ │ ├── Repository.d.ts │ │ │ └── Repository.js │ └── runtime │ │ ├── package.json │ │ ├── runtime.d.ts │ │ └── runtime.js ├── yarn-error.log └── yarn.lock ├── go.mod ├── go.sum ├── main.go ├── repository ├── branch.go ├── commit.go ├── diffCommit.go ├── diffWork.go ├── merge.go ├── mian.go ├── tag.go ├── utils.go └── workTree.go ├── screenshots ├── branch.png ├── change.png ├── history.png └── tag.png ├── utils └── common.go └── wails.json /.gitignore: -------------------------------------------------------------------------------- 1 | build/bin 2 | node_modules 3 | frontend/dist 4 | .idea 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | - [Introduction](#Introduction) 3 | - [Features](#Features) 4 | - [Installation](#Installation) 5 | - [Usage](#Usage) 6 | - [Screenshots](#Screenshots) 7 | - [Contribution](#Contribution) 8 | - [License](#License) 9 | 10 | ## Introduction 11 | git-helper is an open-source software that aims to assist developers in using git more conveniently and efficiently. In some ways, it can also serve as a git GUI, allowing for quick management of multiple git repositories and grouping them for easy organization. When a specific repository is selected, the corresponding repository directory and command line can be quickly opened, and features such as branch and tag management, code committing and merging can be done. 12 | 13 | 14 | ## Features 15 | 16 | - Friendly classification of git repositories 17 | - Convenient code committing and merging 18 | - Viewing commit history 19 | - Branch management, including viewing, adding and deleting branches 20 | - Tag management, including viewing, adding and deleting tags 21 | - Switching between preferred color themes 22 | 23 | ## Installation 24 | 25 | - Clone the source code and compile it. 26 | - Download the binary file from the release page. 27 | 28 | Note: Many features have only been tested on mac os, Although there are no destructive git commands, but this project is currently only provided for learning and research, please do not use it formally. 29 | 30 | ## Usage 31 | Currently, there is no clone operation, so you can add your git repositories through the menu first. git-helper will record the repository's path and generate a repository alias, which will be placed in the default category. You can subsequently add categories to classify and sort these repositories. When you select a repository, any actions you perform will be executed in that repository's path. Therefore, when you delete the repository in git-helper, it only removes the path and does not make any destructive changes to your original repository. 32 | 33 | ## Screenshots 34 | 35 | ![changes](screenshots/change.png) 36 | ![history](screenshots/history.png) 37 | ![branch](screenshots/branch.png) 38 | ![tag](screenshots/tag.png) 39 | 40 | ## Contribution 41 | Feel free to open issues or pull requests if you have any suggestions or found any bugs. 42 | 43 | ## License 44 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "git-helper/repository" 6 | "git-helper/utils" 7 | "github.com/wailsapp/wails/v2/pkg/menu" 8 | "github.com/wailsapp/wails/v2/pkg/menu/keys" 9 | "github.com/wailsapp/wails/v2/pkg/runtime" 10 | "os/user" 11 | ) 12 | 13 | // App struct 14 | type App struct { 15 | ctx context.Context 16 | repository *repository.Repository 17 | dataSaveJson string 18 | } 19 | 20 | // NewApp creates a new App application struct 21 | func NewApp() *App { 22 | return &App{} 23 | } 24 | 25 | // startup is called when the app starts. The context is saved 26 | // so we can call the runtime methods 27 | func (a *App) startup(ctx context.Context) { 28 | a.ctx = ctx 29 | u, err := user.Current() 30 | if err != nil { 31 | panic(err) 32 | } 33 | a.dataSaveJson = u.HomeDir + "/Git_Helper.json" 34 | } 35 | func (a *App) setRepository(r *repository.Repository) { 36 | a.repository = r 37 | } 38 | 39 | // Greet returns a greeting for the given name 40 | func (a *App) menu() *menu.Menu { 41 | appMenu := menu.NewMenu() 42 | 43 | appMenu.Append(menu.EditMenu()) 44 | 45 | repo := appMenu.AddSubmenu("Repository") 46 | repo.AddText("Add local repository", keys.CmdOrCtrl("r"), func(_ *menu.CallbackData) { 47 | path, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{ 48 | Title: "Select Local Git Repository", 49 | }) 50 | if err != nil || len(path) == 0 { 51 | return 52 | } 53 | id := utils.Sha256(path) 54 | runtime.EventsEmit(a.ctx, "Repository_A", id, path) 55 | }) 56 | 57 | repo.AddText("Repository manage", keys.CmdOrCtrl("m"), func(_ *menu.CallbackData) { 58 | runtime.EventsEmit(a.ctx, "Repository_M") 59 | }) 60 | 61 | bt := appMenu.AddSubmenu("Branch&Tag") 62 | 63 | bt.AddText("Branch manage", keys.CmdOrCtrl("b"), func(_ *menu.CallbackData) { 64 | runtime.EventsEmit(a.ctx, "Branch_M") 65 | }) 66 | bt.AddText("Tag manage", keys.CmdOrCtrl("t"), func(_ *menu.CallbackData) { 67 | runtime.EventsEmit(a.ctx, "Tag_M") 68 | }) 69 | 70 | other := appMenu.AddSubmenu("Other") 71 | other.AddText("Theme", keys.CmdOrCtrl(""), func(_ *menu.CallbackData) { 72 | runtime.EventsEmit(a.ctx, "Settings_Theme") 73 | }) 74 | other.AddText("About", keys.CmdOrCtrl(""), func(_ *menu.CallbackData) { 75 | runtime.EventsEmit(a.ctx, "Settings_About") 76 | }) 77 | 78 | return appMenu 79 | } 80 | -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | # Build Directory 2 | 3 | The build directory is used to house all the build files and assets for your application. 4 | 5 | The structure is: 6 | 7 | * bin - Output directory 8 | * darwin - macOS specific files 9 | * windows - Windows specific files 10 | 11 | ## Mac 12 | 13 | The `darwin` directory holds files specific to Mac builds. 14 | These may be customised and used as part of the build. To return these files to the default state, simply delete them 15 | and 16 | build with `wails build`. 17 | 18 | The directory contains the following files: 19 | 20 | - `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. 21 | - `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. 22 | 23 | ## Windows 24 | 25 | The `windows` directory contains the manifest and rc files used when building with `wails build`. 26 | These may be customised for your application. To return these files to the default state, simply delete them and 27 | build with `wails build`. 28 | 29 | - `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to 30 | use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file 31 | will be created using the `appicon.png` file in the build directory. 32 | - `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. 33 | - `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, 34 | as well as the application itself (right click the exe -> properties -> details) 35 | - `wails.exe.manifest` - The main application manifest file. -------------------------------------------------------------------------------- /build/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xusenlin/git-helper/19e88fa5f00ec16a87de17963883de7ddfc176dc/build/appicon.png -------------------------------------------------------------------------------- /build/darwin/Info.dev.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundlePackageType 5 | APPL 6 | CFBundleName 7 | {{.Info.ProductName}} 8 | CFBundleExecutable 9 | {{.Name}} 10 | CFBundleIdentifier 11 | com.wails.{{.Name}} 12 | CFBundleVersion 13 | {{.Info.ProductVersion}} 14 | CFBundleGetInfoString 15 | {{.Info.Comments}} 16 | CFBundleShortVersionString 17 | {{.Info.ProductVersion}} 18 | CFBundleIconFile 19 | iconfile 20 | LSMinimumSystemVersion 21 | 10.13.0 22 | NSHighResolutionCapable 23 | true 24 | NSHumanReadableCopyright 25 | {{.Info.Copyright}} 26 | NSAppTransportSecurity 27 | 28 | NSAllowsLocalNetworking 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /build/darwin/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundlePackageType 5 | APPL 6 | CFBundleName 7 | {{.Info.ProductName}} 8 | CFBundleExecutable 9 | {{.Name}} 10 | CFBundleIdentifier 11 | com.wails.{{.Name}} 12 | CFBundleVersion 13 | {{.Info.ProductVersion}} 14 | CFBundleGetInfoString 15 | {{.Info.Comments}} 16 | CFBundleShortVersionString 17 | {{.Info.ProductVersion}} 18 | CFBundleIconFile 19 | iconfile 20 | LSMinimumSystemVersion 21 | 10.13.0 22 | NSHighResolutionCapable 23 | true 24 | NSHumanReadableCopyright 25 | {{.Info.Copyright}} 26 | 27 | -------------------------------------------------------------------------------- /build/windows/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xusenlin/git-helper/19e88fa5f00ec16a87de17963883de7ddfc176dc/build/windows/icon.ico -------------------------------------------------------------------------------- /build/windows/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "fixed": { 3 | "file_version": "{{.Info.ProductVersion}}" 4 | }, 5 | "info": { 6 | "0000": { 7 | "ProductVersion": "{{.Info.ProductVersion}}", 8 | "CompanyName": "{{.Info.CompanyName}}", 9 | "FileDescription": "{{.Info.ProductName}}", 10 | "LegalCopyright": "{{.Info.Copyright}}", 11 | "ProductName": "{{.Info.ProductName}}", 12 | "Comments": "{{.Info.Comments}}" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /build/windows/installer/project.nsi: -------------------------------------------------------------------------------- 1 | Unicode true 2 | 3 | #### 4 | ## Please note: Template replacements don't work in this file. They are provided with default defines like 5 | ## mentioned underneath. 6 | ## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. 7 | ## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually 8 | ## from outside of Wails for debugging and development of the installer. 9 | ## 10 | ## For development first make a wails nsis build to populate the "wails_tools.nsh": 11 | ## > wails build --target windows/amd64 --nsis 12 | ## Then you can call makensis on this file with specifying the path to your binary: 13 | ## For a AMD64 only installer: 14 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe 15 | ## For a ARM64 only installer: 16 | ## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe 17 | ## For a installer with both architectures: 18 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe 19 | #### 20 | ## The following information is taken from the ProjectInfo file, but they can be overwritten here. 21 | #### 22 | ## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" 23 | ## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" 24 | ## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" 25 | ## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" 26 | ## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" 27 | ### 28 | ## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" 29 | ## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" 30 | #### 31 | ## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html 32 | #### 33 | ## Include the wails tools 34 | #### 35 | !include "wails_tools.nsh" 36 | 37 | # The version information for this two must consist of 4 parts 38 | VIProductVersion "${INFO_PRODUCTVERSION}.0" 39 | VIFileVersion "${INFO_PRODUCTVERSION}.0" 40 | 41 | VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" 42 | VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" 43 | VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" 44 | VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" 45 | VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" 46 | VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" 47 | 48 | !include "MUI.nsh" 49 | 50 | !define MUI_ICON "..\icon.ico" 51 | !define MUI_UNICON "..\icon.ico" 52 | # !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 53 | !define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps 54 | !define MUI_ABORTWARNING # This will warn the user if they exit from the installer. 55 | 56 | !insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. 57 | # !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer 58 | !insertmacro MUI_PAGE_DIRECTORY # In which folder install page. 59 | !insertmacro MUI_PAGE_INSTFILES # Installing page. 60 | !insertmacro MUI_PAGE_FINISH # Finished installation page. 61 | 62 | !insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page 63 | 64 | !insertmacro MUI_LANGUAGE "English" # Set the Language of the installer 65 | 66 | ## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 67 | #!uninstfinalize 'signtool --file "%1"' 68 | #!finalize 'signtool --file "%1"' 69 | 70 | Name "${INFO_PRODUCTNAME}" 71 | OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. 72 | InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). 73 | ShowInstDetails show # This will always show the installation details. 74 | 75 | Function .onInit 76 | !insertmacro wails.checkArchitecture 77 | FunctionEnd 78 | 79 | Section 80 | !insertmacro wails.webview2runtime 81 | 82 | SetOutPath $INSTDIR 83 | 84 | !insertmacro wails.files 85 | 86 | CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" 87 | CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" 88 | 89 | !insertmacro wails.writeUninstaller 90 | SectionEnd 91 | 92 | Section "uninstall" 93 | RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath 94 | 95 | RMDir /r $INSTDIR 96 | 97 | Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" 98 | Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" 99 | 100 | !insertmacro wails.deleteUninstaller 101 | SectionEnd 102 | -------------------------------------------------------------------------------- /build/windows/installer/wails_tools.nsh: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - Generated automatically by `wails build` 2 | 3 | !include "x64.nsh" 4 | !include "WinVer.nsh" 5 | !include "FileFunc.nsh" 6 | 7 | !ifndef INFO_PROJECTNAME 8 | !define INFO_PROJECTNAME "{{.Name}}" 9 | !endif 10 | !ifndef INFO_COMPANYNAME 11 | !define INFO_COMPANYNAME "{{.Info.CompanyName}}" 12 | !endif 13 | !ifndef INFO_PRODUCTNAME 14 | !define INFO_PRODUCTNAME "{{.Info.ProductName}}" 15 | !endif 16 | !ifndef INFO_PRODUCTVERSION 17 | !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" 18 | !endif 19 | !ifndef INFO_COPYRIGHT 20 | !define INFO_COPYRIGHT "{{.Info.Copyright}}" 21 | !endif 22 | !ifndef PRODUCT_EXECUTABLE 23 | !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" 24 | !endif 25 | !ifndef UNINST_KEY_NAME 26 | !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" 27 | !endif 28 | !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" 29 | 30 | !ifndef REQUEST_EXECUTION_LEVEL 31 | !define REQUEST_EXECUTION_LEVEL "admin" 32 | !endif 33 | 34 | RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" 35 | 36 | !ifdef ARG_WAILS_AMD64_BINARY 37 | !define SUPPORTS_AMD64 38 | !endif 39 | 40 | !ifdef ARG_WAILS_ARM64_BINARY 41 | !define SUPPORTS_ARM64 42 | !endif 43 | 44 | !ifdef SUPPORTS_AMD64 45 | !ifdef SUPPORTS_ARM64 46 | !define ARCH "amd64_arm64" 47 | !else 48 | !define ARCH "amd64" 49 | !endif 50 | !else 51 | !ifdef SUPPORTS_ARM64 52 | !define ARCH "arm64" 53 | !else 54 | !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" 55 | !endif 56 | !endif 57 | 58 | !macro wails.checkArchitecture 59 | !ifndef WAILS_WIN10_REQUIRED 60 | !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." 61 | !endif 62 | 63 | !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED 64 | !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" 65 | !endif 66 | 67 | ${If} ${AtLeastWin10} 68 | !ifdef SUPPORTS_AMD64 69 | ${if} ${IsNativeAMD64} 70 | Goto ok 71 | ${EndIf} 72 | !endif 73 | 74 | !ifdef SUPPORTS_ARM64 75 | ${if} ${IsNativeARM64} 76 | Goto ok 77 | ${EndIf} 78 | !endif 79 | 80 | IfSilent silentArch notSilentArch 81 | silentArch: 82 | SetErrorLevel 65 83 | Abort 84 | notSilentArch: 85 | MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" 86 | Quit 87 | ${else} 88 | IfSilent silentWin notSilentWin 89 | silentWin: 90 | SetErrorLevel 64 91 | Abort 92 | notSilentWin: 93 | MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" 94 | Quit 95 | ${EndIf} 96 | 97 | ok: 98 | !macroend 99 | 100 | !macro wails.files 101 | !ifdef SUPPORTS_AMD64 102 | ${if} ${IsNativeAMD64} 103 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" 104 | ${EndIf} 105 | !endif 106 | 107 | !ifdef SUPPORTS_ARM64 108 | ${if} ${IsNativeARM64} 109 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" 110 | ${EndIf} 111 | !endif 112 | !macroend 113 | 114 | !macro wails.writeUninstaller 115 | WriteUninstaller "$INSTDIR\uninstall.exe" 116 | 117 | SetRegView 64 118 | WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" 119 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" 120 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" 121 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" 122 | WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" 123 | WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" 124 | 125 | ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 126 | IntFmt $0 "0x%08X" $0 127 | WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" 128 | !macroend 129 | 130 | !macro wails.deleteUninstaller 131 | Delete "$INSTDIR\uninstall.exe" 132 | 133 | SetRegView 64 134 | DeleteRegKey HKLM "${UNINST_KEY}" 135 | !macroend 136 | 137 | # Install webview2 by launching the bootstrapper 138 | # See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment 139 | !macro wails.webview2runtime 140 | !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT 141 | !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" 142 | !endif 143 | 144 | SetRegView 64 145 | # If the admin key exists and is not empty then webview2 is already installed 146 | ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" 147 | ${If} $0 != "" 148 | Goto ok 149 | ${EndIf} 150 | 151 | ${If} ${REQUEST_EXECUTION_LEVEL} == "user" 152 | # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed 153 | ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" 154 | ${If} $0 != "" 155 | Goto ok 156 | ${EndIf} 157 | ${EndIf} 158 | 159 | SetDetailsPrint both 160 | DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" 161 | SetDetailsPrint listonly 162 | 163 | InitPluginsDir 164 | CreateDirectory "$pluginsdir\webview2bootstrapper" 165 | SetOutPath "$pluginsdir\webview2bootstrapper" 166 | File "tmp\MicrosoftEdgeWebview2Setup.exe" 167 | ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' 168 | 169 | SetDetailsPrint both 170 | ok: 171 | !macroend -------------------------------------------------------------------------------- /build/windows/wails.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true/pm 12 | permonitorv2,permonitor 13 | 14 | 15 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "git-helper/utils" 6 | "github.com/atotto/clipboard" 7 | "github.com/wailsapp/wails/v2/pkg/runtime" 8 | "io/ioutil" 9 | ) 10 | 11 | func (a *App) Sha256(s string) string { 12 | return utils.Sha256(s) 13 | } 14 | func (a *App) MessageDialog(title string, message string) { 15 | _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ 16 | Type: runtime.InfoDialog, 17 | Title: title, 18 | Message: message, 19 | }) 20 | } 21 | func (a *App) SaveJsonFile(t string) error { 22 | 23 | if err := utils.RemoveFile(a.dataSaveJson); err != nil { 24 | return err 25 | } 26 | err := ioutil.WriteFile(a.dataSaveJson, []byte(t), 0666) 27 | 28 | if err != nil { 29 | return err 30 | } 31 | return nil 32 | } 33 | func (a *App) ReadJsonFile() (string, error) { 34 | 35 | b, err := ioutil.ReadFile(a.dataSaveJson) 36 | if err != nil { 37 | return "", err 38 | } 39 | return string(b), nil 40 | } 41 | func (a *App) Clipboard(t string) error { 42 | return clipboard.WriteAll(t) 43 | } 44 | func (a *App) IsGitRepository(path string) (bool, error) { 45 | if !utils.IsDir(path + "/.git") { 46 | return false, errors.New("not a git repository") 47 | } 48 | return true, nil 49 | } 50 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GitUI 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^4.8.0", 13 | "@reduxjs/toolkit": "^1.9.0", 14 | "@types/react-beautiful-dnd": "^13.1.2", 15 | "antd": "^5.0.1", 16 | "dayjs": "^1.11.7", 17 | "react": "^18.2.0", 18 | "react-beautiful-dnd": "^13.1.1", 19 | "react-dom": "^18.2.0", 20 | "react-redux": "^8.0.5", 21 | "sass": "^1.56.1" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.0.17", 25 | "@types/react-dom": "^18.0.6", 26 | "@vitejs/plugin-react": "^2.0.1", 27 | "typescript": "^4.6.4", 28 | "vite": "^3.0.7" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/package.json.md5: -------------------------------------------------------------------------------- 1 | 657141f18b8a91280bdcc9a27bb1581a -------------------------------------------------------------------------------- /frontend/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xusenlin/git-helper/19e88fa5f00ec16a87de17963883de7ddfc176dc/frontend/src/.DS_Store -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | height: 100vh; 3 | width: 100%; 4 | display: flex; 5 | } 6 | 7 | .sides { 8 | width: 24%; 9 | min-width: 240px; 10 | overflow: hidden; 11 | background-color: #dfe3e3; 12 | height: 100%; 13 | } 14 | 15 | .sides-top { 16 | width: 100%; 17 | height: 60px; 18 | display: flex; 19 | align-items: center; 20 | padding: 0 20px; 21 | justify-content: flex-end; 22 | --wails-draggable: drag; 23 | } 24 | .sides-content{ 25 | overflow-y: auto; 26 | height: calc(100vh - 60px); 27 | } 28 | 29 | .theme-icon { 30 | width: 15px; 31 | position: relative; 32 | top: 1px; 33 | cursor: pointer; 34 | opacity: 0.45 35 | } 36 | 37 | .container { 38 | flex: 1; 39 | height: 100%; 40 | position: relative; 41 | background-color: #fff; 42 | } 43 | 44 | .main { 45 | display: flex; 46 | width: 100%; 47 | position: relative; 48 | height: calc(100vh - 60px); 49 | justify-content: center; 50 | align-items: center 51 | } 52 | 53 | .main .ant-tabs-nav { 54 | margin: 0; 55 | } 56 | 57 | .left { 58 | width: 300px; 59 | min-width: 300px; 60 | max-width: 360px; 61 | height: 100%; 62 | border-right: 1px solid rgba(5, 5, 5, 0.06); 63 | } 64 | 65 | .right { 66 | flex: 1; 67 | height: 100%; 68 | overflow-y: auto; 69 | } 70 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import {State} from "./store"; 3 | import {Empty, ConfigProvider, Tabs} from "antd" 4 | import Sides from "./components/sides/Sides" 5 | import SidesTop from "./components/sides/SidesTop" 6 | import Changes from "./components/changes/Index" 7 | import History from "./components/history/Index" 8 | import Tag from "./components/tag/Index" 9 | import Theme from "./components/dialog/Theme" 10 | import About from "./components/dialog/About" 11 | import Branch from "./components/branch/Index" 12 | import TopBar from "./components/topBar/TopBar" 13 | import DiffWorkView from "./components/diff/DiffWorkView" 14 | import DiffCommitView from "./components/diff/DiffCommitView" 15 | import {useSelector} from "react-redux"; 16 | import {useState} from "react"; 17 | 18 | 19 | function App() { 20 | const main = useSelector((state: State) => state.main); 21 | const themeColor = useSelector((state: State) => state.setting.themeColor); 22 | const [activeTab,setActiveTab] = useState("1") 23 | 24 | 25 | const content = <> 26 |
27 | {setActiveTab(k)}} 32 | items={[ 33 | { 34 | label: `Changes`, 35 | key: "1", 36 | children: , 37 | }, 38 | { 39 | label: `History`, 40 | key: "2", 41 | children: , 42 | } 43 | ]} 44 | /> 45 |
46 |
47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 | 56 | const empty =
57 | 58 |
59 | 60 | return ( 61 | 64 |
65 |
66 | 67 | 68 |
69 |
70 | 71 |
72 | {main.selectedRepositoryId && main.selectedRepositoryBranch ? content : empty} 73 |
74 |
75 | <> 76 | 77 | 78 | 79 | 80 | 81 |
82 |
83 | ) 84 | } 85 | 86 | export default App 87 | -------------------------------------------------------------------------------- /frontend/src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xusenlin/git-helper/19e88fa5f00ec16a87de17963883de7ddfc176dc/frontend/src/assets/.DS_Store -------------------------------------------------------------------------------- /frontend/src/assets/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/nunito-v16-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xusenlin/git-helper/19e88fa5f00ec16a87de17963883de7ddfc176dc/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 -------------------------------------------------------------------------------- /frontend/src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xusenlin/git-helper/19e88fa5f00ec16a87de17963883de7ddfc176dc/frontend/src/components/.DS_Store -------------------------------------------------------------------------------- /frontend/src/components/block/Index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import {ReactNode} from "react" 3 | 4 | type propType = { 5 | title?: string|ReactNode 6 | children: ReactNode 7 | bottom?: ReactNode 8 | action?:ReactNode 9 | } 10 | 11 | const Block = (props: propType) => { 12 | return ( 13 |
14 | { props.action || props.title ?
15 |
{props.title}
16 |
{props.action}
17 |
:'' } 18 | 19 |
20 |
21 | {props.children} 22 |
23 |
24 | {props.bottom} 25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default Block; 32 | -------------------------------------------------------------------------------- /frontend/src/components/block/index.scss: -------------------------------------------------------------------------------- 1 | .main-block{ 2 | //flex: 1; 3 | height: calc(100vh - 106px); 4 | //border: 1px solid #dfe3e3; 5 | //margin: 6px; 6 | //border-radius: 8px; 7 | overflow: hidden; 8 | .top-title{ 9 | display: flex; 10 | align-items: center; 11 | width: 100%; 12 | padding: 0 12px; 13 | font-size: 14px; 14 | justify-content: space-between; 15 | height: 40px; 16 | background: #f2f6f6; 17 | } 18 | .main-block-content{ 19 | display: flex; 20 | flex-direction: column; 21 | .top{ 22 | flex: 1; 23 | overflow-x: hidden; 24 | overflow-y: auto; 25 | } 26 | .bottom{ 27 | 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/branch/Index.tsx: -------------------------------------------------------------------------------- 1 | import "./style.scss" 2 | import Item from "./Item" 3 | import {State} from "../../store"; 4 | import React, {useState, useRef, useMemo, useEffect} from "react"; 5 | import {useDispatch, useSelector} from "react-redux"; 6 | import {Button, Input, Space, Drawer, Empty} from "antd"; 7 | import {AddBranch, GetAllBranch, GetLocalBranch} from "../../../wailsjs/go/repository/Repository" 8 | import {repository} from "../../../wailsjs/go/models" 9 | import {setOpenRepositoryBranch} from "../../store/sliceSetting"; 10 | import {warning} from "../../utils/common" 11 | import MergeDialog from "./mergeDialog" 12 | import {setAllBranch} from "../../store/sliceMain"; 13 | 14 | 15 | const Branch = () => { 16 | const limit = 20 17 | 18 | const dispatch = useDispatch(); 19 | // const [branch,setBranch] = useState([]); 20 | const branch = useSelector((state: State) => state.main.currentlyRepositoryLocalBranch); 21 | const selectedRepositoryId = useSelector((state: State) => state.main.selectedRepositoryId); 22 | const showRepositoryBranch = useSelector((state: State) => state.setting.showRepositoryBranch); 23 | const mergeDialogComponentRef = useRef<{ OpenMergeDialog: (branchName: string) => void }>(null); 24 | const [branchName, setBranchName] = useState("") 25 | 26 | const [keyword,setKeyword] = useState("") 27 | const computedBranch = useMemo(() => (branch.filter(r=>r.name.indexOf(keyword)!==-1)), [keyword,branch]); 28 | 29 | // const getAllBranch = () => { 30 | // if(!selectedRepositoryId){ 31 | // return 32 | // } 33 | // GetAllBranch().then(b=>{ 34 | // setBranch(b||[]) 35 | // }).catch(e=>{ 36 | // warning(JSON.stringify(e)) 37 | // }) 38 | // } 39 | // useEffect(() => { 40 | // getAllBranch() 41 | // }, [selectedRepositoryId]); 42 | 43 | 44 | const addBranch = async () => { 45 | try { 46 | await AddBranch(branchName) 47 | const b = await GetLocalBranch() 48 | dispatch(setAllBranch(b)) 49 | setBranchName("") 50 | } catch (e) { 51 | console.log(e) 52 | warning("Branch:" + JSON.stringify(e)) 53 | } 54 | } 55 | 56 | const onCloseBranch = () => { 57 | dispatch(setOpenRepositoryBranch(false)) 58 | } 59 | 60 | 61 | const bottom =
62 | 63 | ) => { 64 | setBranchName(e.target.value) 65 | }}/> 66 | 69 | 70 |
71 | 72 | const content = <> 73 |
74 | ) => { 75 | setKeyword(e.target.value) 76 | }} /> 77 | {(computedBranch.length > limit ? computedBranch.slice(0, limit) : computedBranch).map(r => { 78 | mergeDialogComponentRef.current?.OpenMergeDialog(r.name) 79 | }} b={r} key={r.name}/>)} 80 |
81 |
82 | {bottom} 83 |
84 | 85 | 86 | return ( 87 | Total:{branch.length}} 90 | bodyStyle={{display: 'flex', flexDirection: "column", padding: 0}} 91 | placement="right" 92 | onClose={onCloseBranch} 93 | destroyOnClose={true} 94 | open={showRepositoryBranch} 95 | > 96 | 97 | {selectedRepositoryId ? content : 98 | } 99 | 100 | ); 101 | }; 102 | 103 | export default Branch; 104 | -------------------------------------------------------------------------------- /frontend/src/components/branch/Item.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Modal, Space, Checkbox, Badge, message} from "antd" 2 | import dayjs from "dayjs" 3 | import relativeTime from 'dayjs/plugin/relativeTime'; 4 | import {warning, success, clipboard} from "../../utils/common"; 5 | import {repository} from "../../../wailsjs/go/models" 6 | import {DelBranch, GetLocalBranch, PushBranch} from "../../../wailsjs/go/repository/Repository" 7 | import {DeleteOutlined, SnippetsOutlined, ArrowLeftOutlined, ArrowUpOutlined} from "@ant-design/icons" 8 | import {setAllBranch} from "../../store/sliceMain"; 9 | import {useDispatch, useSelector} from "react-redux"; 10 | import {mainBranch} from "../../config/app"; 11 | import {State} from "../../store"; 12 | 13 | dayjs.extend(relativeTime) 14 | 15 | 16 | const Item = (props: { b: repository.Branch,merge:()=>void }) => { 17 | const sledBranch = useSelector((state: State) => state.main.selectedRepositoryBranch); 18 | const [messageApi, contextHolder] = message.useMessage(); 19 | 20 | const dispatch = useDispatch(); 21 | 22 | const delBranch = (name: string) => { 23 | Modal.warning({ 24 | closable: true, 25 | title: 'Confirm message', 26 | content:
27 |

Are you sure you want to delete this branch?

28 | Delete the remote branch at the same time. 29 |
, 30 | onOk() { 31 | const checkbox = document.getElementById("delRemoteBranchCheckbox") as HTMLInputElement 32 | DelBranch(name,checkbox.checked).then(out => { 33 | success(out); 34 | return GetLocalBranch() 35 | }).then(b => { 36 | dispatch(setAllBranch(b)) 37 | }).catch(e => { 38 | warning(JSON.stringify(e)) 39 | }) 40 | } 41 | }); 42 | } 43 | 44 | const merge = async () => { 45 | if(!sledBranch){ 46 | warning("please select a branch first") 47 | return 48 | } 49 | props.merge() 50 | } 51 | 52 | const pushBranch = (name:string) => { 53 | messageApi.open({ 54 | type: 'loading', 55 | content: 'Pushing...', 56 | duration: 0, 57 | }); 58 | PushBranch(name).then(out=>{ 59 | success(out) 60 | return GetLocalBranch() 61 | }).then(b => { 62 | dispatch(setAllBranch(b)) 63 | }).catch(e=>{ 64 | warning(e) 65 | }).finally(messageApi.destroy) 66 | } 67 | 68 | 69 | const extra = 70 | { !props.b.upstream && { 71 | pushBranch(props.b.name) 72 | }} style={{cursor: "pointer", opacity: 0.45}}/> } 73 | { 74 | (sledBranch != props.b.name ) && 75 | } 76 | { 77 | (mainBranch != props.b.name) && (sledBranch != props.b.name ) && 78 | { 79 | delBranch(props.b.name) 80 | }} style={{cursor: "pointer", opacity: 0.45}}/> 81 | } 82 | 83 | 84 | return ( 85 | } 88 | extra={extra} style={{marginBottom: 10}}> 89 |
90 |

{props.b.refName}

91 |
92 |
Hash:{props.b.hash.substring(0, 7)}
93 | { 94 | await clipboard(props.b.hash) 95 | }} style={{cursor: "pointer", opacity: 0.65}}/> 96 |
97 |
98 | {contextHolder} 99 |
100 | ); 101 | }; 102 | 103 | export default Item; 104 | -------------------------------------------------------------------------------- /frontend/src/components/branch/mergeDialog.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Modal, Result} from 'antd'; 2 | import { LoadingOutlined } from "@ant-design/icons" 3 | import {useDispatch, useSelector} from "react-redux"; 4 | import {State} from "../../store"; 5 | import {useState, useImperativeHandle, forwardRef} from "react"; 6 | import {success, warning} from "../../utils/common"; 7 | import { PreMergeResult, MergeCommit, MergeRebase, MergeSquash} from "../../../wailsjs/go/repository/Repository"; 8 | import {GetBranchHash,Commits} from "../../../wailsjs/go/repository/Repository"; 9 | import {repository} from "../../../wailsjs/go/models"; 10 | import {warningNotification} from "../../utils/notification"; 11 | import {setLog} from "../../store/sliceMain"; 12 | 13 | 14 | const MergeDialog = (props: {}, ref: any) => { 15 | const dispatch = useDispatch(); 16 | const currentBranchName = useSelector((state: State) => state.main.selectedRepositoryBranch); 17 | const [openMerge, setOpenMerge] = useState(false); 18 | const [loading, setLoading] = useState(false); 19 | 20 | const [preMergeResult, setPreMergeResult] = useState(null); 21 | 22 | const [mergeContent, setMergeContent] = useState({ 23 | ourBranchHash: "", 24 | ourBranchName: "", 25 | theirBranchHash: "", 26 | theirBranchName: "", 27 | }); 28 | 29 | const OpenMergeDialog = async (mergeName: string) => { 30 | setOpenMerge(true) 31 | setLoading(true) 32 | try { 33 | let ourBranchHash = await GetBranchHash(currentBranchName) 34 | let theirBranchHash = await GetBranchHash(mergeName) 35 | setMergeContent({ 36 | ourBranchHash, 37 | ourBranchName: currentBranchName, 38 | theirBranchHash, 39 | theirBranchName: mergeName, 40 | }) 41 | 42 | const result = await PreMergeResult(ourBranchHash, theirBranchHash) 43 | setPreMergeResult(result) 44 | 45 | } catch (e) { 46 | console.log(e) 47 | warning(JSON.stringify(e)) 48 | }finally { 49 | setTimeout(()=>{setLoading(false)},500) 50 | } 51 | 52 | } 53 | 54 | 55 | useImperativeHandle(ref, () => ({OpenMergeDialog})) 56 | 57 | 58 | const subTitle = () => { 59 | if(preMergeResult?.kind === 1){ 60 | return
There will be {preMergeResult.count} conflicted file when merging {mergeContent.theirBranchName} into {mergeContent.ourBranchName}
61 | } 62 | if(preMergeResult?.kind === 2){ 63 | if(preMergeResult?.count === 0){ 64 | return
This branch is up to date with {mergeContent.theirBranchName}
65 | }else { 66 | return
67 | This will merge {preMergeResult?.count} commit{preMergeResult?.count >=2 ?'s':''} from {mergeContent.theirBranchName} into {mergeContent.ourBranchName}
68 | } 69 | } 70 | return 71 | } 72 | 73 | 74 | const rebaseMerge = () => { 75 | MergeRebase(mergeContent.ourBranchName,mergeContent.theirBranchName).then(out=>{ 76 | success(out) 77 | return Commits(currentBranchName) 78 | }).then(l=>{ 79 | dispatch(setLog(l)) 80 | setOpenMerge(false) 81 | }).catch(e=>{ 82 | warningNotification(e,{width:500}) 83 | }) 84 | 85 | } 86 | const squashMerge = () => { 87 | MergeSquash(mergeContent.ourBranchName,mergeContent.theirBranchName).then(out=>{ 88 | success(out) 89 | return Commits(currentBranchName) 90 | }).then(l=>{ 91 | dispatch(setLog(l)) 92 | setOpenMerge(false) 93 | }).catch(e=>{ 94 | warningNotification(e,{width:500}) 95 | }) 96 | } 97 | const commitMerge = () => { 98 | MergeCommit(mergeContent.ourBranchName,mergeContent.theirBranchName).then(out=>{ 99 | success(out) 100 | return Commits(currentBranchName) 101 | }).then(l=>{ 102 | dispatch(setLog(l)) 103 | setOpenMerge(false) 104 | }).catch(e=>{ 105 | warningNotification(e,{width:500}) 106 | }) 107 | } 108 | 109 | return { 114 | setOpenMerge(false) 115 | setPreMergeResult(null) 116 | }} 117 | footer={[ 118 | , 121 | , 124 | , 127 | ]} 128 | > 129 | :''} 132 | subTitle={subTitle()}/> 133 | 134 | } 135 | 136 | export default forwardRef(MergeDialog) 137 | 138 | 139 | -------------------------------------------------------------------------------- /frontend/src/components/branch/style.scss: -------------------------------------------------------------------------------- 1 | .branch-content { 2 | p { 3 | margin-bottom: 10px; 4 | color: #6a6a6a; 5 | } 6 | 7 | .item { 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | color: #6a6a6a; 12 | margin: 10px 0; 13 | } 14 | .title{ 15 | 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /frontend/src/components/changes/FileState.tsx: -------------------------------------------------------------------------------- 1 | import {repository} from "../../../wailsjs/go/models" 2 | import React from "react"; 3 | import {Checkbox, Dropdown, MenuProps,Modal} from 'antd'; 4 | import { WarningOutlined } from "@ant-design/icons" 5 | import {diffWorkTreeStageAsync} from "../../store/sliceWorkDiff" 6 | import {useDispatch, useSelector} from "react-redux"; 7 | import {State} from "../../store"; 8 | import { DiscardChanges } from "../../../wailsjs/go/repository/Repository" 9 | import {success, warning} from "../../utils/common" 10 | import {updateWorkZone} from "../../utils/repo"; 11 | 12 | const color = (s: string) => { 13 | // Unmodified StatusCode = ' ' 14 | // Untracked StatusCode = '?' 15 | // Modified StatusCode = 'M' 16 | // Added StatusCode = 'A' 17 | // Deleted StatusCode = 'D' 18 | // Renamed StatusCode = 'R' 19 | // Copied StatusCode = 'C' 20 | // UpdatedButUnmerged StatusCode = 'U' 21 | switch (s) { 22 | case "M": 23 | return {background: "#d48806"} 24 | case "A": 25 | return {background: "#7cb305"} 26 | case "D": 27 | return {background: "#d4380d"} 28 | case "R": 29 | return {background: "#0958d9"} 30 | case "C": 31 | return {background: "#531dab"} 32 | case "U": 33 | return {background: "#c41d7f"} 34 | default: 35 | return {background: "#8c8c8c"} 36 | } 37 | 38 | } 39 | 40 | 41 | const FileState = (props: { s: repository.FileStatus }) => { 42 | const path = useSelector((state: State) => state.diffWork.fileStatus?.path); 43 | const repoId = useSelector((state: State) => state.main.selectedRepositoryId); 44 | const branchName = useSelector((state: State) => state.main.selectedRepositoryBranch); 45 | const dispatch = useDispatch(); 46 | 47 | const contextMenu = (path:string):MenuProps['items'] => { 48 | 49 | const confirmDiscardChanges = () => { 50 | Modal.confirm({ 51 | title: 'Confirm discard changes.', 52 | icon: , 53 | content:
54 |

This file will be restored to its previous state.

55 |

file:{path}

56 |
, 57 | onOk() { 58 | return new Promise((resolve, reject) => { 59 | DiscardChanges(path).then(()=>{ 60 | resolve("success") 61 | return updateWorkZone(repoId,branchName) 62 | }).catch((e)=>{ 63 | reject(e) 64 | }) 65 | }).catch((e) => warning(e)); 66 | }, 67 | onCancel() {}, 68 | }); 69 | }; 70 | 71 | const discardChanges = { 72 | label:DiscardChanges, 73 | key:'discard-changes', 74 | icon: 75 | } 76 | return [discardChanges] 77 | } 78 | 79 | return
80 | 81 | 82 | {dispatch(diffWorkTreeStageAsync(props.s))}} 86 | >{props.s.name} 87 | 88 |
89 | {props.s.staging} 90 | {props.s.worktree} 91 |
92 | 93 |
94 | } 95 | 96 | export default FileState 97 | -------------------------------------------------------------------------------- /frontend/src/components/changes/Index.tsx: -------------------------------------------------------------------------------- 1 | import "./style.scss" 2 | import {State} from "../../store"; 3 | import Block from "../block/Index" 4 | import FileState from "./FileState" 5 | import React, {useState,useMemo,useEffect} from 'react'; 6 | import {useDispatch, useSelector} from "react-redux"; 7 | import { Space, Checkbox,Badge} from 'antd'; 8 | import { PresetStatusColorType } from "antd/es/_util/colors" 9 | import {FileStatus} from "../../../wailsjs/go/repository/Repository" 10 | import type {CheckboxChangeEvent} from 'antd/es/checkbox'; 11 | import type {CheckboxValueType} from 'antd/es/checkbox/Group'; 12 | import {success, warning} from "../../utils/common"; 13 | import {setRepositoryStatus, updateWorkZone} from "../../utils/repo"; 14 | import {setStatus} from "../../store/sliceMain"; 15 | import SubmitView from "./SubmitView"; 16 | 17 | 18 | 19 | const Changes = () => { 20 | const dispatch = useDispatch(); 21 | const branch = useSelector((state: State) => state.main.selectedRepositoryBranch); 22 | const selectedRepositoryId = useSelector((state: State) => state.main.selectedRepositoryId); 23 | const [badgeStatus,setBadgeStatus] = useState("success") 24 | 25 | const fileState = useSelector((state: State) => state.main.currentlyRepositoryFileState); 26 | 27 | const changedFileInWorkspace = useMemo( ()=>fileState.filter(r=>(r.worktree != " ")).map(r => r.path),[fileState]) 28 | const unchangedFileInWorkspace = useMemo( ()=>fileState.filter(r=>(r.worktree == " ")).map(r => r.path),[fileState]) 29 | 30 | const [checkedList, setCheckedList] = useState(unchangedFileInWorkspace); 31 | const [indeterminate, setIndeterminate] = useState(true); 32 | const [checkAll, setCheckAll] = useState(false); 33 | 34 | useEffect(()=>{ 35 | setCheckedList(unchangedFileInWorkspace) 36 | },[fileState]) 37 | 38 | useEffect(()=>{ 39 | if(fileState.length===0){ 40 | setCheckAll(false); 41 | setIndeterminate(false); 42 | return 43 | } 44 | setCheckAll(checkedList.length === fileState.length); 45 | setIndeterminate(checkedList.length !==0 && checkedList.length < fileState.length); 46 | },[checkedList]) 47 | 48 | const commitSuccess = async () => { 49 | try { 50 | setCheckedList([]) 51 | await updateWorkZone(selectedRepositoryId,branch) 52 | }catch (e) { 53 | console.log(e) 54 | warning(JSON.stringify(e)) 55 | } 56 | } 57 | 58 | const onChange = (list: CheckboxValueType[]) => { 59 | setCheckedList(list as string[]); 60 | }; 61 | 62 | const onCheckAllChange = (e: CheckboxChangeEvent) => { 63 | let checkedList = e.target.checked ? [...changedFileInWorkspace,...unchangedFileInWorkspace] : unchangedFileInWorkspace 64 | setCheckedList(checkedList); 65 | }; 66 | 67 | const getChangesStatus = () => { 68 | setBadgeStatus('processing') 69 | FileStatus().then((s)=>{ 70 | dispatch(setStatus(s)) 71 | setRepositoryStatus(selectedRepositoryId,Array.isArray(s)&&s.length !==0) 72 | }).catch(e=>{ 73 | console.log(e) 74 | warning( JSON.stringify(e)) 75 | }).finally(()=>{ 76 | setTimeout(()=>{ 77 | setBadgeStatus('success') 78 | },1000) 79 | }) 80 | } 81 | 82 | const action = 83 |
{checkedList.length}
84 | 85 | Check all 86 | 87 |
88 | 89 | 90 | return ( 91 | 94 | 95 | 96 | } 97 | bottom={} 98 | action={action}> 99 | 103 | {fileState.map(s => )} 104 | 105 | 106 | ); 107 | }; 108 | 109 | export default Changes; 110 | -------------------------------------------------------------------------------- /frontend/src/components/changes/SubmitView.tsx: -------------------------------------------------------------------------------- 1 | import {State} from "../../store"; 2 | import React, {useState} from "react"; 3 | import {Button, Input,Space} from 'antd'; 4 | import { useSelector} from "react-redux"; 5 | import {success, warning} from "../../utils/common" 6 | import {Commit} from "../../../wailsjs/go/repository/Repository" 7 | 8 | const {TextArea} = Input; 9 | 10 | const SubmitView = (props: { checkedList :string[],unchangedFileInWorkspace:string[],success:()=>void }) => { 11 | const [commitName,setCommitName] = useState("") 12 | const [commitMessage,setCommitMessage] = useState("") 13 | const branch = useSelector((state: State) => state.main.selectedRepositoryBranch); 14 | 15 | const commit = async () => { 16 | if(commitName.length === 0){ 17 | warning("Please fill in the title for the submission.") 18 | return 19 | } 20 | if(props.checkedList.length === 0 && props.unchangedFileInWorkspace.length==0){ 21 | warning("Please check the box for the file you want to submit.") 22 | return 23 | } 24 | let submitList = props.checkedList.filter((item) => !props.unchangedFileInWorkspace.includes(item)); 25 | 26 | try { 27 | const hash = await Commit(commitName,commitMessage,submitList); 28 | success("Commit hash:"+hash) 29 | setCommitName("") 30 | setCommitMessage("") 31 | props.success() 32 | }catch (e) { 33 | console.log(e) 34 | warning(JSON.stringify(e)) 35 | } 36 | } 37 | 38 | return
39 | 40 | ) => { 41 | setCommitName(e.target.value) 42 | }} placeholder="Title"/> 43 |