From 30ab8ae954068fe75bb8608a5747dfbcd91add2f Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 29 Apr 2021 20:05:55 +0800 Subject: [PATCH] refactor<*>: rewrite all codes... --- README.md | 2 +- build.bat | 13 +- src/App.preference.cs | 302 ---- src/App.xaml | 2 +- src/App.xaml.cs | 233 +-- src/Commands/Add.cs | 27 + src/Commands/Apply.cs | 15 + src/Commands/Blame.cs | 58 + src/Commands/Branch.cs | 33 + src/Commands/Branches.cs | 78 + src/Commands/Checkout.cs | 46 + src/Commands/CherryPick.cs | 13 + src/Commands/Clean.cs | 12 + src/Commands/Clone.cs | 26 + src/Commands/Command.cs | 161 +++ src/Commands/Commit.cs | 19 + src/Commands/CommitChanges.cs | 39 + src/Commands/CommitRangeChanges.cs | 39 + src/Commands/Commits.cs | 140 ++ src/Commands/Config.cs | 37 + src/Commands/Diff.cs | 61 + src/Commands/Discard.cs | 37 + src/Commands/Fetch.cs | 77 + src/Commands/FormatPatch.cs | 12 + src/Commands/GetRepositoryRootPath.cs | 17 + src/Commands/GitFlow.cs | 71 + src/Commands/Init.cs | 13 + src/Commands/IsBinaryFile.cs | 18 + src/Commands/IsLFSFiltered.cs | 16 + src/Commands/LocalChanges.cs | 64 + src/Commands/Merge.cs | 12 + src/Commands/Pull.cs | 48 + src/Commands/Push.cs | 39 + src/Commands/QueryFileContent.cs | 26 + src/Commands/QueryFileSizeChange.cs | 50 + src/Commands/QueryGitDir.cs | 23 + src/Commands/QueryLFSObject.cs | 28 + src/Commands/QueryLFSObjectChange.cs | 41 + src/Commands/Rebase.cs | 14 + src/Commands/Remote.cs | 31 + src/Commands/Remotes.cs | 35 + src/Commands/Reset.cs | 33 + src/Commands/Revert.cs | 13 + src/Commands/RevisionObjects.cs | 41 + src/Commands/SaveChangesToPatch.cs | 26 + src/Commands/SaveRevisionFile.cs | 44 + src/Commands/Stash.cs | 50 + src/Commands/StashChanges.cs | 38 + src/Commands/Stashes.cs | 44 + src/Commands/Submodule.cs | 44 + src/Commands/Submodules.cs | 28 + src/Commands/Tag.cs | 42 + src/Commands/Tags.cs | 45 + src/Converters/BoolToCollapsed.cs | 37 - src/Converters/FileStatusToColor.cs | 46 - src/Converters/FileStatusToIcon.cs | 45 - src/Converters/FilesDisplayModeToIcon.cs | 26 - .../FilesDisplayModeToVisibility.cs | 46 - src/Converters/IndentToMargin.cs | 37 - src/Converters/IntToRepoColor.cs | 53 - src/Converters/InverseBoolToCollapsed.cs | 37 - src/Converters/Path.cs | 29 - src/Converters/TreeViewItemDepthToMargin.cs | 69 - src/Git/Blame.cs | 30 - src/Git/Branch.cs | 207 --- src/Git/Change.cs | 147 -- src/Git/Commit.cs | 364 ----- src/Git/Diff.cs | 188 --- src/Git/LFS.cs | 18 - src/Git/MergeTool.cs | 202 --- src/Git/Remote.cs | 97 -- src/Git/Repository.cs | 1253 ----------------- src/Git/Stash.cs | 101 -- src/Git/Tag.cs | 119 -- src/Git/Version.cs | 25 - src/Helpers/CommitGraph.cs | 378 ----- src/Helpers/TextBoxHelper.cs | 224 --- src/Helpers/TreeViewHelper.cs | 350 ----- src/Helpers/Validations.cs | 141 -- src/Models/AvatarServer.cs | 22 + src/Models/BlameLine.cs | 12 + src/Models/Branch.cs | 16 + src/Models/CRLFOption.cs | 25 + src/Models/Change.cs | 74 + src/Models/Commit.cs | 21 + src/{Git => Models}/Decorator.cs | 10 +- src/Models/Exception.cs | 15 + src/Models/FileSizeChange.cs | 9 + src/Models/GitFlow.cs | 36 + src/Models/Group.cs | 12 + src/Models/LFSChange.cs | 10 + src/Models/LFSObject.cs | 9 + src/Models/Locale.cs | 22 + src/Models/MergeOption.cs | 25 + src/Models/MergeTool.cs | 119 ++ src/Models/Object.cs | 21 + src/Models/Preference.cs | 314 +++++ src/Models/Remote.cs | 10 + src/Models/Repository.cs | 48 + src/Models/ResetMode.cs | 27 + src/Models/Stash.cs | 11 + src/Models/Tag.cs | 10 + src/Models/TextChanges.cs | 34 + src/Models/TextLine.cs | 9 + src/{Git => Models}/User.cs | 26 +- src/Models/Version.cs | 63 + src/Models/Watcher.cs | 237 ++++ src/Models/WhitespaceOption.cs | 25 + src/Resources/Controls.xaml | 9 +- src/Resources/Icons.xaml | 13 +- src/Resources/Locales/en_US.xaml | 66 +- src/Resources/Locales/zh_CN.xaml | 62 +- src/Resources/Styles/Border.xaml | 12 - src/Resources/Styles/Button.xaml | 25 +- src/Resources/Styles/DataGrid.xaml | 35 +- src/Resources/Styles/IconButton.xaml | 43 + src/Resources/Styles/Label.xaml | 15 - src/Resources/Styles/ListBox.xaml | 25 + src/Resources/Styles/ListView.xaml | 15 +- src/Resources/Styles/Path.xaml | 7 +- src/Resources/Styles/TabControl.xaml | 38 +- src/Resources/Styles/TextBlock.xaml | 28 + src/Resources/Styles/TextBox.xaml | 45 +- src/Resources/Styles/ToggleButton.xaml | 34 +- .../Styles/{TreeView.xaml => Tree.xaml} | 87 +- src/SourceGit.csproj | 10 +- src/SourceGit_48.csproj | 25 + src/UI/About.xaml | 81 -- src/UI/About.xaml.cs | 45 - src/UI/AddSubmodule.xaml | 72 - src/UI/AddSubmodule.xaml.cs | 63 - src/UI/Apply.xaml | 88 -- src/UI/Apply.xaml.cs | 104 -- src/UI/Blame.xaml | 190 --- src/UI/Blame.xaml.cs | 243 ---- src/UI/CherryPick.xaml | 45 - src/UI/CherryPick.xaml.cs | 54 - src/UI/Clone.xaml | 101 -- src/UI/Clone.xaml.cs | 118 -- src/UI/CommitViewer.xaml | 464 ------ src/UI/CommitViewer.xaml.cs | 619 -------- src/UI/Configure.xaml | 55 - src/UI/Configure.xaml.cs | 64 - src/UI/CreateBranch.xaml | 80 -- src/UI/CreateBranch.xaml.cs | 137 -- src/UI/CreateTag.xaml | 70 - src/UI/CreateTag.xaml.cs | 90 -- src/UI/Dashboard.xaml | 526 ------- src/UI/Dashboard.xaml.cs | 1161 --------------- src/UI/DeleteBranch.xaml | 42 - src/UI/DeleteBranch.xaml.cs | 55 - src/UI/DeleteRemote.xaml | 42 - src/UI/DeleteRemote.xaml.cs | 56 - src/UI/DeleteSubmodule.xaml | 42 - src/UI/DeleteSubmodule.xaml.cs | 36 - src/UI/DeleteTag.xaml | 45 - src/UI/DeleteTag.xaml.cs | 60 - src/UI/DiffViewer.xaml | 120 -- src/UI/DiffViewer.xaml.cs | 788 ----------- src/UI/Discard.xaml | 46 - src/UI/Discard.xaml.cs | 67 - src/UI/Fetch.xaml | 66 - src/UI/Fetch.xaml.cs | 76 - src/UI/FileHistories.xaml | 153 -- src/UI/FileHistories.xaml.cs | 120 -- src/UI/FilesDisplayModeSwitch.xaml | 49 - src/UI/FilesDisplayModeSwitch.xaml.cs | 46 - src/UI/FolderDialog.xaml | 105 -- src/UI/FolderDialog.xaml.cs | 211 --- src/UI/GitFlowFinishBranch.xaml | 42 - src/UI/GitFlowFinishBranch.xaml.cs | 76 - src/UI/GitFlowSetup.xaml | 60 - src/UI/GitFlowSetup.xaml.cs | 124 -- src/UI/GitFlowStartBranch.xaml | 49 - src/UI/GitFlowStartBranch.xaml.cs | 92 -- src/UI/Histories.xaml | 229 --- src/UI/Histories.xaml.cs | 599 -------- src/UI/Init.xaml | 45 - src/UI/Init.xaml.cs | 64 - src/UI/InteractiveRebase.xaml | 215 --- src/UI/InteractiveRebase.xaml.cs | 220 --- src/UI/Launcher.xaml | 329 ----- src/UI/Launcher.xaml.cs | 339 ----- src/UI/Merge.xaml | 62 - src/UI/Merge.xaml.cs | 104 -- src/UI/NewPage.xaml | 184 --- src/UI/NewPage.xaml.cs | 430 ------ src/UI/PopupManager.xaml | 28 - src/UI/PopupManager.xaml.cs | 153 -- src/UI/Pull.xaml | 88 -- src/UI/Pull.xaml.cs | 120 -- src/UI/Push.xaml | 96 -- src/UI/Push.xaml.cs | 184 --- src/UI/PushTag.xaml | 57 - src/UI/PushTag.xaml.cs | 62 - src/UI/Rebase.xaml | 55 - src/UI/Rebase.xaml.cs | 81 -- src/UI/Remote.xaml | 69 - src/UI/Remote.xaml.cs | 81 -- src/UI/RenameBranch.xaml | 59 - src/UI/RenameBranch.xaml.cs | 68 - src/UI/Reset.xaml | 64 - src/UI/Reset.xaml.cs | 89 -- src/UI/Revert.xaml | 45 - src/UI/Revert.xaml.cs | 58 - src/UI/SettingDialog.xaml | 222 --- src/UI/SettingDialog.xaml.cs | 285 ---- src/UI/Stash.xaml | 46 - src/UI/Stash.xaml.cs | 56 - src/UI/Stashes.xaml | 137 -- src/UI/Stashes.xaml.cs | 119 -- src/UI/TwoCommitsDiff.xaml | 146 -- src/UI/TwoCommitsDiff.xaml.cs | 288 ---- src/UI/UpdateAvailable.xaml | 108 -- src/UI/UpdateAvailable.xaml.cs | 47 - src/UI/WorkingCopy.xaml | 435 ------ src/UI/WorkingCopy.xaml.cs | 1190 ---------------- src/Views/About.xaml | 88 ++ src/Views/About.xaml.cs | 33 + src/Views/Blame.xaml | 136 ++ src/Views/Blame.xaml.cs | 195 +++ src/{Helpers => Views/Controls}/Avatar.cs | 117 +- src/Views/Controls/Badge.cs | 52 + src/Views/Controls/Bookmark.cs | 86 ++ src/Views/Controls/ChangeDisplaySwitcher.cs | 105 ++ src/Views/Controls/ChangeStatusIcon.cs | 111 ++ src/Views/Controls/CommitGraph.cs | 312 ++++ src/Views/Controls/DragDropAdorner.cs | 80 ++ src/Views/Controls/IconButton.cs | 34 + src/Views/Controls/Loading.cs | 51 + src/Views/Controls/PageContainer.cs | 66 + src/Views/Controls/PopupWidget.cs | 92 ++ src/Views/Controls/TextEdit.cs | 109 ++ src/Views/Controls/Tree.cs | 228 +++ src/Views/Controls/TreeItem.cs | 41 + src/Views/Controls/WindowBorder.cs | 36 + src/Views/Converters/BoolToCollapsed.cs | 18 + src/Views/Converters/BranchToName.cs | 19 + src/{ => Views}/Converters/InverseBool.cs | 10 +- src/Views/Converters/PureFileName.cs | 21 + src/Views/Converters/PureFolderName.cs | 21 + src/Views/FolderBrowser.xaml | 98 ++ src/Views/FolderBrowser.xaml.cs | 102 ++ src/Views/Histories.xaml | 125 ++ src/Views/Histories.xaml.cs | 70 + src/Views/Launcher.xaml | 65 + src/Views/Launcher.xaml.cs | 97 ++ src/Views/Popups/AddSubmodule.xaml | 69 + src/Views/Popups/AddSubmodule.xaml.cs | 40 + src/Views/Popups/Apply.xaml | 94 ++ src/Views/Popups/Apply.xaml.cs | 50 + src/Views/Popups/CherryPick.xaml | 41 + src/Views/Popups/CherryPick.xaml.cs | 35 + src/Views/Popups/Clone.xaml | 128 ++ src/Views/Popups/Clone.xaml.cs | 63 + src/Views/Popups/Configure.xaml | 44 + src/Views/Popups/Configure.xaml.cs | 39 + src/Views/Popups/CreateBranch.xaml | 77 + src/Views/Popups/CreateBranch.xaml.cs | 83 ++ src/Views/Popups/CreateTag.xaml | 69 + src/Views/Popups/CreateTag.xaml.cs | 55 + src/Views/Popups/DeleteBranch.xaml | 33 + src/Views/Popups/DeleteBranch.xaml.cs | 40 + src/Views/Popups/DeleteRemote.xaml | 33 + src/Views/Popups/DeleteRemote.xaml.cs | 33 + src/Views/Popups/DeleteSubmodule.xaml | 33 + src/Views/Popups/DeleteSubmodule.xaml.cs | 33 + src/Views/Popups/DeleteTag.xaml | 41 + src/Views/Popups/DeleteTag.xaml.cs | 35 + src/Views/Popups/Discard.xaml | 39 + src/Views/Popups/Discard.xaml.cs | 42 + src/Views/Popups/Fetch.xaml | 57 + src/Views/Popups/Fetch.xaml.cs | 41 + src/Views/Popups/GitFlowFinish.xaml | 35 + src/Views/Popups/GitFlowFinish.xaml.cs | 57 + src/Views/Popups/GitFlowStart.xaml | 41 + src/Views/Popups/GitFlowStart.xaml.cs | 62 + src/Views/Popups/Init.xaml | 43 + src/Views/Popups/Init.xaml.cs | 31 + src/Views/Popups/InitGitFlow.xaml | 102 ++ src/Views/Popups/InitGitFlow.xaml.cs | 50 + src/Views/Popups/Merge.xaml | 71 + src/Views/Popups/Merge.xaml.cs | 35 + src/Views/Popups/Pull.xaml | 93 ++ src/Views/Popups/Pull.xaml.cs | 68 + src/Views/Popups/Push.xaml | 100 ++ src/Views/Popups/Push.xaml.cs | 108 ++ src/Views/Popups/PushTag.xaml | 53 + src/Views/Popups/PushTag.xaml.cs | 38 + src/Views/Popups/Rebase.xaml | 54 + src/Views/Popups/Rebase.xaml.cs | 48 + src/Views/Popups/Remote.xaml | 61 + src/Views/Popups/Remote.xaml.cs | 59 + src/Views/Popups/RenameBranch.xaml | 55 + src/Views/Popups/RenameBranch.xaml.cs | 40 + src/Views/Popups/Reset.xaml | 72 + src/Views/Popups/Reset.xaml.cs | 35 + src/Views/Popups/Revert.xaml | 41 + src/Views/Popups/Revert.xaml.cs | 35 + src/Views/Popups/Stash.xaml | 38 + src/Views/Popups/Stash.xaml.cs | 46 + src/Views/Preference.xaml | 307 ++++ src/Views/Preference.xaml.cs | 101 ++ src/Views/Upgrade.xaml | 115 ++ src/Views/Upgrade.xaml.cs | 34 + src/Views/Validations/BranchName.cs | 27 + src/Views/Validations/CloneDir.cs | 13 + src/Views/Validations/CommitMessage.cs | 13 + src/Views/Validations/GitURL.cs | 18 + src/Views/Validations/LocalRepositoryName.cs | 16 + src/Views/Validations/PatchFile.cs | 13 + src/Views/Validations/RemoteName.cs | 31 + src/Views/Validations/SubmodulePath.cs | 17 + src/Views/Validations/TagName.cs | 26 + src/Views/Widgets/CommitChanges.xaml | 196 +++ src/Views/Widgets/CommitChanges.xaml.cs | 310 ++++ src/Views/Widgets/CommitDetail.xaml | 344 +++++ src/Views/Widgets/CommitDetail.xaml.cs | 416 ++++++ src/Views/Widgets/Dashboard.xaml | 478 +++++++ src/Views/Widgets/Dashboard.xaml.cs | 982 +++++++++++++ src/Views/Widgets/DiffViewer.xaml | 139 ++ src/Views/Widgets/DiffViewer.xaml.cs | 621 ++++++++ src/Views/Widgets/Exceptions.xaml | 45 + src/Views/Widgets/Exceptions.xaml.cs | 24 + src/Views/Widgets/Histories.xaml | 212 +++ src/Views/Widgets/Histories.xaml.cs | 599 ++++++++ src/Views/Widgets/PageTabBar.xaml | 116 ++ src/Views/Widgets/PageTabBar.xaml.cs | 197 +++ src/Views/Widgets/PageTabItem.xaml | 31 + src/Views/Widgets/PageTabItem.xaml.cs | 78 + src/Views/Widgets/PopupPanel.xaml | 46 + src/Views/Widgets/PopupPanel.xaml.cs | 107 ++ src/Views/Widgets/RevisionCompare.xaml | 107 ++ src/Views/Widgets/RevisionCompare.xaml.cs | 36 + src/Views/Widgets/Stashes.xaml | 129 ++ src/Views/Widgets/Stashes.xaml.cs | 85 ++ src/Views/Widgets/Welcome.xaml | 186 +++ src/Views/Widgets/Welcome.xaml.cs | 386 +++++ src/Views/Widgets/WorkingCopy.xaml | 249 ++++ src/Views/Widgets/WorkingCopy.xaml.cs | 340 +++++ src/Views/Widgets/WorkingCopyChanges.xaml | 151 ++ src/Views/Widgets/WorkingCopyChanges.xaml.cs | 859 +++++++++++ 342 files changed, 17208 insertions(+), 19633 deletions(-) delete mode 100644 src/App.preference.cs create mode 100644 src/Commands/Add.cs create mode 100644 src/Commands/Apply.cs create mode 100644 src/Commands/Blame.cs create mode 100644 src/Commands/Branch.cs create mode 100644 src/Commands/Branches.cs create mode 100644 src/Commands/Checkout.cs create mode 100644 src/Commands/CherryPick.cs create mode 100644 src/Commands/Clean.cs create mode 100644 src/Commands/Clone.cs create mode 100644 src/Commands/Command.cs create mode 100644 src/Commands/Commit.cs create mode 100644 src/Commands/CommitChanges.cs create mode 100644 src/Commands/CommitRangeChanges.cs create mode 100644 src/Commands/Commits.cs create mode 100644 src/Commands/Config.cs create mode 100644 src/Commands/Diff.cs create mode 100644 src/Commands/Discard.cs create mode 100644 src/Commands/Fetch.cs create mode 100644 src/Commands/FormatPatch.cs create mode 100644 src/Commands/GetRepositoryRootPath.cs create mode 100644 src/Commands/GitFlow.cs create mode 100644 src/Commands/Init.cs create mode 100644 src/Commands/IsBinaryFile.cs create mode 100644 src/Commands/IsLFSFiltered.cs create mode 100644 src/Commands/LocalChanges.cs create mode 100644 src/Commands/Merge.cs create mode 100644 src/Commands/Pull.cs create mode 100644 src/Commands/Push.cs create mode 100644 src/Commands/QueryFileContent.cs create mode 100644 src/Commands/QueryFileSizeChange.cs create mode 100644 src/Commands/QueryGitDir.cs create mode 100644 src/Commands/QueryLFSObject.cs create mode 100644 src/Commands/QueryLFSObjectChange.cs create mode 100644 src/Commands/Rebase.cs create mode 100644 src/Commands/Remote.cs create mode 100644 src/Commands/Remotes.cs create mode 100644 src/Commands/Reset.cs create mode 100644 src/Commands/Revert.cs create mode 100644 src/Commands/RevisionObjects.cs create mode 100644 src/Commands/SaveChangesToPatch.cs create mode 100644 src/Commands/SaveRevisionFile.cs create mode 100644 src/Commands/Stash.cs create mode 100644 src/Commands/StashChanges.cs create mode 100644 src/Commands/Stashes.cs create mode 100644 src/Commands/Submodule.cs create mode 100644 src/Commands/Submodules.cs create mode 100644 src/Commands/Tag.cs create mode 100644 src/Commands/Tags.cs delete mode 100644 src/Converters/BoolToCollapsed.cs delete mode 100644 src/Converters/FileStatusToColor.cs delete mode 100644 src/Converters/FileStatusToIcon.cs delete mode 100644 src/Converters/FilesDisplayModeToIcon.cs delete mode 100644 src/Converters/FilesDisplayModeToVisibility.cs delete mode 100644 src/Converters/IndentToMargin.cs delete mode 100644 src/Converters/IntToRepoColor.cs delete mode 100644 src/Converters/InverseBoolToCollapsed.cs delete mode 100644 src/Converters/Path.cs delete mode 100644 src/Converters/TreeViewItemDepthToMargin.cs delete mode 100644 src/Git/Blame.cs delete mode 100644 src/Git/Branch.cs delete mode 100644 src/Git/Change.cs delete mode 100644 src/Git/Commit.cs delete mode 100644 src/Git/Diff.cs delete mode 100644 src/Git/LFS.cs delete mode 100644 src/Git/MergeTool.cs delete mode 100644 src/Git/Remote.cs delete mode 100644 src/Git/Repository.cs delete mode 100644 src/Git/Stash.cs delete mode 100644 src/Git/Tag.cs delete mode 100644 src/Git/Version.cs delete mode 100644 src/Helpers/CommitGraph.cs delete mode 100644 src/Helpers/TextBoxHelper.cs delete mode 100644 src/Helpers/TreeViewHelper.cs delete mode 100644 src/Helpers/Validations.cs create mode 100644 src/Models/AvatarServer.cs create mode 100644 src/Models/BlameLine.cs create mode 100644 src/Models/Branch.cs create mode 100644 src/Models/CRLFOption.cs create mode 100644 src/Models/Change.cs create mode 100644 src/Models/Commit.cs rename src/{Git => Models}/Decorator.cs (55%) create mode 100644 src/Models/Exception.cs create mode 100644 src/Models/FileSizeChange.cs create mode 100644 src/Models/GitFlow.cs create mode 100644 src/Models/Group.cs create mode 100644 src/Models/LFSChange.cs create mode 100644 src/Models/LFSObject.cs create mode 100644 src/Models/Locale.cs create mode 100644 src/Models/MergeOption.cs create mode 100644 src/Models/MergeTool.cs create mode 100644 src/Models/Object.cs create mode 100644 src/Models/Preference.cs create mode 100644 src/Models/Remote.cs create mode 100644 src/Models/Repository.cs create mode 100644 src/Models/ResetMode.cs create mode 100644 src/Models/Stash.cs create mode 100644 src/Models/Tag.cs create mode 100644 src/Models/TextChanges.cs create mode 100644 src/Models/TextLine.cs rename src/{Git => Models}/User.cs (51%) create mode 100644 src/Models/Version.cs create mode 100644 src/Models/Watcher.cs create mode 100644 src/Models/WhitespaceOption.cs delete mode 100644 src/Resources/Styles/Border.xaml create mode 100644 src/Resources/Styles/IconButton.xaml delete mode 100644 src/Resources/Styles/Label.xaml create mode 100644 src/Resources/Styles/ListBox.xaml create mode 100644 src/Resources/Styles/TextBlock.xaml rename src/Resources/Styles/{TreeView.xaml => Tree.xaml} (56%) create mode 100644 src/SourceGit_48.csproj delete mode 100644 src/UI/About.xaml delete mode 100644 src/UI/About.xaml.cs delete mode 100644 src/UI/AddSubmodule.xaml delete mode 100644 src/UI/AddSubmodule.xaml.cs delete mode 100644 src/UI/Apply.xaml delete mode 100644 src/UI/Apply.xaml.cs delete mode 100644 src/UI/Blame.xaml delete mode 100644 src/UI/Blame.xaml.cs delete mode 100644 src/UI/CherryPick.xaml delete mode 100644 src/UI/CherryPick.xaml.cs delete mode 100644 src/UI/Clone.xaml delete mode 100644 src/UI/Clone.xaml.cs delete mode 100644 src/UI/CommitViewer.xaml delete mode 100644 src/UI/CommitViewer.xaml.cs delete mode 100644 src/UI/Configure.xaml delete mode 100644 src/UI/Configure.xaml.cs delete mode 100644 src/UI/CreateBranch.xaml delete mode 100644 src/UI/CreateBranch.xaml.cs delete mode 100644 src/UI/CreateTag.xaml delete mode 100644 src/UI/CreateTag.xaml.cs delete mode 100644 src/UI/Dashboard.xaml delete mode 100644 src/UI/Dashboard.xaml.cs delete mode 100644 src/UI/DeleteBranch.xaml delete mode 100644 src/UI/DeleteBranch.xaml.cs delete mode 100644 src/UI/DeleteRemote.xaml delete mode 100644 src/UI/DeleteRemote.xaml.cs delete mode 100644 src/UI/DeleteSubmodule.xaml delete mode 100644 src/UI/DeleteSubmodule.xaml.cs delete mode 100644 src/UI/DeleteTag.xaml delete mode 100644 src/UI/DeleteTag.xaml.cs delete mode 100644 src/UI/DiffViewer.xaml delete mode 100644 src/UI/DiffViewer.xaml.cs delete mode 100644 src/UI/Discard.xaml delete mode 100644 src/UI/Discard.xaml.cs delete mode 100644 src/UI/Fetch.xaml delete mode 100644 src/UI/Fetch.xaml.cs delete mode 100644 src/UI/FileHistories.xaml delete mode 100644 src/UI/FileHistories.xaml.cs delete mode 100644 src/UI/FilesDisplayModeSwitch.xaml delete mode 100644 src/UI/FilesDisplayModeSwitch.xaml.cs delete mode 100644 src/UI/FolderDialog.xaml delete mode 100644 src/UI/FolderDialog.xaml.cs delete mode 100644 src/UI/GitFlowFinishBranch.xaml delete mode 100644 src/UI/GitFlowFinishBranch.xaml.cs delete mode 100644 src/UI/GitFlowSetup.xaml delete mode 100644 src/UI/GitFlowSetup.xaml.cs delete mode 100644 src/UI/GitFlowStartBranch.xaml delete mode 100644 src/UI/GitFlowStartBranch.xaml.cs delete mode 100644 src/UI/Histories.xaml delete mode 100644 src/UI/Histories.xaml.cs delete mode 100644 src/UI/Init.xaml delete mode 100644 src/UI/Init.xaml.cs delete mode 100644 src/UI/InteractiveRebase.xaml delete mode 100644 src/UI/InteractiveRebase.xaml.cs delete mode 100644 src/UI/Launcher.xaml delete mode 100644 src/UI/Launcher.xaml.cs delete mode 100644 src/UI/Merge.xaml delete mode 100644 src/UI/Merge.xaml.cs delete mode 100644 src/UI/NewPage.xaml delete mode 100644 src/UI/NewPage.xaml.cs delete mode 100644 src/UI/PopupManager.xaml delete mode 100644 src/UI/PopupManager.xaml.cs delete mode 100644 src/UI/Pull.xaml delete mode 100644 src/UI/Pull.xaml.cs delete mode 100644 src/UI/Push.xaml delete mode 100644 src/UI/Push.xaml.cs delete mode 100644 src/UI/PushTag.xaml delete mode 100644 src/UI/PushTag.xaml.cs delete mode 100644 src/UI/Rebase.xaml delete mode 100644 src/UI/Rebase.xaml.cs delete mode 100644 src/UI/Remote.xaml delete mode 100644 src/UI/Remote.xaml.cs delete mode 100644 src/UI/RenameBranch.xaml delete mode 100644 src/UI/RenameBranch.xaml.cs delete mode 100644 src/UI/Reset.xaml delete mode 100644 src/UI/Reset.xaml.cs delete mode 100644 src/UI/Revert.xaml delete mode 100644 src/UI/Revert.xaml.cs delete mode 100644 src/UI/SettingDialog.xaml delete mode 100644 src/UI/SettingDialog.xaml.cs delete mode 100644 src/UI/Stash.xaml delete mode 100644 src/UI/Stash.xaml.cs delete mode 100644 src/UI/Stashes.xaml delete mode 100644 src/UI/Stashes.xaml.cs delete mode 100644 src/UI/TwoCommitsDiff.xaml delete mode 100644 src/UI/TwoCommitsDiff.xaml.cs delete mode 100644 src/UI/UpdateAvailable.xaml delete mode 100644 src/UI/UpdateAvailable.xaml.cs delete mode 100644 src/UI/WorkingCopy.xaml delete mode 100644 src/UI/WorkingCopy.xaml.cs create mode 100644 src/Views/About.xaml create mode 100644 src/Views/About.xaml.cs create mode 100644 src/Views/Blame.xaml create mode 100644 src/Views/Blame.xaml.cs rename src/{Helpers => Views/Controls}/Avatar.cs (66%) create mode 100644 src/Views/Controls/Badge.cs create mode 100644 src/Views/Controls/Bookmark.cs create mode 100644 src/Views/Controls/ChangeDisplaySwitcher.cs create mode 100644 src/Views/Controls/ChangeStatusIcon.cs create mode 100644 src/Views/Controls/CommitGraph.cs create mode 100644 src/Views/Controls/DragDropAdorner.cs create mode 100644 src/Views/Controls/IconButton.cs create mode 100644 src/Views/Controls/Loading.cs create mode 100644 src/Views/Controls/PageContainer.cs create mode 100644 src/Views/Controls/PopupWidget.cs create mode 100644 src/Views/Controls/TextEdit.cs create mode 100644 src/Views/Controls/Tree.cs create mode 100644 src/Views/Controls/TreeItem.cs create mode 100644 src/Views/Controls/WindowBorder.cs create mode 100644 src/Views/Converters/BoolToCollapsed.cs create mode 100644 src/Views/Converters/BranchToName.cs rename src/{ => Views}/Converters/InverseBool.cs (71%) create mode 100644 src/Views/Converters/PureFileName.cs create mode 100644 src/Views/Converters/PureFolderName.cs create mode 100644 src/Views/FolderBrowser.xaml create mode 100644 src/Views/FolderBrowser.xaml.cs create mode 100644 src/Views/Histories.xaml create mode 100644 src/Views/Histories.xaml.cs create mode 100644 src/Views/Launcher.xaml create mode 100644 src/Views/Launcher.xaml.cs create mode 100644 src/Views/Popups/AddSubmodule.xaml create mode 100644 src/Views/Popups/AddSubmodule.xaml.cs create mode 100644 src/Views/Popups/Apply.xaml create mode 100644 src/Views/Popups/Apply.xaml.cs create mode 100644 src/Views/Popups/CherryPick.xaml create mode 100644 src/Views/Popups/CherryPick.xaml.cs create mode 100644 src/Views/Popups/Clone.xaml create mode 100644 src/Views/Popups/Clone.xaml.cs create mode 100644 src/Views/Popups/Configure.xaml create mode 100644 src/Views/Popups/Configure.xaml.cs create mode 100644 src/Views/Popups/CreateBranch.xaml create mode 100644 src/Views/Popups/CreateBranch.xaml.cs create mode 100644 src/Views/Popups/CreateTag.xaml create mode 100644 src/Views/Popups/CreateTag.xaml.cs create mode 100644 src/Views/Popups/DeleteBranch.xaml create mode 100644 src/Views/Popups/DeleteBranch.xaml.cs create mode 100644 src/Views/Popups/DeleteRemote.xaml create mode 100644 src/Views/Popups/DeleteRemote.xaml.cs create mode 100644 src/Views/Popups/DeleteSubmodule.xaml create mode 100644 src/Views/Popups/DeleteSubmodule.xaml.cs create mode 100644 src/Views/Popups/DeleteTag.xaml create mode 100644 src/Views/Popups/DeleteTag.xaml.cs create mode 100644 src/Views/Popups/Discard.xaml create mode 100644 src/Views/Popups/Discard.xaml.cs create mode 100644 src/Views/Popups/Fetch.xaml create mode 100644 src/Views/Popups/Fetch.xaml.cs create mode 100644 src/Views/Popups/GitFlowFinish.xaml create mode 100644 src/Views/Popups/GitFlowFinish.xaml.cs create mode 100644 src/Views/Popups/GitFlowStart.xaml create mode 100644 src/Views/Popups/GitFlowStart.xaml.cs create mode 100644 src/Views/Popups/Init.xaml create mode 100644 src/Views/Popups/Init.xaml.cs create mode 100644 src/Views/Popups/InitGitFlow.xaml create mode 100644 src/Views/Popups/InitGitFlow.xaml.cs create mode 100644 src/Views/Popups/Merge.xaml create mode 100644 src/Views/Popups/Merge.xaml.cs create mode 100644 src/Views/Popups/Pull.xaml create mode 100644 src/Views/Popups/Pull.xaml.cs create mode 100644 src/Views/Popups/Push.xaml create mode 100644 src/Views/Popups/Push.xaml.cs create mode 100644 src/Views/Popups/PushTag.xaml create mode 100644 src/Views/Popups/PushTag.xaml.cs create mode 100644 src/Views/Popups/Rebase.xaml create mode 100644 src/Views/Popups/Rebase.xaml.cs create mode 100644 src/Views/Popups/Remote.xaml create mode 100644 src/Views/Popups/Remote.xaml.cs create mode 100644 src/Views/Popups/RenameBranch.xaml create mode 100644 src/Views/Popups/RenameBranch.xaml.cs create mode 100644 src/Views/Popups/Reset.xaml create mode 100644 src/Views/Popups/Reset.xaml.cs create mode 100644 src/Views/Popups/Revert.xaml create mode 100644 src/Views/Popups/Revert.xaml.cs create mode 100644 src/Views/Popups/Stash.xaml create mode 100644 src/Views/Popups/Stash.xaml.cs create mode 100644 src/Views/Preference.xaml create mode 100644 src/Views/Preference.xaml.cs create mode 100644 src/Views/Upgrade.xaml create mode 100644 src/Views/Upgrade.xaml.cs create mode 100644 src/Views/Validations/BranchName.cs create mode 100644 src/Views/Validations/CloneDir.cs create mode 100644 src/Views/Validations/CommitMessage.cs create mode 100644 src/Views/Validations/GitURL.cs create mode 100644 src/Views/Validations/LocalRepositoryName.cs create mode 100644 src/Views/Validations/PatchFile.cs create mode 100644 src/Views/Validations/RemoteName.cs create mode 100644 src/Views/Validations/SubmodulePath.cs create mode 100644 src/Views/Validations/TagName.cs create mode 100644 src/Views/Widgets/CommitChanges.xaml create mode 100644 src/Views/Widgets/CommitChanges.xaml.cs create mode 100644 src/Views/Widgets/CommitDetail.xaml create mode 100644 src/Views/Widgets/CommitDetail.xaml.cs create mode 100644 src/Views/Widgets/Dashboard.xaml create mode 100644 src/Views/Widgets/Dashboard.xaml.cs create mode 100644 src/Views/Widgets/DiffViewer.xaml create mode 100644 src/Views/Widgets/DiffViewer.xaml.cs create mode 100644 src/Views/Widgets/Exceptions.xaml create mode 100644 src/Views/Widgets/Exceptions.xaml.cs create mode 100644 src/Views/Widgets/Histories.xaml create mode 100644 src/Views/Widgets/Histories.xaml.cs create mode 100644 src/Views/Widgets/PageTabBar.xaml create mode 100644 src/Views/Widgets/PageTabBar.xaml.cs create mode 100644 src/Views/Widgets/PageTabItem.xaml create mode 100644 src/Views/Widgets/PageTabItem.xaml.cs create mode 100644 src/Views/Widgets/PopupPanel.xaml create mode 100644 src/Views/Widgets/PopupPanel.xaml.cs create mode 100644 src/Views/Widgets/RevisionCompare.xaml create mode 100644 src/Views/Widgets/RevisionCompare.xaml.cs create mode 100644 src/Views/Widgets/Stashes.xaml create mode 100644 src/Views/Widgets/Stashes.xaml.cs create mode 100644 src/Views/Widgets/Welcome.xaml create mode 100644 src/Views/Widgets/Welcome.xaml.cs create mode 100644 src/Views/Widgets/WorkingCopy.xaml create mode 100644 src/Views/Widgets/WorkingCopy.xaml.cs create mode 100644 src/Views/Widgets/WorkingCopyChanges.xaml create mode 100644 src/Views/Widgets/WorkingCopyChanges.xaml.cs diff --git a/README.md b/README.md index 72c1500a..597efba8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [发行版](https://gitee.com/sourcegit/SourceGit/releases/) * `SourceGit.exe`为不带.NET 5.0运行时的可执行文件,需要先安装.NET 5 -* `SourceGit.zip`为自带.NET 5.0的可执行文件 +* `SourceGit_48.exe`为.NET 4.8编译的可执行文件,Window 10 已内置该运行时 ## 预览 diff --git a/build.bat b/build.bat index d039bd6f..d973a67a 100644 --- a/build.bat +++ b/build.bat @@ -1,9 +1,16 @@ +rmdir /s /q publish + cd src rmdir /s /q bin rmdir /s /q obj -dotnet publish --nologo -c Release -r win-x64 -p:PublishSingleFile=true --no-self-contained -o ../publish +dotnet publish SourceGit.csproj --nologo -c Release -r win-x64 -p:PublishSingleFile=true --no-self-contained -o ../publish rmdir /s /q bin rmdir /s /q obj -dotnet publish --nologo -c Release -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=true -p:TrimMode=link --self-contained -o ../publish/SourceGit -cd .. \ No newline at end of file +dotnet publish SourceGit_48.csproj --nologo -c Release -r win-x64 -o ../publish/net48 + +cd ../publish +ilrepack /ndebug /out:SourceGit_48.exe net48/SourceGit.exe net48/Newtonsoft.Json.dll +rmdir /s /q net48 + +cd ../ \ No newline at end of file diff --git a/src/App.preference.cs b/src/App.preference.cs deleted file mode 100644 index a30204a1..00000000 --- a/src/App.preference.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace SourceGit { - - /// - /// User's preference settings. Serialized to - /// - public class Preference { - - /// - /// Tools setting. - /// - public class ToolSetting { - /// - /// Git executable file path. - /// - public string GitExecutable { get; set; } - /// - /// Default clone directory. - /// - public string GitDefaultCloneDir { get; set; } - /// - /// Selected merge tool. - /// - public int MergeTool { get; set; } = 0; - /// - /// Executable file path for merge tool. - /// - public string MergeExecutable { get; set; } = "--"; - } - - /// - /// File's display mode. - /// - public enum FilesDisplayMode { - Tree, - List, - Grid, - } - - /// - /// Settings for UI. - /// - public class UISetting { - /// - /// Use light theme? - /// - public bool UseLightTheme { get; set; } - /// - /// Locale - /// - public string Locale { get; set; } = "en_US"; - /// - /// Base URL to get avatar - /// - public string AvatarServer { get; set; } = "https://www.gravatar.com/avatar/"; - /// - /// Main window width - /// - public double WindowWidth { get; set; } - /// - /// Main window height - /// - public double WindowHeight { get; set; } - /// - /// Move commit viewer from bottom to right - /// - public bool MoveCommitViewerRight { get; set; } - /// - /// File's display mode in unstaged view. - /// - public FilesDisplayMode UnstageFileDisplayMode { get; set; } - /// - /// File's display mode in staged view. - /// - public FilesDisplayMode StagedFileDisplayMode { get; set; } - /// - /// Use DataGrid instead of TreeView in changes view. - /// - public bool UseListInChanges { get; set; } - /// - /// Use combined instead of side-by-side mode in diff viewer. - /// - public bool UseCombinedDiff { get; set; } - } - - /// - /// Group(Virtual folder) for watched repositories. - /// - public class Group { - /// - /// Unique ID of this group. - /// - public string Id { get; set; } - /// - /// Display name. - /// - public string Name { get; set; } - /// - /// Parent ID. - /// - public string ParentId { get; set; } - /// - /// Cache UI IsExpended status. - /// - public bool IsExpended { get; set; } - } - - #region SAVED_DATAS - /// - /// Check for updates. - /// - public bool CheckUpdate { get; set; } = true; - /// - /// Last UNIX timestamp to check for update. - /// - public int LastCheckUpdate { get; set; } = 0; - /// - /// Fetch remotes automatically? - /// - public bool AutoFetchRemotes { get; set; } = true; - /// - /// Settings for executables. - /// - public ToolSetting Tools { get; set; } = new ToolSetting(); - /// - /// Use light color theme. - /// - public UISetting UI { get; set; } = new UISetting(); - #endregion - - #region SETTING_REPOS - /// - /// Groups for repositories. - /// - public List Groups { get; set; } = new List(); - /// - /// Watched repositories. - /// - public List Repositories { get; set; } = new List(); - #endregion - - #region METHODS_ON_GROUP - /// - /// Add new group(virtual folder). - /// - /// Display name. - /// Parent group ID. - /// Added group instance. - public Group AddGroup(string name, string parentId) { - var group = new Group() { - Name = name, - Id = Guid.NewGuid().ToString(), - ParentId = parentId, - IsExpended = false, - }; - - Groups.Add(group); - Groups.Sort((l, r) => l.Name.CompareTo(r.Name)); - - return group; - } - - /// - /// Find group by ID. - /// - /// Unique ID - /// Founded group's instance. - public Group FindGroup(string id) { - foreach (var group in Groups) { - if (group.Id == id) return group; - } - return null; - } - - /// - /// Rename group. - /// - /// Unique ID - /// New name. - public void RenameGroup(string id, string newName) { - foreach (var group in Groups) { - if (group.Id == id) { - group.Name = newName; - break; - } - } - - Groups.Sort((l, r) => l.Name.CompareTo(r.Name)); - } - - /// - /// Remove a group. - /// - /// Unique ID - public void RemoveGroup(string id) { - int removedIdx = -1; - - for (int i = 0; i < Groups.Count; i++) { - if (Groups[i].Id == id) { - removedIdx = i; - break; - } - } - - if (removedIdx >= 0) Groups.RemoveAt(removedIdx); - } - - /// - /// Check if given group has relations. - /// - /// - /// - /// - public bool IsSubGroup(string parentId, string subId) { - if (string.IsNullOrEmpty(parentId)) return false; - if (parentId == subId) return true; - - var g = FindGroup(subId); - if (g == null) return false; - - g = FindGroup(g.ParentId); - while (g != null) { - if (g.Id == parentId) return true; - g = FindGroup(g.ParentId); - } - - return false; - } - #endregion - - #region METHODS_ON_REPOS - /// - /// Add repository. - /// - /// Local storage path. - /// Group's ID - /// Added repository instance. - public Git.Repository AddRepository(string path, string groupId) { - var repo = FindRepository(path); - if (repo != null) return repo; - - var dir = new DirectoryInfo(path); - repo = new Git.Repository() { - Path = dir.FullName, - Name = dir.Name, - GroupId = groupId, - }; - - Repositories.Add(repo); - Repositories.Sort((l, r) => l.Name.CompareTo(r.Name)); - return repo; - } - - /// - /// Find repository by path. - /// - /// Local storage path. - /// Founded repository instance. - public Git.Repository FindRepository(string path) { - var dir = new DirectoryInfo(path); - foreach (var repo in Repositories) { - if (repo.Path == dir.FullName) return repo; - } - return null; - } - - /// - /// Change a repository's display name in RepositoryManager. - /// - /// Local storage path. - /// New name - public void RenameRepository(string path, string newName) { - var repo = FindRepository(path); - if (repo == null) return; - - repo.Name = newName; - Repositories.Sort((l, r) => l.Name.CompareTo(r.Name)); - } - - /// - /// Remove a repository in RepositoryManager. - /// - /// Local storage path. - public void RemoveRepository(string path) { - var dir = new DirectoryInfo(path); - var removedIdx = -1; - - for (int i = 0; i < Repositories.Count; i++) { - if (Repositories[i].Path == dir.FullName) { - removedIdx = i; - break; - } - } - - if (removedIdx >= 0) Repositories.RemoveAt(removedIdx); - } - #endregion - } -} diff --git a/src/App.xaml b/src/App.xaml index a85e0cf5..3eb35fa1 100644 --- a/src/App.xaml +++ b/src/App.xaml @@ -8,7 +8,7 @@ - + diff --git a/src/App.xaml.cs b/src/App.xaml.cs index a92df019..94a8780b 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -1,10 +1,8 @@ -using Microsoft.Win32; using System; using System.IO; using System.Net; using System.Reflection; using System.Text; -using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; @@ -12,211 +10,106 @@ using System.Windows; namespace SourceGit { /// - /// Application. + /// 程序入口. /// public partial class App : Application { - /// - /// Getter/Setter for application user setting. - /// - public static Preference Setting { get; set; } /// - /// Check if GIT has been configured. + /// 读取本地化字串 /// - public static bool IsGitConfigured { - get { - return !string.IsNullOrEmpty(Setting.Tools.GitExecutable) - && File.Exists(Setting.Tools.GitExecutable); - } + /// 本地化字串的Key + /// 可选格式化参数 + /// 本地化字串 + public static string Text(string key, params object[] args) { + var data = Current.FindResource($"Text.{key}") as string; + if (string.IsNullOrEmpty(data)) return $"Text.{key}"; + return string.Format(data, args); } /// - /// Load text from locales. - /// - /// - /// - public static string Text(string key) { - return Current.FindResource("Text." + key) as string; - } - - /// - /// Format text - /// - /// - /// - /// - public static string Format(string key, params object[] args) { - return string.Format(Text(key), args); - } - - /// - /// Raise error message. - /// - /// - public static void RaiseError(string msg) { - Current.Dispatcher.Invoke(() => { - (Current.MainWindow as UI.Launcher).Errors.Add(msg); - }); - } - - /// - /// Open repository. - /// - /// - public static void Open(Git.Repository repo) { - (Current.MainWindow as UI.Launcher).Open(repo); - } - - /// - /// Save settings. - /// - public static void SaveSetting() { - var settingFile = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "SourceGit", - "preference.json"); - - var dir = Path.GetDirectoryName(settingFile); - if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); - - var data = JsonSerializer.Serialize(Setting, new JsonSerializerOptions() { WriteIndented = true }); - File.WriteAllText(settingFile, data); - } - - /// - /// Startup event. + /// 启动. /// /// /// private void OnAppStartup(object sender, StartupEventArgs e) { - // Use this app as a sequence editor? - if (OpenAsEditor(e)) return; - - // Load settings. - var settingFile = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "SourceGit", - "preference.json"); - if (!File.Exists(settingFile)) { - Setting = new Preference(); - } else { - Setting = JsonSerializer.Deserialize(File.ReadAllText(settingFile)); + // 创建必要目录 + if (!Directory.Exists(Views.Controls.Avatar.CACHE_PATH)) { + Directory.CreateDirectory(Views.Controls.Avatar.CACHE_PATH); } - // Make sure avatar cache folder exists - if (!Directory.Exists(Helpers.Avatar.CACHE_PATH)) Directory.CreateDirectory(Helpers.Avatar.CACHE_PATH); - - // Try auto configure git via registry. - if (Setting == null || !IsGitConfigured) { - var root = RegistryKey.OpenBaseKey( - RegistryHive.LocalMachine, - Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); - - var git = root.OpenSubKey("SOFTWARE\\GitForWindows"); - if (git != null) { - Setting.Tools.GitExecutable = Path.Combine( - git.GetValue("InstallPath") as string, - "bin", - "git.exe"); - } - } - - // Apply themes - if (Setting.UI.UseLightTheme) { + // 控制主题 + if (Models.Preference.Instance.General.UseDarkTheme) { foreach (var rs in Current.Resources.MergedDictionaries) { - if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Themes/")) { - rs.Source = new Uri("pack://application:,,,/Resources/Themes/Light.xaml", UriKind.Absolute); + if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Themes/", StringComparison.Ordinal)) { + rs.Source = new Uri("pack://application:,,,/Resources/Themes/Dark.xaml", UriKind.Absolute); break; } } } - // Apply locales - if (Setting.UI.Locale != "en_US") { + // 控制显示语言 + var lang = Models.Preference.Instance.General.Locale; + if (lang != "en_US") { foreach (var rs in Current.Resources.MergedDictionaries) { - if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Locales/")) { - rs.Source = new Uri($"pack://application:,,,/Resources/Locales/{Setting.UI.Locale}.xaml", UriKind.Absolute); + if (rs.Source != null && rs.Source.OriginalString.StartsWith("pack://application:,,,/Resources/Locales/", StringComparison.Ordinal)) { + rs.Source = new Uri($"pack://application:,,,/Resources/Locales/{lang}.xaml", UriKind.Absolute); break; } } } - // Show main window - if (e.Args.Length == 1) { - MainWindow = new UI.Launcher(e.Args[0]); - } else { - MainWindow = new UI.Launcher(null); - } + // 主界面显示 + MainWindow = new Views.Launcher(); MainWindow.Show(); + // 如果启动命令中指定了路径,打开指定目录的仓库 + if (e.Args.Length > 0) { + var repo = Models.Preference.Instance.FindRepository(e.Args[0]); + if (repo == null) { + var path = new Commands.GetRepositoryRootPath(e.Args[0]).Result(); + if (path != null) { + var gitDir = new Commands.QueryGitDir(path).Result(); + repo = Models.Preference.Instance.AddRepository(path, gitDir, ""); + } + } - // Check for update. - if (Setting.CheckUpdate && Setting.LastCheckUpdate != DateTime.Now.DayOfYear) { - Setting.LastCheckUpdate = DateTime.Now.DayOfYear; - SaveSetting(); - Task.Run(CheckUpdate); + if (repo != null) Models.Watcher.Open(repo); + } + + // 检测更新 + if (Models.Preference.Instance.General.CheckForUpdate) { + var curDayOfYear = DateTime.Now.DayOfYear; + var lastDayOfYear = Models.Preference.Instance.General.LastCheckDay; + if (lastDayOfYear != curDayOfYear) { + Models.Preference.Instance.General.LastCheckDay = curDayOfYear; + Task.Run(() => { + try { + var web = new WebClient() { Encoding = Encoding.UTF8 }; + var raw = web.DownloadString("https://gitee.com/api/v5/repos/sourcegit/SourceGit/releases/latest"); + var ver = Models.Version.Load(raw); + var cur = Assembly.GetExecutingAssembly().GetName().Version; + + var matches = Regex.Match(ver.TagName, @"^v(\d+)\.(\d+).*"); + if (!matches.Success) return; + + var major = int.Parse(matches.Groups[1].Value); + var minor = int.Parse(matches.Groups[2].Value); + if (major > cur.Major || (major == cur.Major && minor > cur.Minor)) { + Dispatcher.Invoke(() => Views.Upgrade.Open(MainWindow, ver)); + } + } catch {} + }); + } } } /// - /// Deactivated event. + /// 后台运行 /// /// /// private void OnAppDeactivated(object sender, EventArgs e) { - GC.Collect(); - SaveSetting(); - } - - /// - /// Try to open app as git editor - /// - /// - /// - private bool OpenAsEditor(StartupEventArgs e) { - if (e.Args.Length < 3) return false; - - switch (e.Args[0]) { - case "--sequence": - var output = File.CreateText(e.Args[2]); - output.Write(File.ReadAllText(e.Args[1])); - output.Flush(); - output.Close(); - - Environment.Exit(0); - break; - default: - return false; - } - - return true; - } - - /// - /// Check for update. - /// - private void CheckUpdate() { - try { - var web = new WebClient() { Encoding = Encoding.UTF8 }; - var raw = web.DownloadString("https://gitee.com/api/v5/repos/sourcegit/SourceGit/releases/latest"); - var ver = JsonSerializer.Deserialize(raw); - var cur = Assembly.GetExecutingAssembly().GetName().Version; - - var matches = Regex.Match(ver.TagName, @"^v(\d+)\.(\d+).*"); - if (!matches.Success) return; - - var major = int.Parse(matches.Groups[1].Value); - var minor = int.Parse(matches.Groups[2].Value); - if (major > cur.Major || (major == cur.Major && minor > cur.Minor)) { - Dispatcher.Invoke(() => { - var dialog = new UI.UpdateAvailable(ver); - dialog.Owner = MainWindow; - dialog.ShowDialog(); - }); - } - } catch { - // IGNORE - } + Models.Preference.Save(); } } } diff --git a/src/Commands/Add.cs b/src/Commands/Add.cs new file mode 100644 index 00000000..fb98cf87 --- /dev/null +++ b/src/Commands/Add.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Text; + +namespace SourceGit.Commands { + /// + /// `git add`命令 + /// + public class Add : Command { + public Add(string repo) { + Cwd = repo; + Args = "add ."; + } + + public Add(string repo, List paths) { + StringBuilder builder = new StringBuilder(); + builder.Append("add --"); + foreach (var p in paths) { + builder.Append(" \""); + builder.Append(p); + builder.Append("\""); + } + + Cwd = repo; + Args = builder.ToString(); + } + } +} diff --git a/src/Commands/Apply.cs b/src/Commands/Apply.cs new file mode 100644 index 00000000..f62f113c --- /dev/null +++ b/src/Commands/Apply.cs @@ -0,0 +1,15 @@ +namespace SourceGit.Commands { + /// + /// 应用Patch + /// + public class Apply : Command { + + public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode) { + Cwd = repo; + Args = "apply "; + if (ignoreWhitespace) Args += "--ignore-whitespace "; + else Args += $"--whitespace={whitespaceMode} "; + Args += $"\"{file}\""; + } + } +} diff --git a/src/Commands/Blame.cs b/src/Commands/Blame.cs new file mode 100644 index 00000000..eede5927 --- /dev/null +++ b/src/Commands/Blame.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 逐行追溯 + /// + public class Blame : Command { + private static readonly Regex REG_FORMAT = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)"); + private Data data = new Data(); + + public class Data { + public List Lines = new List(); + public bool IsBinary = false; + } + + public Blame(string repo, string file, string revision) { + Cwd = repo; + Args = $"blame -t {revision} -- \"{file}\""; + } + + public Data Result() { + Exec(); + return data; + } + + public override void OnReadline(string line) { + if (data.IsBinary) return; + if (string.IsNullOrEmpty(line)) return; + + if (line.IndexOf('\0') >= 0) { + data.IsBinary = true; + data.Lines.Clear(); + return; + } + + var match = REG_FORMAT.Match(line); + if (!match.Success) return; + + var commit = match.Groups[1].Value; + var author = match.Groups[2].Value; + var timestamp = int.Parse(match.Groups[3].Value); + var content = match.Groups[4].Value; + var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); + + var blameLine = new Models.BlameLine() { + LineNumber = $"{data.Lines.Count+1}", + CommitSHA = commit, + Author = author, + Time = when, + Content = content, + }; + + data.Lines.Add(blameLine); + } + } +} diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs new file mode 100644 index 00000000..7f640c4f --- /dev/null +++ b/src/Commands/Branch.cs @@ -0,0 +1,33 @@ +namespace SourceGit.Commands { + /// + /// 分支相关操作 + /// + class Branch : Command { + private string target = null; + + public Branch(string repo, string branch) { + Cwd = repo; + target = branch; + } + + public void Create(string basedOn) { + Args = $"branch {target} {basedOn}"; + Exec(); + } + + public void Rename(string to) { + Args = $"branch -M {target} {to}"; + Exec(); + } + + public void SetUpstream(string upstream) { + Args = $"branch {target} -u {upstream}"; + Exec(); + } + + public void Delete() { + Args = $"branch -D {target}"; + Exec(); + } + } +} diff --git a/src/Commands/Branches.cs b/src/Commands/Branches.cs new file mode 100644 index 00000000..687bb151 --- /dev/null +++ b/src/Commands/Branches.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 解析所有的分支 + /// + public class Branches : Command { + private static readonly string PREFIX_LOCAL = "refs/heads/"; + private static readonly string PREFIX_REMOTE = "refs/remotes/"; + private static readonly string CMD = "branch -l --all -v --format=\"$%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)$%(contents:subject)\""; + private static readonly Regex REG_FORMAT = new Regex(@"\$(.*)\$(.*)\$([\* ])\$(.*)\$(.*?)\$(.*)"); + private static readonly Regex REG_AHEAD = new Regex(@"ahead (\d+)"); + private static readonly Regex REG_BEHIND = new Regex(@"behind (\d+)"); + + private List loaded = new List(); + + public Branches(string path) { + Cwd = path; + Args = CMD; + } + + public List Result() { + Exec(); + return loaded; + } + + public override void OnReadline(string line) { + var match = REG_FORMAT.Match(line); + if (!match.Success) return; + + var branch = new Models.Branch(); + var refName = match.Groups[1].Value; + if (refName.EndsWith("/HEAD")) return; + + if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) { + branch.Name = refName.Substring(PREFIX_LOCAL.Length); + branch.IsLocal = true; + } else if (refName.StartsWith(PREFIX_REMOTE, StringComparison.Ordinal)) { + var name = refName.Substring(PREFIX_REMOTE.Length); + branch.Remote = name.Substring(0, name.IndexOf('/')); + branch.Name = name.Substring(branch.Remote.Length + 1); + branch.IsLocal = false; + } else { + branch.Name = refName; + branch.IsLocal = true; + } + + branch.FullName = refName; + branch.Head = match.Groups[2].Value; + branch.IsCurrent = match.Groups[3].Value == "*"; + branch.Upstream = match.Groups[4].Value; + branch.UpstreamTrackStatus = ParseTrackStatus(match.Groups[5].Value); + branch.HeadSubject = match.Groups[6].Value; + + loaded.Add(branch); + } + + private string ParseTrackStatus(string data) { + if (string.IsNullOrEmpty(data)) return ""; + + string track = ""; + + var ahead = REG_AHEAD.Match(data); + if (ahead.Success) { + track += ahead.Groups[1].Value + "↑ "; + } + + var behind = REG_BEHIND.Match(data); + if (behind.Success) { + track += behind.Groups[1].Value + "↓"; + } + + return track.Trim(); + } + } +} diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs new file mode 100644 index 00000000..f3d976da --- /dev/null +++ b/src/Commands/Checkout.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Text; + +namespace SourceGit.Commands { + /// + /// 检出 + /// + public class Checkout : Command { + + public Checkout(string repo) { + Cwd = repo; + } + + public bool Branch(string branch) { + Args = $"checkout {branch}"; + return Exec(); + } + + public bool Branch(string branch, string basedOn) { + Args = $"checkout -b {branch} {basedOn}"; + return Exec(); + } + + public bool File(string file, bool useTheirs) { + if (useTheirs) { + Args = $"checkout --theirs -- \"{file}\""; + } else { + Args = $"checkout --ours -- \"{file}\""; + } + + return Exec(); + } + + public bool Files(List files) { + StringBuilder builder = new StringBuilder(); + builder.Append("checkout -qf --"); + foreach (var f in files) { + builder.Append(" \""); + builder.Append(f); + builder.Append("\""); + } + Args = builder.ToString(); + return Exec(); + } + } +} diff --git a/src/Commands/CherryPick.cs b/src/Commands/CherryPick.cs new file mode 100644 index 00000000..ca939e76 --- /dev/null +++ b/src/Commands/CherryPick.cs @@ -0,0 +1,13 @@ +namespace SourceGit.Commands { + /// + /// 遴选命令 + /// + public class CherryPick : Command { + + public CherryPick(string repo, string commit, bool noCommit) { + var mode = noCommit ? "-n" : "--ff"; + Cwd = repo; + Args = $"cherry-pick {mode} {commit}"; + } + } +} diff --git a/src/Commands/Clean.cs b/src/Commands/Clean.cs new file mode 100644 index 00000000..83de46cd --- /dev/null +++ b/src/Commands/Clean.cs @@ -0,0 +1,12 @@ +namespace SourceGit.Commands { + /// + /// 清理指令 + /// + public class Clean : Command { + + public Clean(string repo) { + Cwd = repo; + Args = "clean -qfd"; + } + } +} diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs new file mode 100644 index 00000000..d4b3f2fc --- /dev/null +++ b/src/Commands/Clone.cs @@ -0,0 +1,26 @@ +using System; + +namespace SourceGit.Commands { + + /// + /// 克隆 + /// + public class Clone : Command { + private Action handler = null; + + public Clone(string path, string url, string localName, string extraArgs, Action outputHandler) { + Cwd = path; + TraitErrorAsOutput = true; + Args = "-c credential.helper=manager clone --progress --verbose --recurse-submodules "; + handler = outputHandler; + + if (!string.IsNullOrEmpty(extraArgs)) Args += $"{extraArgs} "; + Args += $"{url} "; + if (!string.IsNullOrEmpty(localName)) Args += localName; + } + + public override void OnReadline(string line) { + handler?.Invoke(line); + } + } +} diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs new file mode 100644 index 00000000..adf87876 --- /dev/null +++ b/src/Commands/Command.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + + /// + /// 取消命令执行的对象 + /// + public class Cancellable { + public bool IsCancelRequested { get; set; } = false; + } + + /// + /// 命令接口 + /// + public class Command { + + /// + /// 读取全部输出时的结果 + /// + public class ReadToEndResult { + public bool IsSuccess { get; set; } + public string Output { get; set; } + public string Error { get; set; } + } + + /// + /// 运行路径 + /// + public string Cwd { get; set; } = ""; + + /// + /// 参数 + /// + public string Args { get; set; } = ""; + + /// + /// 使用标准错误输出 + /// + public bool TraitErrorAsOutput { get; set; } = false; + + /// + /// 用于取消命令指行的Token + /// + public Cancellable Token { get; set; } = null; + + /// + /// 运行 + /// + public bool Exec() { + var start = new ProcessStartInfo(); + start.FileName = Models.Preference.Instance.Git.Path; + start.Arguments = "--no-pager -c core.quotepath=off " + Args; + start.UseShellExecute = false; + start.CreateNoWindow = true; + start.RedirectStandardOutput = true; + start.RedirectStandardError = true; + start.StandardOutputEncoding = Encoding.UTF8; + start.StandardErrorEncoding = Encoding.UTF8; + + if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd; + + var progressFilter = new Regex(@"\d+\%"); + var errs = new List(); + var proc = new Process() { StartInfo = start }; + var isCancelled = false; + + proc.OutputDataReceived += (o, e) => { + if (Token != null && Token.IsCancelRequested) { + isCancelled = true; + proc.CancelErrorRead(); + proc.CancelOutputRead(); +#if NET48 + proc.Kill(); +#else + proc.Kill(true); +#endif + return; + } + + if (e.Data == null) return; + OnReadline(e.Data); + }; + proc.ErrorDataReceived += (o, e) => { + if (Token != null && Token.IsCancelRequested) { + isCancelled = true; + proc.CancelErrorRead(); + proc.CancelOutputRead(); +#if NET48 + proc.Kill(); +#else + proc.Kill(true); +#endif + return; + } + + if (e.Data == null) return; + if (TraitErrorAsOutput) OnReadline(e.Data); + + if (string.IsNullOrEmpty(e.Data)) return; + if (progressFilter.IsMatch(e.Data)) return; + if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return; + errs.Add(e.Data); + }; + + proc.Start(); + proc.BeginOutputReadLine(); + proc.BeginErrorReadLine(); + proc.WaitForExit(); + + int exitCode = proc.ExitCode; + proc.Close(); + + if (!isCancelled && exitCode != 0 && errs.Count > 0) { + Models.Exception.Raise(string.Join("\n", errs)); + return false; + } else { + return true; + } + } + + /// + /// 直接读取全部标准输出 + /// + public ReadToEndResult ReadToEnd() { + var start = new ProcessStartInfo(); + start.FileName = Models.Preference.Instance.Git.Path; + start.Arguments = "--no-pager -c core.quotepath=off " + Args; + start.UseShellExecute = false; + start.CreateNoWindow = true; + start.RedirectStandardOutput = true; + start.RedirectStandardError = true; + start.StandardOutputEncoding = Encoding.UTF8; + start.StandardErrorEncoding = Encoding.UTF8; + + if (!string.IsNullOrEmpty(Cwd)) start.WorkingDirectory = Cwd; + + var proc = new Process() { StartInfo = start }; + proc.Start(); + + var rs = new ReadToEndResult(); + rs.Output = proc.StandardOutput.ReadToEnd(); + rs.Error = proc.StandardError.ReadToEnd(); + + proc.WaitForExit(); + rs.IsSuccess = proc.ExitCode == 0; + proc.Close(); + + return rs; + } + + /// + /// 调用Exec时的读取函数 + /// + /// + public virtual void OnReadline(string line) {} + } +} diff --git a/src/Commands/Commit.cs b/src/Commands/Commit.cs new file mode 100644 index 00000000..4a5f2471 --- /dev/null +++ b/src/Commands/Commit.cs @@ -0,0 +1,19 @@ +using System.IO; + +namespace SourceGit.Commands { + /// + /// `git commit`命令 + /// + public class Commit : Command { + private string msg = null; + + public Commit(string repo, string message, bool amend) { + msg = Path.GetTempFileName(); + File.WriteAllText(msg, message); + + Cwd = repo; + Args = $"commit --file=\"{msg}\""; + if (amend) Args += " --amend --no-edit"; + } + } +} diff --git a/src/Commands/CommitChanges.cs b/src/Commands/CommitChanges.cs new file mode 100644 index 00000000..defcbff2 --- /dev/null +++ b/src/Commands/CommitChanges.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + + /// + /// 取得一个提交的变更列表 + /// + public class CommitChanges : Command { + private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); + private List changes = new List(); + + public CommitChanges(string cwd, string commit) { + Cwd = cwd; + Args = $"show --name-status {commit}"; + } + + public List Result() { + Exec(); + return changes; + } + + public override void OnReadline(string line) { + var match = REG_FORMAT.Match(line); + if (!match.Success) return; + + var change = new Models.Change() { Path = match.Groups[2].Value }; + var status = match.Groups[1].Value; + + switch (status[0]) { + case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break; + case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break; + case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break; + case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break; + case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break; + } + } + } +} diff --git a/src/Commands/CommitRangeChanges.cs b/src/Commands/CommitRangeChanges.cs new file mode 100644 index 00000000..05cc778e --- /dev/null +++ b/src/Commands/CommitRangeChanges.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + + /// + /// 对比两个提交间的变更 + /// + public class CommitRangeChanges : Command { + private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); + private List changes = new List(); + + public CommitRangeChanges(string cwd, string start, string end) { + Cwd = cwd; + Args = $"diff --name-status {start} {end}"; + } + + public List Result() { + Exec(); + return changes; + } + + public override void OnReadline(string line) { + var match = REG_FORMAT.Match(line); + if (!match.Success) return; + + var change = new Models.Change() { Path = match.Groups[2].Value }; + var status = match.Groups[1].Value; + + switch (status[0]) { + case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break; + case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break; + case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break; + case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break; + case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break; + } + } + } +} diff --git a/src/Commands/Commits.cs b/src/Commands/Commits.cs new file mode 100644 index 00000000..e5839e5c --- /dev/null +++ b/src/Commands/Commits.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SourceGit.Commands { + + /// + /// 取得提交列表 + /// + public class Commits : Command { + private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; + private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----"; + + private List commits = new List(); + private Models.Commit current = null; + private bool isSkipingGpgsig = false; + private bool isHeadFounded = false; + private bool findFirstMerged = true; + + public Commits(string path, string limits, bool needFindHead = true) { + Cwd = path; + Args = "log --date-order --decorate=full --pretty=raw " + limits; + findFirstMerged = needFindHead; + } + + public List Result() { + Exec(); + + if (current != null) { + current.Message = current.Message.Trim(); + commits.Add(current); + } + + if (findFirstMerged && !isHeadFounded && commits.Count > 0) { + MarkFirstMerged(); + } + + return commits; + } + + public override void OnReadline(string line) { + if (isSkipingGpgsig) { + if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) isSkipingGpgsig = false; + return; + } else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) { + isSkipingGpgsig = true; + return; + } + + if (line.StartsWith("commit ", StringComparison.Ordinal)) { + if (current != null) { + current.Message = current.Message.Trim(); + commits.Add(current); + } + + current = new Models.Commit(); + line = line.Substring(7); + + var decoratorStart = line.IndexOf('('); + if (decoratorStart < 0) { + current.SHA = line.Trim(); + } else { + current.SHA = line.Substring(0, decoratorStart).Trim(); + current.IsMerged = ParseDecorators(current.Decorators, line.Substring(decoratorStart + 1)); + if (!isHeadFounded) isHeadFounded = current.IsMerged; + } + + return; + } + + if (current == null) return; + + if (line.StartsWith("tree ", StringComparison.Ordinal)) { + return; + } else if (line.StartsWith("parent ", StringComparison.Ordinal)) { + current.Parents.Add(line.Substring("parent ".Length)); + } else if (line.StartsWith("author ", StringComparison.Ordinal)) { + current.Author.Parse(line); + } else if (line.StartsWith("committer ", StringComparison.Ordinal)) { + current.Committer.Parse(line); + } else if (string.IsNullOrEmpty(current.Subject)) { + current.Subject = line.Trim(); + } else { + current.Message += (line.Trim() + "\n"); + } + } + + private bool ParseDecorators(List decorators, string data) { + bool isHeadOfCurrent = false; + + var subs = data.Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var sub in subs) { + var d = sub.Trim(); + if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) { + decorators.Add(new Models.Decorator() { + Type = Models.DecoratorType.Tag, + Name = d.Substring(15).Trim(), + }); + } else if (d.EndsWith("/HEAD", StringComparison.Ordinal)) { + continue; + } else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) { + isHeadOfCurrent = true; + decorators.Add(new Models.Decorator() { + Type = Models.DecoratorType.CurrentBranchHead, + Name = d.Substring(19).Trim(), + }); + } else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) { + decorators.Add(new Models.Decorator() { + Type = Models.DecoratorType.LocalBranchHead, + Name = d.Substring(11).Trim(), + }); + } else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) { + decorators.Add(new Models.Decorator() { + Type = Models.DecoratorType.RemoteBranchHead, + Name = d.Substring(13).Trim(), + }); + } + } + + return isHeadOfCurrent; + } + + private void MarkFirstMerged() { + Args = $"log --since=\"{commits.Last().Committer.Time}\" --min-parents=2 --format=\"%H\""; + + var rs = ReadToEnd(); + var shas = rs.Output.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + if (shas.Length == 0) return; + + var merges = commits.Where(x => x.Parents.Count > 1).ToList(); + foreach (var sha in shas) { + var c = merges.Find(x => x.SHA == sha); + if (c != null) { + c.IsMerged = true; + return; + } + } + } + } +} diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs new file mode 100644 index 00000000..7b99a99d --- /dev/null +++ b/src/Commands/Config.cs @@ -0,0 +1,37 @@ +namespace SourceGit.Commands { + /// + /// config命令 + /// + public class Config : Command { + + public Config() { + } + + public Config(string repo) { + Cwd = repo; + } + + public string Get(string key) { + Args = $"config {key}"; + return ReadToEnd().Output.Trim(); + } + + public bool Set(string key, string val, bool allowEmpty = false) { + if (!allowEmpty && string.IsNullOrEmpty(val)) { + if (string.IsNullOrEmpty(Cwd)) { + Args = $"config --global --unset {key}"; + } else { + Args = $"config --unset {key}"; + } + } else { + if (string.IsNullOrEmpty(Cwd)) { + Args = $"config --global {key} \"{val}\""; + } else { + Args = $"config {key} \"{val}\""; + } + } + + return Exec(); + } + } +} diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs new file mode 100644 index 00000000..6679279f --- /dev/null +++ b/src/Commands/Diff.cs @@ -0,0 +1,61 @@ +using System; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// Diff命令(用于文件文件比对) + /// + public class Diff : Command { + private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@"); + private Models.TextChanges changes = new Models.TextChanges(); + private int oldLine = 0; + private int newLine = 0; + + public Diff(string repo, string args) { + Cwd = repo; + Args = $"diff --ignore-cr-at-eol {args}"; + } + + public Models.TextChanges Result() { + Exec(); + if (changes.IsBinary) changes.Lines.Clear(); + return changes; + } + + public override void OnReadline(string line) { + if (changes.IsBinary) return; + + if (changes.Lines.Count == 0) { + var match = REG_INDICATOR.Match(line); + if (!match.Success) { + if (line.StartsWith("Binary", StringComparison.Ordinal)) changes.IsBinary = true; + return; + } + + oldLine = int.Parse(match.Groups[1].Value); + newLine = int.Parse(match.Groups[2].Value); + changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Indicator, line, "", "")); + } else { + var ch = line[0]; + if (ch == '-') { + changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Deleted, line.Substring(1), $"{oldLine}", "")); + oldLine++; + } else if (ch == '+') { + changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Added, line.Substring(1), "", $"{newLine}")); + newLine++; + } else if (ch != '\\') { + var match = REG_INDICATOR.Match(line); + if (match.Success) { + oldLine = int.Parse(match.Groups[1].Value); + newLine = int.Parse(match.Groups[2].Value); + changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Indicator, line, "", "")); + } else { + changes.Lines.Add(new Models.TextChanges.Line(Models.TextChanges.LineMode.Normal, line.Substring(1), $"{oldLine}", $"{newLine}")); + oldLine++; + newLine++; + } + } + } + } + } +} diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs new file mode 100644 index 00000000..42b4005e --- /dev/null +++ b/src/Commands/Discard.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace SourceGit.Commands { + /// + /// 忽略变更 + /// + public class Discard { + private string repo = null; + private List files = new List(); + + public Discard(string repo, List changes) { + this.repo = repo; + + if (changes != null && changes.Count > 0) { + foreach (var c in changes) { + if (c.WorkTree == Models.Change.Status.Untracked || c.WorkTree == Models.Change.Status.Added) continue; + files.Add(c.Path); + } + } + } + + public bool Exec() { + if (files.Count == 0) { + new Reset(repo, "HEAD", "--hard").Exec(); + } else { + for (int i = 0; i < files.Count; i += 10) { + var count = Math.Min(10, files.Count - i); + new Checkout(repo).Files(files.GetRange(i, count)); + } + } + + new Clean(repo).Exec(); + return true; + } + } +} diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs new file mode 100644 index 00000000..2eeeacee --- /dev/null +++ b/src/Commands/Fetch.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace SourceGit.Commands { + + /// + /// 拉取 + /// + public class Fetch : Command { + private Action handler = null; + + public Fetch(string repo, string remote, bool prune, Action outputHandler) { + Cwd = repo; + TraitErrorAsOutput = true; + Args = "-c credential.helper=manager fetch --progress --verbose "; + if (prune) Args += "--prune "; + Args += remote; + handler = outputHandler; + AutoFetch.MarkFetched(repo); + } + + public override void OnReadline(string line) { + handler?.Invoke(line); + } + } + + /// + /// 自动拉取(每隔10分钟) + /// + public class AutoFetch { + private static Dictionary jobs = new Dictionary(); + + private Fetch cmd = null; + private long nextFetchPoint = 0; + private Timer timer = null; + + public static void Start(string repo) { + if (!Models.Preference.Instance.General.AutoFetchRemotes) return; + + // 只自动更新加入管理列表中的仓库(子模块等不自动更新) + var exists = Models.Preference.Instance.FindRepository(repo); + if (exists == null) return; + + var job = new AutoFetch(repo); + jobs.Add(repo, job); + } + + public static void MarkFetched(string repo) { + if (!jobs.ContainsKey(repo)) return; + jobs[repo].nextFetchPoint = DateTime.Now.AddMinutes(1).ToFileTime(); + } + + public static void Stop(string repo) { + if (!jobs.ContainsKey(repo)) return; + + jobs[repo].timer.Dispose(); + jobs.Remove(repo); + } + + public AutoFetch(string repo) { + cmd = new Fetch(repo, "--all", true, null); + nextFetchPoint = DateTime.Now.AddMinutes(1).ToFileTime(); + timer = new Timer(OnTick, null, 60000, 10000); + } + + private void OnTick(object o) { + var now = DateTime.Now.ToFileTime(); + if (nextFetchPoint > now) return; + + Models.Watcher.SetEnabled(cmd.Cwd, false); + cmd.Exec(); + nextFetchPoint = DateTime.Now.AddMinutes(1).ToFileTime(); + Models.Watcher.SetEnabled(cmd.Cwd, true); + } + } +} diff --git a/src/Commands/FormatPatch.cs b/src/Commands/FormatPatch.cs new file mode 100644 index 00000000..af5fb624 --- /dev/null +++ b/src/Commands/FormatPatch.cs @@ -0,0 +1,12 @@ +namespace SourceGit.Commands { + /// + /// 将Commit另存为Patch文件 + /// + public class FormatPatch : Command { + + public FormatPatch(string repo, string commit, string path) { + Cwd = repo; + Args = $"format-patch {commit} -1 -o \"{path}\""; + } + } +} diff --git a/src/Commands/GetRepositoryRootPath.cs b/src/Commands/GetRepositoryRootPath.cs new file mode 100644 index 00000000..c4dc6777 --- /dev/null +++ b/src/Commands/GetRepositoryRootPath.cs @@ -0,0 +1,17 @@ +namespace SourceGit.Commands { + /// + /// 取得一个库的根路径 + /// + public class GetRepositoryRootPath : Command { + public GetRepositoryRootPath(string path) { + Cwd = path; + Args = "rev-parse --show-toplevel"; + } + + public string Result() { + var rs = ReadToEnd().Output; + if (string.IsNullOrEmpty(rs)) return null; + return rs.Trim(); + } + } +} diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs new file mode 100644 index 00000000..b33b623c --- /dev/null +++ b/src/Commands/GitFlow.cs @@ -0,0 +1,71 @@ +namespace SourceGit.Commands { + /// + /// Git-Flow命令 + /// + public class GitFlow : Command { + + public GitFlow(string repo) { + Cwd = repo; + } + + public bool Init(string master, string develop, string feature, string release, string hotfix, string version) { + var branches = new Branches(Cwd).Result(); + var current = branches.Find(x => x.IsCurrent); + + var masterBranch = branches.Find(x => x.Name == master); + if (masterBranch == null) new Branch(Cwd, master).Create(current.Head); + + var devBranch = branches.Find(x => x.Name == develop); + if (devBranch == null) new Branch(Cwd, develop).Create(current.Head); + + var cmd = new Config(Cwd); + cmd.Set("gitflow.branch.master", master); + cmd.Set("gitflow.branch.develop", develop); + cmd.Set("gitflow.prefix.feature", feature); + cmd.Set("gitflow.prefix.bugfix", "bugfix/"); + cmd.Set("gitflow.prefix.release", release); + cmd.Set("gitflow.prefix.hotfix", hotfix); + cmd.Set("gitflow.prefix.support", "support/"); + cmd.Set("gitflow.prefix.versiontag", version, true); + + Args = "flow init -d"; + return Exec(); + } + + public void Start(Models.GitFlowBranchType type, string name) { + switch (type) { + case Models.GitFlowBranchType.Feature: + Args = $"flow feature start {name}"; + break; + case Models.GitFlowBranchType.Release: + Args = $"flow release start {name}"; + break; + case Models.GitFlowBranchType.Hotfix: + Args = $"flow hotfix start {name}"; + break; + default: + return; + } + + Exec(); + } + + public void Finish(Models.GitFlowBranchType type, string name) { + switch (type) { + case Models.GitFlowBranchType.Feature: + Args = $"flow feature finish {name}"; + break; + case Models.GitFlowBranchType.Release: + Args = $"flow release finish {name}"; + break; + case Models.GitFlowBranchType.Hotfix: + Args = $"flow hotfix finish {name}"; + break; + default: + return; + } + + Exec(); + } + } +} diff --git a/src/Commands/Init.cs b/src/Commands/Init.cs new file mode 100644 index 00000000..35dde5a2 --- /dev/null +++ b/src/Commands/Init.cs @@ -0,0 +1,13 @@ +namespace SourceGit.Commands { + + /// + /// 初始化Git仓库 + /// + public class Init : Command { + + public Init(string workDir) { + Cwd = workDir; + Args = "init -q"; + } + } +} diff --git a/src/Commands/IsBinaryFile.cs b/src/Commands/IsBinaryFile.cs new file mode 100644 index 00000000..68cbff62 --- /dev/null +++ b/src/Commands/IsBinaryFile.cs @@ -0,0 +1,18 @@ +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 查询指定版本下的某文件是否是二进制文件 + /// + public class IsBinaryFile : Command { + private static readonly Regex REG_TEST = new Regex(@"^\-\s+\-\s+.*$"); + public IsBinaryFile(string repo, string commit, string path) { + Cwd = repo; + Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\""; + } + + public bool Result() { + return REG_TEST.IsMatch(ReadToEnd().Output); + } + } +} diff --git a/src/Commands/IsLFSFiltered.cs b/src/Commands/IsLFSFiltered.cs new file mode 100644 index 00000000..65a4f1f1 --- /dev/null +++ b/src/Commands/IsLFSFiltered.cs @@ -0,0 +1,16 @@ +namespace SourceGit.Commands { + /// + /// 检测目录是否被LFS管理 + /// + public class IsLFSFiltered : Command { + public IsLFSFiltered(string cwd, string path) { + Cwd = cwd; + Args = $"check-attr -a -z \"{path}\""; + } + + public bool Result() { + var rs = ReadToEnd(); + return rs.Output.Contains("filter\0lfs"); + } + } +} diff --git a/src/Commands/LocalChanges.cs b/src/Commands/LocalChanges.cs new file mode 100644 index 00000000..68f3e8f8 --- /dev/null +++ b/src/Commands/LocalChanges.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 取得本地工作副本变更 + /// + public class LocalChanges : Command { + private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); + private List changes = new List(); + + public LocalChanges(string path) { + Cwd = path; + Args = "status -uall --ignore-submodules=dirty --porcelain"; + } + + public List Result() { + Exec(); + return changes; + } + + public override void OnReadline(string line) { + var match = REG_FORMAT.Match(line); + if (!match.Success) return; + + var change = new Models.Change() { Path = match.Groups[2].Value }; + var status = match.Groups[1].Value; + + switch (status) { + case " M": change.Set(Models.Change.Status.None, Models.Change.Status.Modified); break; + case " A": change.Set(Models.Change.Status.None, Models.Change.Status.Added); break; + case " D": change.Set(Models.Change.Status.None, Models.Change.Status.Deleted); break; + case " R": change.Set(Models.Change.Status.None, Models.Change.Status.Renamed); break; + case " C": change.Set(Models.Change.Status.None, Models.Change.Status.Copied); break; + case "M": change.Set(Models.Change.Status.Modified, Models.Change.Status.None); break; + case "MM": change.Set(Models.Change.Status.Modified, Models.Change.Status.Modified); break; + case "MD": change.Set(Models.Change.Status.Modified, Models.Change.Status.Deleted); break; + case "A": change.Set(Models.Change.Status.Added, Models.Change.Status.None); break; + case "AM": change.Set(Models.Change.Status.Added, Models.Change.Status.Modified); break; + case "AD": change.Set(Models.Change.Status.Added, Models.Change.Status.Deleted); break; + case "D": change.Set(Models.Change.Status.Deleted, Models.Change.Status.None); break; + case "R": change.Set(Models.Change.Status.Renamed, Models.Change.Status.None); break; + case "RM": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Modified); break; + case "RD": change.Set(Models.Change.Status.Renamed, Models.Change.Status.Deleted); break; + case "C": change.Set(Models.Change.Status.Copied, Models.Change.Status.None); break; + case "CM": change.Set(Models.Change.Status.Copied, Models.Change.Status.Modified); break; + case "CD": change.Set(Models.Change.Status.Copied, Models.Change.Status.Deleted); break; + case "DR": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Renamed); break; + case "DC": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Copied); break; + case "DD": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Deleted); break; + case "AU": change.Set(Models.Change.Status.Added, Models.Change.Status.Unmerged); break; + case "UD": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Deleted); break; + case "UA": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Added); break; + case "DU": change.Set(Models.Change.Status.Deleted, Models.Change.Status.Unmerged); break; + case "AA": change.Set(Models.Change.Status.Added, Models.Change.Status.Added); break; + case "UU": change.Set(Models.Change.Status.Unmerged, Models.Change.Status.Unmerged); break; + case "??": change.Set(Models.Change.Status.Untracked, Models.Change.Status.Untracked); break; + default: return; + } + + changes.Add(change); + } + } +} diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs new file mode 100644 index 00000000..ee827390 --- /dev/null +++ b/src/Commands/Merge.cs @@ -0,0 +1,12 @@ +namespace SourceGit.Commands { + /// + /// 合并分支 + /// + public class Merge : Command { + + public Merge(string repo, string source, string mode) { + Cwd = repo; + Args = $"merge {source} {mode}"; + } + } +} diff --git a/src/Commands/Pull.cs b/src/Commands/Pull.cs new file mode 100644 index 00000000..99eaa86b --- /dev/null +++ b/src/Commands/Pull.cs @@ -0,0 +1,48 @@ +using System; + +namespace SourceGit.Commands { + + /// + /// 拉回 + /// + public class Pull : Command { + private Action handler = null; + private bool needStash = false; + + public Pull(string repo, string remote, string branch, bool useRebase, bool autoStash, Action onProgress) { + Cwd = repo; + Args = "-c credential.helper=manager pull --verbose --progress --tags "; + TraitErrorAsOutput = true; + handler = onProgress; + + if (useRebase) Args += "--rebase "; + if (autoStash) { + if (useRebase) Args += "--autostash "; + else needStash = true; + } + + Args += $"{remote} {branch}"; + } + + public bool Run() { + if (needStash) { + var changes = new LocalChanges(Cwd).Result(); + if (changes.Count > 0) { + if (!new Stash(Cwd).Push(null, "PULL_AUTO_STASH", true)) { + return false; + } + } else { + needStash = false; + } + } + + var succ = Exec(); + if (needStash) new Stash(Cwd).Pop("stash@{0}"); + return succ; + } + + public override void OnReadline(string line) { + handler?.Invoke(line); + } + } +} diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs new file mode 100644 index 00000000..2beafcb7 --- /dev/null +++ b/src/Commands/Push.cs @@ -0,0 +1,39 @@ +using System; + +namespace SourceGit.Commands { + /// + /// 推送 + /// + public class Push : Command { + private Action handler = null; + + public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool force, bool track, Action onProgress) { + Cwd = repo; + TraitErrorAsOutput = true; + handler = onProgress; + Args = "-c credential.helper=manager push --progress --verbose "; + + if (withTags) Args += "--tags "; + if (track) Args += "-u "; + if (force) Args += "--force-with-lease "; + + Args += $"{remote} {local}:{remoteBranch}"; + } + + public Push(string repo, string remote, string branch) { + Cwd = repo; + Args = $"-c credential.helper=manager push {remote} --delete {branch}"; + } + + public Push(string repo, string remote, string tag, bool isDelete) { + Cwd = repo; + Args = $"-c credential.helper=manager push "; + if (isDelete) Args += "--delete "; + Args += $"{remote} refs/tags/{tag}"; + } + + public override void OnReadline(string line) { + handler?.Invoke(line); + } + } +} diff --git a/src/Commands/QueryFileContent.cs b/src/Commands/QueryFileContent.cs new file mode 100644 index 00000000..04eab006 --- /dev/null +++ b/src/Commands/QueryFileContent.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace SourceGit.Commands { + /// + /// 取得指定提交下的某文件内容 + /// + public class QueryFileContent : Command { + private List lines = new List(); + private int added = 0; + + public QueryFileContent(string repo, string commit, string path) { + Cwd = repo; + Args = $"show {commit}:\"{path}\""; + } + + public List Result() { + Exec(); + return lines; + } + + public override void OnReadline(string line) { + added++; + lines.Add(new Models.TextLine() { Number = added, Data = line }); + } + } +} diff --git a/src/Commands/QueryFileSizeChange.cs b/src/Commands/QueryFileSizeChange.cs new file mode 100644 index 00000000..f17b2fd4 --- /dev/null +++ b/src/Commands/QueryFileSizeChange.cs @@ -0,0 +1,50 @@ +using System.IO; + +namespace SourceGit.Commands { + /// + /// 查询文件大小变化 + /// + public class QueryFileSizeChange { + + class QuerySizeCmd : Command { + public QuerySizeCmd(string repo, string path, string revision) { + Cwd = repo; + Args = $"cat-file -s {revision}:\"{path}\""; + } + + public long Result() { + string data = ReadToEnd().Output; + long size; + if (!long.TryParse(data, out size)) size = 0; + return size; + } + } + + private Models.FileSizeChange change = new Models.FileSizeChange(); + + public QueryFileSizeChange(string repo, string[] revisions, string path, string orgPath) { + if (revisions.Length == 0) { + change.NewSize = new FileInfo(Path.Combine(repo, path)).Length; + change.OldSize = new QuerySizeCmd(repo, path, "HEAD").Result(); + } else if (revisions.Length == 1) { + change.NewSize = new QuerySizeCmd(repo, path, "HEAD").Result(); + if (string.IsNullOrEmpty(orgPath)) { + change.OldSize = new QuerySizeCmd(repo, path, revisions[0]).Result(); + } else { + change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[0]).Result(); + } + } else { + change.NewSize = new QuerySizeCmd(repo, path, revisions[0]).Result(); + if (string.IsNullOrEmpty(orgPath)) { + change.OldSize = new QuerySizeCmd(repo, path, revisions[1]).Result(); + } else { + change.OldSize = new QuerySizeCmd(repo, orgPath, revisions[1]).Result(); + } + } + } + + public Models.FileSizeChange Result() { + return change; + } + } +} diff --git a/src/Commands/QueryGitDir.cs b/src/Commands/QueryGitDir.cs new file mode 100644 index 00000000..dd421a21 --- /dev/null +++ b/src/Commands/QueryGitDir.cs @@ -0,0 +1,23 @@ +using System.IO; + +namespace SourceGit.Commands { + + /// + /// 取得GitDir + /// + public class QueryGitDir : Command { + public QueryGitDir(string workDir) { + Cwd = workDir; + Args = "rev-parse --git-dir"; + } + + public string Result() { + var rs = ReadToEnd().Output; + if (string.IsNullOrEmpty(rs)) return null; + + rs = rs.Trim(); + if (Path.IsPathRooted(rs)) return rs; + return Path.Combine(Cwd, rs); + } + } +} diff --git a/src/Commands/QueryLFSObject.cs b/src/Commands/QueryLFSObject.cs new file mode 100644 index 00000000..8db8bbe0 --- /dev/null +++ b/src/Commands/QueryLFSObject.cs @@ -0,0 +1,28 @@ +using System; + +namespace SourceGit.Commands { + /// + /// 取得一个LFS对象的信息 + /// + public class QueryLFSObject : Command { + private Models.LFSObject obj = new Models.LFSObject(); + + public QueryLFSObject(string repo, string commit, string path) { + Cwd = repo; + Args = $"show {commit}:\"{path}\""; + } + + public Models.LFSObject Result() { + Exec(); + return obj; + } + + public override void OnReadline(string line) { + if (line.StartsWith("oid sha256:", StringComparison.Ordinal)) { + obj.OID = line.Substring(11).Trim(); + } else if (line.StartsWith("size")) { + obj.Size = int.Parse(line.Substring(4).Trim()); + } + } + } +} diff --git a/src/Commands/QueryLFSObjectChange.cs b/src/Commands/QueryLFSObjectChange.cs new file mode 100644 index 00000000..5f9e1631 --- /dev/null +++ b/src/Commands/QueryLFSObjectChange.cs @@ -0,0 +1,41 @@ +namespace SourceGit.Commands { + /// + /// 查询LFS对象变更 + /// + public class QueryLFSObjectChange : Command { + private Models.LFSChange change = new Models.LFSChange(); + + public QueryLFSObjectChange(string repo, string args) { + Cwd = repo; + Args = $"diff --ignore-cr-at-eol {args}"; + } + + public Models.LFSChange Result() { + Exec(); + return change; + } + + public override void OnReadline(string line) { + var ch = line[0]; + if (ch == '-') { + if (change.Old == null) change.Old = new Models.LFSObject(); + line = line.Substring(1); + if (line.StartsWith("oid sha256:")) { + change.Old.OID = line.Substring(11); + } else if (line.StartsWith("size ")) { + change.Old.Size = int.Parse(line.Substring(5)); + } + } else if (ch == '+') { + if (change.New == null) change.New = new Models.LFSObject(); + line = line.Substring(1); + if (line.StartsWith("oid sha256:")) { + change.New.OID = line.Substring(11); + } else if (line.StartsWith("size ")) { + change.New.Size = int.Parse(line.Substring(5)); + } + } else if (line.StartsWith(" size ")) { + change.New.Size = change.Old.Size = int.Parse(line.Substring(6)); + } + } + } +} diff --git a/src/Commands/Rebase.cs b/src/Commands/Rebase.cs new file mode 100644 index 00000000..791233af --- /dev/null +++ b/src/Commands/Rebase.cs @@ -0,0 +1,14 @@ +namespace SourceGit.Commands { + /// + /// 变基命令 + /// + public class Rebase : Command { + + public Rebase(string repo, string basedOn, bool autoStash) { + Cwd = repo; + Args = "rebase "; + if (autoStash) Args += "--autostash "; + Args += basedOn; + } + } +} diff --git a/src/Commands/Remote.cs b/src/Commands/Remote.cs new file mode 100644 index 00000000..7bbc73ad --- /dev/null +++ b/src/Commands/Remote.cs @@ -0,0 +1,31 @@ +namespace SourceGit.Commands { + /// + /// 远程操作 + /// + public class Remote : Command { + + public Remote(string repo) { + Cwd = repo; + } + + public bool Add(string name, string url) { + Args = $"remote add {name} {url}"; + return Exec(); + } + + public bool Delete(string name) { + Args = $"remote remove {name}"; + return Exec(); + } + + public bool Rename(string name, string to) { + Args = $"remote rename {name} {to}"; + return Exec(); + } + + public bool SetURL(string name, string url) { + Args = $"remote set-url {name} {url}"; + return Exec(); + } + } +} diff --git a/src/Commands/Remotes.cs b/src/Commands/Remotes.cs new file mode 100644 index 00000000..1866b7f2 --- /dev/null +++ b/src/Commands/Remotes.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 获取远程列表 + /// + public class Remotes : Command { + private static readonly Regex REG_REMOTE = new Regex(@"^([\w\.\-]+)\s*(\S+).*$"); + private List loaded = new List(); + + public Remotes(string repo) { + Cwd = repo; + Args = "remote -v"; + } + + public List Result() { + Exec(); + return loaded; + } + + public override void OnReadline(string line) { + var match = REG_REMOTE.Match(line); + if (!match.Success) return; + + var remote = new Models.Remote() { + Name = match.Groups[1].Value, + URL = match.Groups[2].Value, + }; + + if (loaded.Find(x => x.Name == remote.Name) != null) return; + loaded.Add(remote); + } + } +} diff --git a/src/Commands/Reset.cs b/src/Commands/Reset.cs new file mode 100644 index 00000000..b60ca174 --- /dev/null +++ b/src/Commands/Reset.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Text; + +namespace SourceGit.Commands { + /// + /// 重置命令 + /// + public class Reset : Command { + + public Reset(string repo) { + Cwd = repo; + Args = "reset"; + } + + public Reset(string repo, string revision, string mode) { + Cwd = repo; + Args = $"reset {mode} {revision}"; + } + + public Reset(string repo, List files) { + Cwd = repo; + + StringBuilder builder = new StringBuilder(); + builder.Append("reset --"); + foreach (var f in files) { + builder.Append(" \""); + builder.Append(f); + builder.Append("\""); + } + Args = builder.ToString(); + } + } +} diff --git a/src/Commands/Revert.cs b/src/Commands/Revert.cs new file mode 100644 index 00000000..2a656fc8 --- /dev/null +++ b/src/Commands/Revert.cs @@ -0,0 +1,13 @@ +namespace SourceGit.Commands { + /// + /// 撤销提交 + /// + public class Revert : Command { + + public Revert(string repo, string commit, bool autoCommit) { + Cwd = repo; + Args = $"revert {commit} --no-edit"; + if (!autoCommit) Args += " --no-commit"; + } + } +} diff --git a/src/Commands/RevisionObjects.cs b/src/Commands/RevisionObjects.cs new file mode 100644 index 00000000..25a190ec --- /dev/null +++ b/src/Commands/RevisionObjects.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 取出指定Revision下的文件列表 + /// + public class RevisionObjects : Command { + private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$"); + private List objects = new List(); + + public RevisionObjects(string cwd, string sha) { + Cwd = cwd; + Args = $"ls-tree -r {sha}"; + } + + public List Result() { + Exec(); + return objects; + } + + public override void OnReadline(string line) { + var match = REG_FORMAT.Match(line); + if (!match.Success) return; + + var obj = new Models.Object(); + obj.SHA = match.Groups[2].Value; + obj.Type = Models.ObjectType.Blob; + obj.Path = match.Groups[3].Value; + + switch (match.Groups[1].Value) { + case "blob": obj.Type = Models.ObjectType.Blob; break; + case "tree": obj.Type = Models.ObjectType.Tree; break; + case "tag": obj.Type = Models.ObjectType.Tag; break; + case "commit": obj.Type = Models.ObjectType.Commit; break; + } + + objects.Add(obj); + } + } +} diff --git a/src/Commands/SaveChangesToPatch.cs b/src/Commands/SaveChangesToPatch.cs new file mode 100644 index 00000000..b40b9bee --- /dev/null +++ b/src/Commands/SaveChangesToPatch.cs @@ -0,0 +1,26 @@ +using System.IO; + +namespace SourceGit.Commands { + /// + /// 将Changes保存到文件流中 + /// + public class SaveChangeToStream : Command { + private StreamWriter writer = null; + + public SaveChangeToStream(string repo, Models.Change change, StreamWriter to) { + Cwd = repo; + if (change.WorkTree == Models.Change.Status.Added || change.WorkTree == Models.Change.Status.Untracked) { + Args = $"diff --no-index --no-ext-diff --find-renames -- /dev/null \"{change.Path}\""; + } else { + var pathspec = $"\"{change.Path}\""; + if (!string.IsNullOrEmpty(change.OriginalPath)) pathspec = $"\"{change.OriginalPath}\" \"{change.Path}\""; + Args = $"diff --binary --no-ext-diff --find-renames --full-index -- {pathspec}"; + } + writer = to; + } + + public override void OnReadline(string line) { + writer.WriteLine(line); + } + } +} diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs new file mode 100644 index 00000000..8e02d8c5 --- /dev/null +++ b/src/Commands/SaveRevisionFile.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; +using System.IO; + +namespace SourceGit.Commands { + /// + /// 保存指定版本的文件 + /// + public class SaveRevisionFile { + private string cwd = ""; + private string bat = ""; + + public SaveRevisionFile(string repo, string path, string sha, string saveTo) { + var tmp = Path.GetTempFileName(); + var cmd = $"\"{Models.Preference.Instance.Git.Path}\" --no-pager "; + + var isLFS = new IsLFSFiltered(repo, path).Result(); + if (isLFS) { + cmd += $"show {sha}:\"{path}\" > {tmp}.lfs\n"; + cmd += $"\"{Models.Preference.Instance.Git.Path}\" --no-pager lfs smudge < {tmp}.lfs > \"{saveTo}\"\n"; + } else { + cmd += $"show {sha}:\"{path}\" > \"{saveTo}\"\n"; + } + + cwd = repo; + bat = tmp + ".bat"; + + File.WriteAllText(bat, cmd); + } + + public void Exec() { + var starter = new ProcessStartInfo(); + starter.FileName = bat; + starter.WorkingDirectory = cwd; + starter.CreateNoWindow = true; + starter.WindowStyle = ProcessWindowStyle.Hidden; + + var proc = Process.Start(starter); + proc.WaitForExit(); + proc.Close(); + + File.Delete(bat); + } + } +} diff --git a/src/Commands/Stash.cs b/src/Commands/Stash.cs new file mode 100644 index 00000000..02d46342 --- /dev/null +++ b/src/Commands/Stash.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Text; + +namespace SourceGit.Commands { + /// + /// 单个贮藏相关操作 + /// + public class Stash : Command { + + public Stash(string repo) { + Cwd = repo; + } + + public bool Push(List files, string message, bool includeUntracked) { + StringBuilder builder = new StringBuilder(); + builder.Append("stash push "); + if (includeUntracked) builder.Append("-u "); + builder.Append("-m \""); + builder.Append(message); + builder.Append("\" "); + + if (files != null && files.Count > 0) { + builder.Append("--"); + foreach (var f in files) { + builder.Append(" \""); + builder.Append(f); + builder.Append("\""); + } + } + + Args = builder.ToString(); + return Exec(); + } + + public bool Apply(string name) { + Args = $"stash apply -q {name}"; + return Exec(); + } + + public bool Pop(string name) { + Args = $"stash pop -q {name}"; + return Exec(); + } + + public bool Drop(string name) { + Args = $"stash drop -q {name}"; + return Exec(); + } + } +} diff --git a/src/Commands/StashChanges.cs b/src/Commands/StashChanges.cs new file mode 100644 index 00000000..459a3776 --- /dev/null +++ b/src/Commands/StashChanges.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 查看Stash中的修改 + /// + public class StashChanges : Command { + private static readonly Regex REG_FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); + private List changes = new List(); + + public StashChanges(string repo, string sha) { + Cwd = repo; + Args = $"diff --name-status --pretty=format: {sha}^ {sha}"; + } + + public List Result() { + Exec(); + return changes; + } + + public override void OnReadline(string line) { + var match = REG_FORMAT.Match(line); + if (!match.Success) return; + + var change = new Models.Change() { Path = match.Groups[2].Value }; + var status = match.Groups[1].Value; + + switch (status[0]) { + case 'M': change.Set(Models.Change.Status.Modified); changes.Add(change); break; + case 'A': change.Set(Models.Change.Status.Added); changes.Add(change); break; + case 'D': change.Set(Models.Change.Status.Deleted); changes.Add(change); break; + case 'R': change.Set(Models.Change.Status.Renamed); changes.Add(change); break; + case 'C': change.Set(Models.Change.Status.Copied); changes.Add(change); break; + } + } + } +} diff --git a/src/Commands/Stashes.cs b/src/Commands/Stashes.cs new file mode 100644 index 00000000..c40e256c --- /dev/null +++ b/src/Commands/Stashes.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 解析当前仓库中的贮藏 + /// + public class Stashes : Command { + private static readonly Regex REG_STASH = new Regex(@"^Reflog: refs/(stash@\{\d+\}).*$"); + private List parsed = new List(); + private Models.Stash current = null; + + public Stashes(string path) { + Cwd = path; + Args = "stash list --pretty=raw"; + } + + public List Result() { + Exec(); + if (current != null) parsed.Add(current); + return parsed; + } + + public override void OnReadline(string line) { + if (line.StartsWith("commit ", StringComparison.Ordinal)) { + if (current != null && !string.IsNullOrEmpty(current.Name)) parsed.Add(current); + current = new Models.Stash() { SHA = line.Substring(7, 8) }; + return; + } + + if (current == null) return; + + if (line.StartsWith("Reflog: refs/stash@", StringComparison.Ordinal)) { + var match = REG_STASH.Match(line); + if (match.Success) current.Name = match.Groups[1].Value; + } else if (line.StartsWith("Reflog message: ", StringComparison.Ordinal)) { + current.Message = line.Substring(16); + } else if (line.StartsWith("author ", StringComparison.Ordinal)) { + current.Author.Parse(line); + } + } + } +} diff --git a/src/Commands/Submodule.cs b/src/Commands/Submodule.cs new file mode 100644 index 00000000..bf3ba87d --- /dev/null +++ b/src/Commands/Submodule.cs @@ -0,0 +1,44 @@ +using System; + +namespace SourceGit.Commands { + /// + /// 子模块 + /// + public class Submodule : Command { + private Action onProgress = null; + + public Submodule(string cwd) { + Cwd = cwd; + } + + public bool Add(string url, string path, bool recursive, Action handler) { + Args = $"submodule add {url} {path}"; + onProgress = handler; + if (!Exec()) return false; + + if (recursive) { + Args = $"submodule update --init --recursive -- {path}"; + return Exec(); + } else { + return true; + } + } + + public bool Update() { + Args = $"submodule update --rebase --remote"; + return Exec(); + } + + public bool Delete(string path) { + Args = $"submodule deinit -f {path}"; + if (!Exec()) return false; + + Args = $"rm -rf {path}"; + return Exec(); + } + + public override void OnReadline(string line) { + onProgress?.Invoke(line); + } + } +} diff --git a/src/Commands/Submodules.cs b/src/Commands/Submodules.cs new file mode 100644 index 00000000..4daf69f6 --- /dev/null +++ b/src/Commands/Submodules.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 获取子模块列表 + /// + public class Submodules : Command { + private readonly Regex REG_FORMAT = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$"); + private List modules = new List(); + + public Submodules(string repo) { + Cwd = repo; + Args = "submodule status"; + } + + public List Result() { + Exec(); + return modules; + } + + public override void OnReadline(string line) { + var match = REG_FORMAT.Match(line); + if (!match.Success) return; + modules.Add(match.Groups[1].Value); + } + } +} diff --git a/src/Commands/Tag.cs b/src/Commands/Tag.cs new file mode 100644 index 00000000..88942878 --- /dev/null +++ b/src/Commands/Tag.cs @@ -0,0 +1,42 @@ +using System.IO; + +namespace SourceGit.Commands { + + /// + /// 标签相关指令 + /// + public class Tag : Command { + + public Tag(string repo) { + Cwd = repo; + } + + public bool Add(string name, string basedOn, string message) { + Args = $"tag -a {name} {basedOn} "; + + if (!string.IsNullOrEmpty(message)) { + string tmp = Path.GetTempFileName(); + File.WriteAllText(tmp, message); + Args += $"-F \"{tmp}\""; + } else { + Args += $"-m {name}"; + } + + return Exec(); + } + + public bool Delete(string name, bool push) { + Args = $"tag --delete {name}"; + if (!Exec()) return false; + + if (push) { + var remotes = new Remotes(Cwd).Result(); + foreach (var r in remotes) { + new Push(Cwd, r.Name, name, true).Exec(); + } + } + + return true; + } + } +} diff --git a/src/Commands/Tags.cs b/src/Commands/Tags.cs new file mode 100644 index 00000000..f534c7b2 --- /dev/null +++ b/src/Commands/Tags.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands { + /// + /// 解析所有的Tags + /// + public class Tags : Command { + public static readonly string CMD = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags"; + public static readonly Regex REG_FORMAT = new Regex(@"\$(.*)\$(.*)\$(.*)"); + + private List loaded = new List(); + + public Tags(string path) { + Cwd = path; + Args = CMD; + } + + public List Result() { + Exec(); + return loaded; + } + + public override void OnReadline(string line) { + var match = REG_FORMAT.Match(line); + if (!match.Success) return; + + var name = match.Groups[1].Value; + var commit = match.Groups[2].Value; + var dereference = match.Groups[3].Value; + + if (string.IsNullOrEmpty(dereference)) { + loaded.Add(new Models.Tag() { + Name = name, + SHA = commit, + }); + } else { + loaded.Add(new Models.Tag() { + Name = name, + SHA = dereference, + }); + } + } + } +} diff --git a/src/Converters/BoolToCollapsed.cs b/src/Converters/BoolToCollapsed.cs deleted file mode 100644 index 47ce3c73..00000000 --- a/src/Converters/BoolToCollapsed.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace SourceGit.Converters { - - /// - /// Same as BoolToVisibilityConverter. - /// - public class BoolToCollapsed : IValueConverter { - - /// - /// Implement IValueConverter.Convert - /// - /// - /// - /// - /// - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (bool)value ? Visibility.Visible : Visibility.Collapsed; - } - - /// - /// Implement IValueConverter.ConvertBack - /// - /// - /// - /// - /// - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } -} diff --git a/src/Converters/FileStatusToColor.cs b/src/Converters/FileStatusToColor.cs deleted file mode 100644 index 75af6980..00000000 --- a/src/Converters/FileStatusToColor.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; -using System.Windows.Media; - -namespace SourceGit.Converters { - - /// - /// Convert file status to brush - /// - public class FileStatusToColor : IValueConverter { - - /// - /// Is only test local changes. - /// - public bool OnlyWorkTree { get; set; } = false; - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - var change = value as Git.Change; - if (change == null) return Brushes.Transparent; - - var status = Git.Change.Status.None; - if (OnlyWorkTree) { - if (change.IsConflit) return Brushes.Yellow; - status = change.WorkTree; - } else { - status = change.Index; - } - - switch (status) { - case Git.Change.Status.Modified: return new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90); - case Git.Change.Status.Added: return new LinearGradientBrush(Colors.LimeGreen, Color.FromRgb(124, 241, 124), 90); - case Git.Change.Status.Deleted: return new LinearGradientBrush(Colors.Tomato, Color.FromRgb(252, 165, 150), 90); - case Git.Change.Status.Renamed: return new LinearGradientBrush(Colors.Orchid, Color.FromRgb(248, 161, 245), 90); - case Git.Change.Status.Copied: return new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90); - case Git.Change.Status.Unmerged: return new LinearGradientBrush(Colors.Orange, Color.FromRgb(255, 213, 134), 90); - case Git.Change.Status.Untracked: return new LinearGradientBrush(Colors.LimeGreen, Color.FromRgb(124, 241, 124), 90); - default: return Brushes.Transparent; - } - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } -} diff --git a/src/Converters/FileStatusToIcon.cs b/src/Converters/FileStatusToIcon.cs deleted file mode 100644 index 85447770..00000000 --- a/src/Converters/FileStatusToIcon.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; - -namespace SourceGit.Converters { - - /// - /// Convert file status to icon. - /// - public class FileStatusToIcon : IValueConverter { - - /// - /// Is only test local changes. - /// - public bool OnlyWorkTree { get; set; } = false; - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - var change = value as Git.Change; - if (change == null) return ""; - - var status = Git.Change.Status.None; - if (OnlyWorkTree) { - if (change.IsConflit) return "X"; - status = change.WorkTree; - } else { - status = change.Index; - } - - switch (status) { - case Git.Change.Status.Modified: return "M"; - case Git.Change.Status.Added: return "A"; - case Git.Change.Status.Deleted: return "D"; - case Git.Change.Status.Renamed: return "R"; - case Git.Change.Status.Copied: return "C"; - case Git.Change.Status.Unmerged: return "U"; - case Git.Change.Status.Untracked: return "?"; - default: return "?"; - } - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } -} diff --git a/src/Converters/FilesDisplayModeToIcon.cs b/src/Converters/FilesDisplayModeToIcon.cs deleted file mode 100644 index 441c54c7..00000000 --- a/src/Converters/FilesDisplayModeToIcon.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; -using System.Windows.Media; - -namespace SourceGit.Converters { - - public class FilesDisplayModeToIcon : IValueConverter { - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - var mode = (Preference.FilesDisplayMode)value; - switch (mode) { - case Preference.FilesDisplayMode.Grid: - return App.Current.FindResource("Icon.Grid") as Geometry; - case Preference.FilesDisplayMode.List: - return App.Current.FindResource("Icon.List") as Geometry; - default: - return App.Current.FindResource("Icon.Tree") as Geometry; - } - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } -} diff --git a/src/Converters/FilesDisplayModeToVisibility.cs b/src/Converters/FilesDisplayModeToVisibility.cs deleted file mode 100644 index 2bea761f..00000000 --- a/src/Converters/FilesDisplayModeToVisibility.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace SourceGit.Converters { - - public class FilesDisplayModeToList : IValueConverter { - - public bool TreatGridAsList { get; set; } = true; - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - var mode = (Preference.FilesDisplayMode)value; - if (mode == Preference.FilesDisplayMode.Tree) return Visibility.Collapsed; - if (mode == Preference.FilesDisplayMode.List) return Visibility.Visible; - if (TreatGridAsList) return Visibility.Visible; - return Visibility.Collapsed; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } - - public class FilesDisplayModeToGrid : IValueConverter { - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (Preference.FilesDisplayMode)value == Preference.FilesDisplayMode.Grid ? Visibility.Visible : Visibility.Collapsed; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } - - public class FilesDisplayModeToTree : IValueConverter { - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (Preference.FilesDisplayMode)value == Preference.FilesDisplayMode.Tree ? Visibility.Visible : Visibility.Collapsed; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } -} diff --git a/src/Converters/IndentToMargin.cs b/src/Converters/IndentToMargin.cs deleted file mode 100644 index 5214396b..00000000 --- a/src/Converters/IndentToMargin.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace SourceGit.Converters { - - /// - /// Convert indent(horizontal offset) to Margin property - /// - public class IndentToMargin : IValueConverter { - - /// - /// Implement IValueConverter.Convert - /// - /// - /// - /// - /// - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return new Thickness((double)value, 0, 0, 0); - } - - /// - /// Implement IValueConverter.ConvertBack - /// - /// - /// - /// - /// - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return ((Thickness)value).Left; - } - } -} diff --git a/src/Converters/IntToRepoColor.cs b/src/Converters/IntToRepoColor.cs deleted file mode 100644 index 338f5403..00000000 --- a/src/Converters/IntToRepoColor.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; -using System.Windows.Media; - -namespace SourceGit.Converters { - - /// - /// Integer to color. - /// - public class IntToRepoColor : IValueConverter { - - /// - /// All supported colors. - /// - public static Brush[] Colors = new Brush[] { - Brushes.Transparent, - Brushes.White, - Brushes.Red, - Brushes.Orange, - Brushes.Yellow, - Brushes.ForestGreen, - Brushes.Purple, - Brushes.DeepSkyBlue, - Brushes.Magenta, - }; - - /// - /// Implement IValueConverter.Convert - /// - /// - /// - /// - /// - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return Colors[((int)value) % Colors.Length]; - } - - /// - /// Implement IValueConverter.ConvertBack - /// - /// - /// - /// - /// - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return ((Thickness)value).Left; - } - } -} diff --git a/src/Converters/InverseBoolToCollapsed.cs b/src/Converters/InverseBoolToCollapsed.cs deleted file mode 100644 index 862bf913..00000000 --- a/src/Converters/InverseBoolToCollapsed.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace SourceGit.Converters { - - /// - /// Inverse BoolToCollapsed. - /// - public class InverseBoolToCollapsed : IValueConverter { - - /// - /// Implement IValueConverter.Convert - /// - /// - /// - /// - /// - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (bool)value ? Visibility.Collapsed : Visibility.Visible; - } - - /// - /// Implement IValueConverter.ConvertBack - /// - /// - /// - /// - /// - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } -} diff --git a/src/Converters/Path.cs b/src/Converters/Path.cs deleted file mode 100644 index 8062b658..00000000 --- a/src/Converters/Path.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Windows.Data; - -namespace SourceGit.Converters { - - public class PathToFileName : IValueConverter { - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return Path.GetFileName(value as string); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } - - public class PathToFolderName : IValueConverter { - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return Path.GetDirectoryName(value as string); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - } -} diff --git a/src/Converters/TreeViewItemDepthToMargin.cs b/src/Converters/TreeViewItemDepthToMargin.cs deleted file mode 100644 index 8c7eb856..00000000 --- a/src/Converters/TreeViewItemDepthToMargin.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Media; - -namespace SourceGit.Converters { - - /// - /// Convert depth of a TreeViewItem to Margin property. - /// - public class TreeViewItemDepthToMargin : IValueConverter { - - /// - /// Indent length - /// - public double Indent { get; set; } = 19; - - /// - /// Implement IValueConverter.Convert - /// - /// - /// - /// - /// - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - TreeViewItem item = value as TreeViewItem; - if (item == null) return new Thickness(0); - - TreeViewItem iterator = GetParent(item); - int depth = 0; - while (iterator != null) { - depth++; - iterator = GetParent(iterator); - } - - return new Thickness(Indent * depth, 0, 0, 0); - } - - /// - /// Implement IValueConvert.ConvertBack - /// - /// - /// - /// - /// - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotImplementedException(); - } - - /// - /// Get parent item. - /// - /// - /// - private TreeViewItem GetParent(TreeViewItem item) { - var parent = VisualTreeHelper.GetParent(item); - - while (parent != null && !(parent is TreeView) && !(parent is TreeViewItem)) { - parent = VisualTreeHelper.GetParent(parent); - } - - return parent as TreeViewItem; - } - } -} diff --git a/src/Git/Blame.cs b/src/Git/Blame.cs deleted file mode 100644 index 7985aaa2..00000000 --- a/src/Git/Blame.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; - -namespace SourceGit.Git { - - /// - /// Blame - /// - public class Blame { - - /// - /// Line content. - /// - public class Line { - public string CommitSHA { get; set; } - public string Author { get; set; } - public string Time { get; set; } - public string Content { get; set; } - } - - /// - /// Lines - /// - public List Lines { get; set; } = new List(); - - /// - /// Is binary file? - /// - public bool IsBinary { get; set; } = false; - } -} diff --git a/src/Git/Branch.cs b/src/Git/Branch.cs deleted file mode 100644 index fd1030c0..00000000 --- a/src/Git/Branch.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace SourceGit.Git { - - /// - /// Git branch - /// - public class Branch { - private static readonly string PRETTY_FORMAT = @"$%(refname)$%(objectname)$%(HEAD)$%(upstream)$%(upstream:track)$%(contents:subject)"; - private static readonly Regex PARSE = new Regex(@"\$(.*)\$(.*)\$([\* ])\$(.*)\$(.*?)\$(.*)"); - private static readonly Regex AHEAD = new Regex(@"ahead (\d+)"); - private static readonly Regex BEHIND = new Regex(@"behind (\d+)"); - - /// - /// Branch type. - /// - public enum Type { - Normal, - Feature, - Release, - Hotfix, - } - - /// - /// Branch name - /// - public string Name { get; set; } = ""; - - /// - /// Full name. - /// - public string FullName { get; set; } = ""; - - /// - /// Head ref - /// - public string Head { get; set; } = ""; - - /// - /// Subject for head ref. - /// - public string HeadSubject { get; set; } = ""; - - /// - /// Is local branch - /// - public bool IsLocal { get; set; } = false; - - /// - /// Branch type. - /// - public Type Kind { get; set; } = Type.Normal; - - /// - /// Remote name. Only used for remote branch - /// - public string Remote { get; set; } = ""; - - /// - /// Upstream. Only used for local branches. - /// - public string Upstream { get; set; } - - /// - /// Track information for upstream. Only used for local branches. - /// - public string UpstreamTrack { get; set; } - - /// - /// Is current branch. Only used for local branches. - /// - public bool IsCurrent { get; set; } - - /// - /// Is this branch's HEAD same with upstream? - /// - public bool IsSameWithUpstream => string.IsNullOrEmpty(UpstreamTrack); - - /// - /// Enable filter in log histories. - /// - public bool IsFiltered { get; set; } - - /// - /// Load branches. - /// - /// - public static List Load(Repository repo) { - var localPrefix = "refs/heads/"; - var remotePrefix = "refs/remotes/"; - var branches = new List(); - var remoteBranches = new List(); - - repo.RunCommand("branch -l --all -v --format=\"" + PRETTY_FORMAT + "\"", line => { - var match = PARSE.Match(line); - if (!match.Success) return; - - var branch = new Branch(); - var refname = match.Groups[1].Value; - if (refname.EndsWith("/HEAD")) return; - - if (refname.StartsWith(localPrefix, StringComparison.Ordinal)) { - branch.Name = refname.Substring(localPrefix.Length); - branch.IsLocal = true; - } else if (refname.StartsWith(remotePrefix, StringComparison.Ordinal)) { - var name = refname.Substring(remotePrefix.Length); - if (name.Contains("/")) { - branch.Remote = name.Substring(0, name.IndexOf('/')); - } else { - branch.Remote = name; - } - branch.Name = name; - branch.IsLocal = false; - remoteBranches.Add(refname); - } - - branch.FullName = refname; - branch.Head = match.Groups[2].Value; - branch.IsCurrent = match.Groups[3].Value == "*"; - branch.Upstream = match.Groups[4].Value; - branch.UpstreamTrack = ParseTrack(match.Groups[5].Value); - branch.HeadSubject = match.Groups[6].Value; - - branches.Add(branch); - }); - - // Fixed deleted remote branch - foreach (var b in branches) { - if (!string.IsNullOrEmpty(b.Upstream) && !remoteBranches.Contains(b.Upstream)) { - b.Upstream = null; - } - } - - return branches; - } - - /// - /// Create new branch. - /// - /// - /// - /// - public static void Create(Repository repo, string name, string startPoint) { - var errs = repo.RunCommand($"branch {name} {startPoint}", null); - if (errs != null) App.RaiseError(errs); - } - - /// - /// Rename branch - /// - /// - /// - public void Rename(Repository repo, string name) { - var errs = repo.RunCommand($"branch -M {Name} {name}", null); - if (errs != null) App.RaiseError(errs); - } - - /// - /// Change upstream - /// - /// - /// - public void SetUpstream(Repository repo, string upstream) { - var errs = repo.RunCommand($"branch {Name} -u {upstream}", null); - if (errs != null) App.RaiseError(errs); - - repo.Branches(true); - repo.OnBranchChanged?.Invoke(); - } - - /// - /// Delete branch. - /// - /// - public void Delete(Repository repo) { - string errs = null; - - if (!IsLocal) { - errs = repo.RunCommand($"-c credential.helper=manager push {Remote} --delete {Name.Substring(Name.IndexOf('/')+1)}", null); - } else { - errs = repo.RunCommand($"branch -D {Name}", null); - } - - if (errs != null) App.RaiseError(errs); - } - - private static string ParseTrack(string data) { - if (string.IsNullOrEmpty(data)) return ""; - - string track = ""; - - var ahead = AHEAD.Match(data); - if (ahead.Success) { - track += ahead.Groups[1].Value + "↑ "; - } - - var behind = BEHIND.Match(data); - if (behind.Success) { - track += behind.Groups[1].Value + "↓"; - } - - return track.Trim(); - } - } -} diff --git a/src/Git/Change.cs b/src/Git/Change.cs deleted file mode 100644 index c5f9ca4a..00000000 --- a/src/Git/Change.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Text.RegularExpressions; - -namespace SourceGit.Git { - - /// - /// Changed file status. - /// - public class Change { - private static readonly Regex FORMAT = new Regex(@"^(\s?[\w\?]{1,4})\s+(.+)$"); - - /// - /// Status Code - /// - public enum Status { - None, - Modified, - Added, - Deleted, - Renamed, - Copied, - Unmerged, - Untracked, - } - - /// - /// Index status - /// - public Status Index { get; set; } - - /// - /// Work tree status. - /// - public Status WorkTree { get; set; } - - /// - /// Current file path. - /// - public string Path { get; set; } - - /// - /// Original file path before this revision. - /// - public string OriginalPath { get; set; } - - /// - /// Staged(added) in index? - /// - public bool IsAddedToIndex { - get { - if (Index == Status.None || Index == Status.Untracked) return false; - return true; - } - } - - /// - /// Is conflict? - /// - public bool IsConflit { - get { - if (Index == Status.Unmerged || WorkTree == Status.Unmerged) return true; - if (Index == Status.Added && WorkTree == Status.Added) return true; - if (Index == Status.Deleted && WorkTree == Status.Deleted) return true; - return false; - } - } - - /// - /// Parse change for `--name-status` data. - /// - /// Raw data. - /// Read from commit? - /// Parsed change instance. - public static Change Parse(string data, bool fromCommit = false) { - var match = FORMAT.Match(data); - if (!match.Success) return null; - - var change = new Change() { Path = match.Groups[2].Value }; - var status = match.Groups[1].Value; - - if (fromCommit) { - switch (status[0]) { - case 'M': change.Set(Status.Modified); break; - case 'A': change.Set(Status.Added); break; - case 'D': change.Set(Status.Deleted); break; - case 'R': change.Set(Status.Renamed); break; - case 'C': change.Set(Status.Copied); break; - default: return null; - } - } else { - switch (status) { - case " M": change.Set(Status.None, Status.Modified); break; - case " A": change.Set(Status.None, Status.Added); break; - case " D": change.Set(Status.None, Status.Deleted); break; - case " R": change.Set(Status.None, Status.Renamed); break; - case " C": change.Set(Status.None, Status.Copied); break; - case "M": change.Set(Status.Modified, Status.None); break; - case "MM": change.Set(Status.Modified, Status.Modified); break; - case "MD": change.Set(Status.Modified, Status.Deleted); break; - case "A": change.Set(Status.Added, Status.None); break; - case "AM": change.Set(Status.Added, Status.Modified); break; - case "AD": change.Set(Status.Added, Status.Deleted); break; - case "D": change.Set(Status.Deleted, Status.None); break; - case "R": change.Set(Status.Renamed, Status.None); break; - case "RM": change.Set(Status.Renamed, Status.Modified); break; - case "RD": change.Set(Status.Renamed, Status.Deleted); break; - case "C": change.Set(Status.Copied, Status.None); break; - case "CM": change.Set(Status.Copied, Status.Modified); break; - case "CD": change.Set(Status.Copied, Status.Deleted); break; - case "DR": change.Set(Status.Deleted, Status.Renamed); break; - case "DC": change.Set(Status.Deleted, Status.Copied); break; - case "DD": change.Set(Status.Deleted, Status.Deleted); break; - case "AU": change.Set(Status.Added, Status.Unmerged); break; - case "UD": change.Set(Status.Unmerged, Status.Deleted); break; - case "UA": change.Set(Status.Unmerged, Status.Added); break; - case "DU": change.Set(Status.Deleted, Status.Unmerged); break; - case "AA": change.Set(Status.Added, Status.Added); break; - case "UU": change.Set(Status.Unmerged, Status.Unmerged); break; - case "??": change.Set(Status.Untracked, Status.Untracked); break; - default: return null; - } - } - - if (change.Path[0] == '"') change.Path = change.Path.Substring(1, change.Path.Length - 2); - if (!string.IsNullOrEmpty(change.OriginalPath) && change.OriginalPath[0] == '"') change.OriginalPath = change.OriginalPath.Substring(1, change.OriginalPath.Length - 2); - return change; - } - - private void Set(Status index, Status workTree = Status.None) { - Index = index; - WorkTree = workTree; - - if (index == Status.Renamed || workTree == Status.Renamed) { - var idx = Path.IndexOf('\t'); - if (idx >= 0) { - OriginalPath = Path.Substring(0, idx); - Path = Path.Substring(idx + 1); - } else { - idx = Path.IndexOf(" -> "); - if (idx > 0) { - OriginalPath = Path.Substring(0, idx); - Path = Path.Substring(idx + 4); - } - } - } - } - } -} diff --git a/src/Git/Commit.cs b/src/Git/Commit.cs deleted file mode 100644 index bfda00c7..00000000 --- a/src/Git/Commit.cs +++ /dev/null @@ -1,364 +0,0 @@ -using System; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text.RegularExpressions; - -namespace SourceGit.Git { - - /// - /// Git commit information. - /// - public class Commit { - private static readonly string GPGSIG_START = "gpgsig -----BEGIN PGP SIGNATURE-----"; - private static readonly string GPGSIG_END = " -----END PGP SIGNATURE-----"; - private static readonly Regex REG_TESTBINARY = new Regex(@"^\-\s+\-\s+.*$"); - - /// - /// Object in commit. - /// - public class Object { - public enum Type { - Tag, - Blob, - Tree, - Commit, - } - - public string Path { get; set; } - public Type Kind { get; set; } - public string SHA { get; set; } - } - - /// - /// Line of text in file. - /// - public class Line { - public int No { get; set; } - public string Content { get; set; } - } - - /// - /// SHA - /// - public string SHA { get; set; } - - /// - /// Short SHA. - /// - public string ShortSHA => SHA.Substring(0, 8); - - /// - /// Parent commit SHAs. - /// - public List Parents { get; set; } = new List(); - - /// - /// Author - /// - public User Author { get; set; } = new User(); - - /// - /// Committer. - /// - public User Committer { get; set; } = new User(); - - /// - /// Subject - /// - public string Subject { get; set; } = ""; - - /// - /// Extra message. - /// - public string Message { get; set; } = ""; - - /// - /// HEAD commit? - /// - public bool IsHEAD { get; set; } = false; - - /// - /// Merged in current branch? - /// - public bool IsMerged { get; set; } = false; - - /// - /// X offset in graph - /// - public double GraphOffset { get; set; } = 0; - - /// - /// Has decorators. - /// - public bool HasDecorators => Decorators.Count > 0; - - /// - /// Decorators. - /// - public List Decorators { get; set; } = new List(); - - /// - /// Read commits. - /// - /// Repository - /// Limitations - /// Parsed commits. - public static List Load(Repository repo, string limit) { - List commits = new List(); - Commit current = null; - bool bSkippingGpgsig = false; - bool findHead = false; - - repo.RunCommand("log --date-order --decorate=full --pretty=raw " + limit, line => { - if (bSkippingGpgsig) { - if (line.StartsWith(GPGSIG_END, StringComparison.Ordinal)) bSkippingGpgsig = false; - return; - } else if (line.StartsWith(GPGSIG_START, StringComparison.Ordinal)) { - bSkippingGpgsig = true; - return; - } - - if (line.StartsWith("commit ", StringComparison.Ordinal)) { - if (current != null) { - current.Message = current.Message.TrimEnd(); - commits.Add(current); - } - - current = new Commit(); - ParseSHA(current, line.Substring("commit ".Length)); - if (!findHead) findHead = current.IsHEAD; - return; - } - - if (current == null) return; - - if (line.StartsWith("tree ", StringComparison.Ordinal)) { - return; - } else if (line.StartsWith("parent ", StringComparison.Ordinal)) { - current.Parents.Add(line.Substring("parent ".Length)); - } else if (line.StartsWith("author ", StringComparison.Ordinal)) { - current.Author.Parse(line); - } else if (line.StartsWith("committer ", StringComparison.Ordinal)) { - current.Committer.Parse(line); - } else if (string.IsNullOrEmpty(current.Subject)) { - current.Subject = line.Trim(); - } else { - current.Message += (line.Trim() + "\n"); - } - }); - - if (current != null) { - current.Message = current.Message.TrimEnd(); - commits.Add(current); - } - - if (!findHead && commits.Count > 0) { - if (commits[commits.Count - 1].IsAncestorOfHead(repo)) { - if (commits.Count == 1) { - commits[0].IsMerged = true; - } else { - var head = FindFirstMerged(repo, commits, 0, commits.Count - 1); - if (head != null) head.IsMerged = true; - } - } - } - - return commits; - } - - /// - /// Get changed file list. - /// - /// - /// - public List GetChanges(Repository repo) { - var changes = new List(); - var regex = new Regex(@"^[MADRC]\d*\s*.*$"); - - var errs = repo.RunCommand($"show --name-status {SHA}", line => { - if (!regex.IsMatch(line)) return; - - var change = Change.Parse(line, true); - if (change != null) changes.Add(change); - }); - - if (errs != null) App.RaiseError(errs); - return changes; - } - - /// - /// Get revision files. - /// - /// - /// - public List GetFiles(Repository repo) { - var files = new List(); - var test = new Regex(@"^\d+\s+(\w+)\s+([0-9a-f]+)\s+(.*)$"); - - var errs = repo.RunCommand($"ls-tree -r {SHA}", line => { - var match = test.Match(line); - if (!match.Success) return; - - var obj = new Object(); - obj.Path = match.Groups[3].Value; - obj.Kind = Object.Type.Blob; - obj.SHA = match.Groups[2].Value; - - switch (match.Groups[1].Value) { - case "tag": obj.Kind = Object.Type.Tag; break; - case "blob": obj.Kind = Object.Type.Blob; break; - case "tree": obj.Kind = Object.Type.Tree; break; - case "commit": obj.Kind = Object.Type.Commit; break; - } - - files.Add(obj); - }); - - if (errs != null) App.RaiseError(errs); - return files; - } - - /// - /// Get file content. - /// - /// - /// - /// - /// - public bool GetTextFileContent(Repository repo, string file, List lines) { - var binary = false; - var count = 0; - - repo.RunCommand($"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {SHA} --numstat -- \"{file}\"", line => { - if (REG_TESTBINARY.IsMatch(line)) binary = true; - }); - - if (!binary) { - var errs = repo.RunCommand($"show {SHA}:\"{file}\"", line => { - if (binary) return; - - if (line.IndexOf('\0') >= 0) { - binary = true; - lines.Clear(); - return; - } - - count++; - lines.Add(new Line() { No = count, Content = line }); - }); - - if (errs != null) App.RaiseError(errs); - } - - return binary; - } - - /// - /// Save file to. - /// - /// - /// - /// - public void SaveFileTo(Repository repo, string file, string saveTo) { - var tmp = Path.GetTempFileName(); - var bat = tmp + ".bat"; - var cmd = ""; - - if (repo.IsLFSFiltered(file)) { - cmd += $"git --no-pager show {SHA}:\"{file}\" > {tmp}.lfs\n"; - cmd += $"git --no-pager lfs smudge < {tmp}.lfs > {saveTo}\n"; - } else { - cmd = $"git --no-pager show {SHA}:\"{file}\" > {saveTo}\n"; - } - - File.WriteAllText(bat, cmd); - - var starter = new ProcessStartInfo(); - starter.FileName = bat; - starter.WorkingDirectory = repo.Path; - starter.CreateNoWindow = true; - starter.WindowStyle = ProcessWindowStyle.Hidden; - - var proc = Process.Start(starter); - proc.WaitForExit(); - proc.Close(); - - File.Delete(bat); - } - - private bool IsAncestorOfHead(Repository repo) { - var startInfo = new ProcessStartInfo(); - startInfo.FileName = App.Setting.Tools.GitExecutable; - startInfo.Arguments = $"merge-base --is-ancestor {SHA} HEAD"; - startInfo.WorkingDirectory = repo.Path; - startInfo.UseShellExecute = false; - startInfo.CreateNoWindow = true; - startInfo.RedirectStandardOutput = false; - startInfo.RedirectStandardError = false; - - var proc = new Process() { StartInfo = startInfo }; - proc.Start(); - proc.WaitForExit(); - - var ret = proc.ExitCode; - proc.Close(); - return ret == 0; - } - - private static Commit FindFirstMerged(Repository repo, List commits, int start, int end) { - var isStartAncestor = commits[start].IsAncestorOfHead(repo); - if (isStartAncestor) return commits[start]; - - if (end - start <= 1) { - return commits[end]; - } else { - var mid = (int)Math.Floor((end + start) * 0.5f); - if (commits[mid].IsAncestorOfHead(repo)) { - return FindFirstMerged(repo, commits, start + 1, mid); - } else { - return FindFirstMerged(repo, commits, mid + 1, end); - } - } - } - - private static void ParseSHA(Commit commit, string data) { - var decoratorStart = data.IndexOf('('); - if (decoratorStart < 0) { - commit.SHA = data.Trim(); - return; - } - - commit.SHA = data.Substring(0, decoratorStart).Trim(); - - var subs = data.Substring(decoratorStart + 1).Split(new char[] { ',', ')', '(' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var sub in subs) { - var d = sub.Trim(); - if (d.StartsWith("tag: refs/tags/", StringComparison.Ordinal)) { - commit.Decorators.Add(new Decorator() { - Type = DecoratorType.Tag, - Name = d.Substring(15).Trim() - }); - } else if (d.EndsWith("/HEAD")) { - continue; - } else if (d.StartsWith("HEAD -> refs/heads/", StringComparison.Ordinal)) { - commit.IsHEAD = true; - commit.Decorators.Add(new Decorator() { - Type = DecoratorType.CurrentBranchHead, - Name = d.Substring(19).Trim() - }); - } else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) { - commit.Decorators.Add(new Decorator() { - Type = DecoratorType.LocalBranchHead, - Name = d.Substring(11).Trim() - }); - } else if (d.StartsWith("refs/remotes/", StringComparison.Ordinal)) { - commit.Decorators.Add(new Decorator() { - Type = DecoratorType.RemoteBranchHead, - Name = d.Substring(13).Trim() - }); - } - } - } - } -} diff --git a/src/Git/Diff.cs b/src/Git/Diff.cs deleted file mode 100644 index c4cfd8e5..00000000 --- a/src/Git/Diff.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; - -namespace SourceGit.Git { - - /// - /// Diff helper. - /// - public class Diff { - private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None); - - /// - /// Line mode. - /// - public enum LineMode { - None, - Normal, - Indicator, - Added, - Deleted, - } - - /// - /// Line change. - /// - public class LineChange { - public LineMode Mode = LineMode.Normal; - public string Content = ""; - public string OldLine = ""; - public string NewLine = ""; - - public LineChange(LineMode mode, string content, string oldLine = "", string newLine = "") { - Mode = mode; - Content = content; - OldLine = oldLine; - NewLine = newLine; - } - } - - /// - /// Text change. - /// - public class TextChange { - public List Lines = new List(); - public bool IsBinary = false; - } - - /// - /// Binary change. - /// - public class BinaryChange { - public long Size = 0; - public long PreSize = 0; - } - - /// - /// Change for LFS object information. - /// - public class LFSChange { - public LFSObject Old; - public LFSObject New; - public bool IsValid => Old != null || New != null; - } - - /// - /// Run diff process. - /// - /// - /// - /// - public static TextChange GetTextChange(Repository repo, string args) { - var rs = new TextChange(); - var started = false; - var oldLine = 0; - var newLine = 0; - - repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => { - if (rs.IsBinary) return; - - if (!started) { - var match = REG_INDICATOR.Match(line); - if (!match.Success) { - if (line.StartsWith("Binary ")) rs.IsBinary = true; - return; - } - - started = true; - oldLine = int.Parse(match.Groups[1].Value); - newLine = int.Parse(match.Groups[2].Value); - rs.Lines.Add(new LineChange(LineMode.Indicator, line)); - } else { - if (line[0] == '-') { - rs.Lines.Add(new LineChange(LineMode.Deleted, line.Substring(1), $"{oldLine}", "")); - oldLine++; - } else if (line[0] == '+') { - rs.Lines.Add(new LineChange(LineMode.Added, line.Substring(1), "", $"{newLine}")); - newLine++; - } else if (line[0] == '\\') { - // IGNORE \ No new line end of file. - } else { - var match = REG_INDICATOR.Match(line); - if (match.Success) { - oldLine = int.Parse(match.Groups[1].Value); - newLine = int.Parse(match.Groups[2].Value); - rs.Lines.Add(new LineChange(LineMode.Indicator, line)); - } else { - rs.Lines.Add(new LineChange(LineMode.Normal, line.Substring(1), $"{oldLine}", $"{newLine}")); - oldLine++; - newLine++; - } - } - } - }); - - if (rs.IsBinary) rs.Lines.Clear(); - return rs; - } - - /// - /// Get file size changes for binary file. - /// - /// - /// - /// - /// - /// - public static BinaryChange GetSizeChange(Repository repo, string[] revisions, string path, string orgPath = null) { - var change = new BinaryChange(); - - if (revisions.Length == 0) { // Compare working copy with HEAD - change.Size = new FileInfo(Path.Combine(repo.Path, path)).Length; - change.PreSize = repo.GetFileSize("HEAD", path); - } else if (revisions.Length == 1) { // Compare HEAD with given revision. - change.Size = repo.GetFileSize("HEAD", path); - if (!string.IsNullOrEmpty(orgPath)) { - change.PreSize = repo.GetFileSize(revisions[0], orgPath); - } else { - change.PreSize = repo.GetFileSize(revisions[0], path); - } - } else { - change.Size = repo.GetFileSize(revisions[1], path); - if (!string.IsNullOrEmpty(orgPath)) { - change.PreSize = repo.GetFileSize(revisions[0], orgPath); - } else { - change.PreSize = repo.GetFileSize(revisions[0], path); - } - } - - return change; - } - - /// - /// Get LFS object changes. - /// - /// - /// - /// - public static LFSChange GetLFSChange(Repository repo, string args) { - var rc = new LFSChange(); - - repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => { - if (line[0] == '-') { - if (rc.Old == null) rc.Old = new LFSObject(); - line = line.Substring(1); - if (line.StartsWith("oid sha256:")) { - rc.Old.OID = line.Substring(11); - } else if (line.StartsWith("size ")) { - rc.Old.Size = int.Parse(line.Substring(5)); - } - } else if (line[0] == '+') { - if (rc.New == null) rc.New = new LFSObject(); - line = line.Substring(1); - if (line.StartsWith("oid sha256:")) { - rc.New.OID = line.Substring(11); - } else if (line.StartsWith("size ")) { - rc.New.Size = int.Parse(line.Substring(5)); - } - } else if (line.StartsWith(" size ")) { - rc.New.Size = rc.Old.Size = int.Parse(line.Substring(6)); - } - }); - - return rc; - } - } -} diff --git a/src/Git/LFS.cs b/src/Git/LFS.cs deleted file mode 100644 index 3b4c5a92..00000000 --- a/src/Git/LFS.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace SourceGit.Git { - - /// - /// Object filtered by LFS - /// - public class LFSObject { - - /// - /// Object id - /// - public string OID { get; set; } - - /// - /// Object size. - /// - public long Size { get; set; } - } -} diff --git a/src/Git/MergeTool.cs b/src/Git/MergeTool.cs deleted file mode 100644 index 8fda9492..00000000 --- a/src/Git/MergeTool.cs +++ /dev/null @@ -1,202 +0,0 @@ -using Microsoft.Win32; -using SourceGit.UI; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SourceGit.Git { - - /// - /// External merge tool - /// - public class MergeTool { - - /// - /// Display name - /// - public string Name { get; set; } - - /// - /// Executable file name. - /// - public string ExecutableName { get; set; } - - /// - /// Command line parameter. - /// - public string Parameter { get; set; } - - /// - /// Auto finder. - /// - public Func Finder { get; set; } - - /// - /// Is this merge tool configured. - /// - public bool IsConfigured => !string.IsNullOrEmpty(ExecutableName); - - /// - /// Supported merge tools. - /// - public static List Supported = new List() { - new MergeTool("--", "", "", FindInvalid), - new MergeTool("Araxis Merge", "Compare.exe", "/wait /merge /3 /a1 \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindAraxisMerge), - new MergeTool("Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", FindBCompare), - new MergeTool("KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", FindKDiff3), - new MergeTool("P4Merge", "p4merge.exe", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", FindP4Merge), - new MergeTool("Tortoise Merge", "TortoiseMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", FindTortoiseMerge), - new MergeTool("Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" //m", FindVSMerge), - new MergeTool("Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", FindVSCode), - }; - - /// - /// Finder for invalid merge tool. - /// - /// - public static string FindInvalid() { - return "--"; - } - - /// - /// Find araxis merge tool install path. - /// - /// - public static string FindAraxisMerge() { - var path = @"C:\Program Files\Araxis\Araxis Merge\Compare.exe"; - if (File.Exists(path)) return path; - return ""; - } - - /// - /// Find kdiff3.exe by registry. - /// - /// - public static string FindKDiff3() { - var root = RegistryKey.OpenBaseKey( - RegistryHive.LocalMachine, - Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); - - var kdiff = root.OpenSubKey(@"SOFTWARE\KDiff3\diff-ext"); - if (kdiff == null) return ""; - return kdiff.GetValue("diffcommand") as string; - } - - /// - /// Finder for p4merge - /// - /// - public static string FindP4Merge() { - var path = @"C:\Program Files\Perforce\p4merge.exe"; - if (File.Exists(path)) return path; - return ""; - } - - /// - /// Find BComp.exe by registry. - /// - /// - public static string FindBCompare() { - var root = RegistryKey.OpenBaseKey( - RegistryHive.LocalMachine, - Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); - - var bc = root.OpenSubKey(@"SOFTWARE\Scooter Software\Beyond Compare"); - if (bc == null) return ""; - - var exec = bc.GetValue("ExePath") as string; - var dir = Path.GetDirectoryName(exec); - return $"{dir}\\BComp.exe"; - } - - /// - /// Find TortoiseMerge.exe by registry. - /// - /// - public static string FindTortoiseMerge() { - var root = RegistryKey.OpenBaseKey( - RegistryHive.LocalMachine, - Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); - - var tortoiseSVN = root.OpenSubKey("SOFTWARE\\TortoiseSVN"); - if (tortoiseSVN == null) return ""; - return tortoiseSVN.GetValue("TMergePath") as string; - } - - /// - /// Find vsDiffMerge.exe. - /// - /// - public static string FindVSMerge() { - var dir = @"C:\Program Files (x86)\Microsoft Visual Studio"; - if (Directory.Exists($"{dir}\\2019")) { - dir += "\\2019"; - } else if (Directory.Exists($"{dir}\\2017")) { - dir += "\\2017"; - } else { - return ""; - } - - if (Directory.Exists($"{dir}\\Community")) { - dir += "\\Community"; - } else if (Directory.Exists($"{dir}\\Enterprise")) { - dir += "\\Enterprise"; - } else if (Directory.Exists($"{dir}\\Professional")) { - dir += "\\Professional"; - } else { - return ""; - } - - return $"{dir}\\Common7\\IDE\\CommonExtensions\\Microsoft\\TeamFoundation\\Team Explorer\\vsDiffMerge.exe"; - } - - /// - /// Find VSCode executable file path. - /// - /// - public static string FindVSCode() { - var root = RegistryKey.OpenBaseKey( - RegistryHive.LocalMachine, - Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); - - var vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1"); - if (vscode != null) { - return vscode.GetValue("DisplayIcon") as string; - } - - vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1"); - if (vscode != null) { - return vscode.GetValue("DisplayIcon") as string; - } - - vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1"); - if (vscode != null) { - return vscode.GetValue("DisplayIcon") as string; - } - - vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1"); - if (vscode != null) { - return vscode.GetValue("DisplayIcon") as string; - } - - return ""; - } - - /// - /// Constructor. - /// - /// - /// - /// - /// - public MergeTool(string name, string exe, string param, Func finder) { - Name = name; - ExecutableName = exe; - Parameter = param; - Finder = finder; - } - } -} diff --git a/src/Git/Remote.cs b/src/Git/Remote.cs deleted file mode 100644 index 079bd2ea..00000000 --- a/src/Git/Remote.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace SourceGit.Git { - - /// - /// Git remote - /// - public class Remote { - private static readonly Regex FORMAT = new Regex(@"^([\w\.\-]+)\s*(\S+).*$"); - - /// - /// Name of this remote - /// - public string Name { get; set; } - - /// - /// URL - /// - public string URL { get; set; } - - /// - /// Parsing remote - /// - /// Repository - /// - public static List Load(Repository repo) { - var remotes = new List(); - var added = new List(); - - repo.RunCommand("remote -v", data => { - var match = FORMAT.Match(data); - if (!match.Success) return; - - var remote = new Remote() { - Name = match.Groups[1].Value, - URL = match.Groups[2].Value, - }; - - if (added.Contains(remote.Name)) return; - - added.Add(remote.Name); - remotes.Add(remote); - }); - - return remotes; - } - - /// - /// Add new remote - /// - /// - /// - /// - public static void Add(Repository repo, string name, string url) { - var errs = repo.RunCommand($"remote add {name} {url}", null); - if (errs != null) { - App.RaiseError(errs); - } else { - repo.Fetch(new Remote() { Name = name }, true, null); - } - } - - /// - /// Delete remote. - /// - /// - /// - public static void Delete(Repository repo, string remote) { - var errs = repo.RunCommand($"remote remove {remote}", null); - if (errs != null) App.RaiseError(errs); - } - - /// - /// Edit remote. - /// - /// - /// - /// - public void Edit(Repository repo, string name, string url) { - string errs = null; - - if (name != Name) { - errs = repo.RunCommand($"remote rename {Name} {name}", null); - if (errs != null) { - App.RaiseError(errs); - return; - } - } - - if (url != URL) { - errs = repo.RunCommand($"remote set-url {name} {url}", null); - if (errs != null) App.RaiseError(errs); - } - } - } -} diff --git a/src/Git/Repository.cs b/src/Git/Repository.cs deleted file mode 100644 index e16f202f..00000000 --- a/src/Git/Repository.cs +++ /dev/null @@ -1,1253 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using System.Threading; -using System.Windows.Threading; - -namespace SourceGit.Git { - - /// - /// Git repository - /// - public class Repository { - - #region HOOKS - [JsonIgnore] public Action OnNavigateCommit = null; - [JsonIgnore] public Action OnWorkingCopyChanged = null; - [JsonIgnore] public Action OnTagChanged = null; - [JsonIgnore] public Action OnStashChanged = null; - [JsonIgnore] public Action OnBranchChanged = null; - [JsonIgnore] public Action OnCommitsChanged = null; - [JsonIgnore] public Action OnSubmoduleChanged = null; - [JsonIgnore] public Action OnClosing = null; - #endregion - - #region PROPERTIES_SAVED - /// - /// Storage path. - /// - public string Path { get; set; } - /// - /// Display name. - /// - public string Name { get; set; } - /// - /// Owner group. - /// - public string GroupId { get; set; } - /// - /// Custom color. - /// - public int Color { get; set; } = 0; - /// - /// Expand tags. - /// - public bool ExpandTags { get; set; } - /// - /// Filters for logs. - /// - public List LogFilters { get; set; } = new List(); - /// - /// Last 10 Commit message. - /// - public List CommitMsgRecords { get; set; } = new List(); - #endregion - - #region PROPERTIES_RUNTIME - [JsonIgnore] public Repository Parent = null; - [JsonIgnore] public string GitDir = null; - - private List cachedRemotes = new List(); - private List cachedBranches = new List(); - private List cachedTags = new List(); - private FileSystemWatcher gitDirWatcher = null; - private FileSystemWatcher workingCopyWatcher = null; - private Timer timer = null; - private bool isWatcherDisabled = false; - private long nextUpdateTags = 0; - private long nextUpdateLocalChanges = 0; - private long nextUpdateStashes = 0; - private long nextUpdateTree = 0; - private long nextFetchingRemotes = 0; - - private string featurePrefix = null; - private string releasePrefix = null; - private string hotfixPrefix = null; - #endregion - - #region METHOD_PROCESS - /// - /// Read git config - /// - /// - /// - public string GetConfig(string key) { - var startInfo = new ProcessStartInfo(); - startInfo.FileName = App.Setting.Tools.GitExecutable; - startInfo.Arguments = $"config {key}"; - startInfo.WorkingDirectory = Path; - startInfo.UseShellExecute = false; - startInfo.CreateNoWindow = true; - startInfo.RedirectStandardOutput = true; - startInfo.StandardOutputEncoding = Encoding.UTF8; - - var proc = new Process() { StartInfo = startInfo }; - proc.Start(); - var output = proc.StandardOutput.ReadToEnd(); - proc.WaitForExit(); - proc.Close(); - - return output.Trim(); - } - - /// - /// Configure git. - /// - /// - /// - public void SetConfig(string key, string value) { - var startInfo = new ProcessStartInfo(); - startInfo.FileName = App.Setting.Tools.GitExecutable; - startInfo.Arguments = $"config {key} \"{value}\""; - startInfo.WorkingDirectory = Path; - startInfo.UseShellExecute = false; - startInfo.CreateNoWindow = true; - - var proc = new Process() { StartInfo = startInfo }; - proc.Start(); - proc.WaitForExit(); - proc.Close(); - } - - /// - /// Run git command without repository. - /// - /// Working directory. - /// Arguments for running git command. - /// Handler for output. - /// Handle error as output. - /// Errors if exists. - public static string RunCommand(string cwd, string args, Action outputHandler, bool includeError = false) { - var startInfo = new ProcessStartInfo(); - startInfo.FileName = App.Setting.Tools.GitExecutable; - startInfo.Arguments = "--no-pager -c core.quotepath=off " + args; - startInfo.WorkingDirectory = cwd; - startInfo.UseShellExecute = false; - startInfo.CreateNoWindow = true; - startInfo.RedirectStandardOutput = true; - startInfo.RedirectStandardError = true; - startInfo.StandardOutputEncoding = Encoding.UTF8; - startInfo.StandardErrorEncoding = Encoding.UTF8; - - var progressFilter = new Regex(@"\d+\%"); - var errs = new List(); - var proc = new Process() { StartInfo = startInfo }; - - proc.OutputDataReceived += (o, e) => { - if (e.Data == null) return; - outputHandler?.Invoke(e.Data); - }; - proc.ErrorDataReceived += (o, e) => { - if (e.Data == null) return; - if (includeError) outputHandler?.Invoke(e.Data); - if (string.IsNullOrEmpty(e.Data)) return; - if (progressFilter.IsMatch(e.Data)) return; - if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal)) return; - errs.Add(e.Data); - }; - - proc.Start(); - proc.BeginOutputReadLine(); - proc.BeginErrorReadLine(); - proc.WaitForExit(); - - int exitCode = proc.ExitCode; - proc.Close(); - - if (exitCode != 0 && errs.Count > 0) { - return string.Join("\n", errs); - } else { - return null; - } - } - - /// - /// Create process for reading outputs/errors using git.exe - /// - /// Arguments for running git command. - /// Handler for output. - /// Handle error as output. - /// Errors if exists. - public string RunCommand(string args, Action outputHandler, bool includeError = false) { - return RunCommand(Path, args, outputHandler, includeError); - } - - /// - /// Assert command result and then update branches and commits. - /// - /// - public void AssertCommand(string err) { - if (!string.IsNullOrEmpty(err)) App.RaiseError(err); - - Branches(true); - OnBranchChanged?.Invoke(); - OnCommitsChanged?.Invoke(); - OnWorkingCopyChanged?.Invoke(); - OnTagChanged?.Invoke(); - - nextUpdateLocalChanges = 0; - nextUpdateTags = 0; - nextUpdateTree = 0; - isWatcherDisabled = false; - } - #endregion - - #region METHOD_VALIDATIONS - /// - /// Is valid git directory. - /// - /// Local path. - /// - public static bool IsValid(string path) { - if (!Directory.Exists(path)) return false; - - var startInfo = new ProcessStartInfo(); - startInfo.FileName = App.Setting.Tools.GitExecutable; - startInfo.Arguments = "rev-parse --git-dir"; - startInfo.WorkingDirectory = path; - startInfo.UseShellExecute = false; - startInfo.CreateNoWindow = true; - - try { - var proc = new Process() { StartInfo = startInfo }; - proc.Start(); - proc.WaitForExit(); - - var test = proc.ExitCode == 0; - proc.Close(); - return test; - } catch { - return false; - } - } - - /// - /// Is remote url valid. - /// - /// - /// - public static bool IsValidUrl(string url) { - return !string.IsNullOrEmpty(url) - && (url.StartsWith("http://", StringComparison.Ordinal) - || url.StartsWith("https://", StringComparison.Ordinal) - || url.StartsWith("git@", StringComparison.Ordinal) - || url.StartsWith("file://", StringComparison.Ordinal)); - } - #endregion - - #region METHOD_OPEN_CLOSE - /// - /// Open repository. - /// - public void Open() { - isWatcherDisabled = false; - nextFetchingRemotes = DateTime.Now.AddMinutes(10).ToFileTime(); - - GitDir = ".git"; - RunCommand("rev-parse --git-dir", line => { - GitDir = line; - }); - if (!System.IO.Path.IsPathRooted(GitDir)) GitDir = System.IO.Path.Combine(Path, GitDir); - - var checkGitDir = new DirectoryInfo(GitDir); - if (!checkGitDir.Exists) { - App.RaiseError(App.Text("GitDirNotFound")); - return; - } else { - GitDir = checkGitDir.FullName; - } - - gitDirWatcher = new FileSystemWatcher(); - gitDirWatcher.Path = GitDir; - gitDirWatcher.Filter = "*"; - gitDirWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; - gitDirWatcher.IncludeSubdirectories = true; - gitDirWatcher.Created += OnGitDirFSChanged; - gitDirWatcher.Renamed += OnGitDirFSChanged; - gitDirWatcher.Changed += OnGitDirFSChanged; - gitDirWatcher.Deleted += OnGitDirFSChanged; - gitDirWatcher.EnableRaisingEvents = true; - - workingCopyWatcher = new FileSystemWatcher(); - workingCopyWatcher.Path = Path; - workingCopyWatcher.Filter = "*"; - workingCopyWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; - workingCopyWatcher.IncludeSubdirectories = true; - workingCopyWatcher.Created += OnWorkingCopyFSChanged; - workingCopyWatcher.Renamed += OnWorkingCopyFSChanged; - workingCopyWatcher.Changed += OnWorkingCopyFSChanged; - workingCopyWatcher.Deleted += OnWorkingCopyFSChanged; - workingCopyWatcher.EnableRaisingEvents = true; - - timer = new Timer(Tick, null, 100, 100); - - featurePrefix = GetConfig("gitflow.prefix.feature"); - releasePrefix = GetConfig("gitflow.prefix.release"); - hotfixPrefix = GetConfig("gitflow.prefix.hotfix"); - } - - /// - /// Close repository. - /// - public void Close() { - OnClosing?.Invoke(); - - OnBranchChanged = null; - OnCommitsChanged = null; - OnTagChanged = null; - OnStashChanged = null; - OnWorkingCopyChanged = null; - OnNavigateCommit = null; - OnSubmoduleChanged = null; - OnClosing = null; - - cachedBranches.Clear(); - cachedRemotes.Clear(); - cachedTags.Clear(); - - gitDirWatcher.EnableRaisingEvents = false; - workingCopyWatcher.EnableRaisingEvents = false; - gitDirWatcher.Dispose(); - workingCopyWatcher.Dispose(); - timer.Dispose(); - - gitDirWatcher = null; - workingCopyWatcher = null; - timer = null; - featurePrefix = null; - releasePrefix = null; - hotfixPrefix = null; - - GC.Collect(); - } - #endregion - - #region METHOD_WATCHER - public void SetWatcherEnabled(bool enabled) { - isWatcherDisabled = !enabled; - } - - private void Tick(object sender) { - var now = DateTime.Now.ToFileTime(); - if (now >= nextFetchingRemotes) { - Fetch(null, true, null, false); - return; - } - - if (isWatcherDisabled) { - nextUpdateLocalChanges = 0; - nextUpdateStashes = 0; - nextUpdateTags = 0; - nextUpdateTree = 0; - return; - } - - if (nextUpdateLocalChanges > 0 && now >= nextUpdateLocalChanges) { - nextUpdateLocalChanges = 0; - OnWorkingCopyChanged?.Invoke(); - } - - if (nextUpdateTags > 0 && now >= nextUpdateTags) { - nextUpdateTags = 0; - OnTagChanged?.Invoke(); - } - - if (nextUpdateStashes > 0 && now >= nextUpdateStashes) { - nextUpdateStashes = 0; - OnStashChanged?.Invoke(); - } - - if (nextUpdateTree > 0 && now >= nextUpdateTree) { - nextUpdateTree = 0; - - var branches = Branches(true); - var badFilters = new List(); - foreach (var filter in LogFilters) { - if (filter.StartsWith("refs/heads/") || filter.StartsWith("refs/remotes/")) { - var idx = branches.FindIndex(b => b.FullName == filter); - if (idx < 0) badFilters.Add(filter); - } - } - foreach (var bad in badFilters) LogFilters.Remove(bad); - - OnBranchChanged?.Invoke(); - OnCommitsChanged?.Invoke(); - } - } - - private void OnGitDirFSChanged(object sender, FileSystemEventArgs e) { - if (string.IsNullOrEmpty(e.Name)) return; - if (e.Name.StartsWith("index")) return; - - if (e.Name.StartsWith("refs\\tags", StringComparison.Ordinal)) { - nextUpdateTags = DateTime.Now.AddSeconds(.5).ToFileTime(); - } else if (e.Name.StartsWith("refs\\stash", StringComparison.Ordinal)) { - nextUpdateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); - } else if (e.Name.EndsWith("_HEAD", StringComparison.Ordinal) || - e.Name.StartsWith("refs\\heads", StringComparison.Ordinal) || - e.Name.StartsWith("refs\\remotes", StringComparison.Ordinal)) { - nextUpdateTree = DateTime.Now.AddSeconds(.5).ToFileTime(); - } - } - - private void OnWorkingCopyFSChanged(object sender, FileSystemEventArgs e) { - if (string.IsNullOrEmpty(e.Name)) return; - if (e.Name == ".git" || e.Name.StartsWith(".git\\")) return; - - nextUpdateLocalChanges = DateTime.Now.AddSeconds(1.5).ToFileTime(); - } - #endregion - - #region METHOD_GITCOMMANDS - /// - /// Clone repository. - /// - /// Remote repository URL - /// Folder to clone into - /// Remote name - /// Local name - /// Additional parameters - /// - /// - public static bool Clone(string url, string folder, string rName, string lName, string extra, Action onProgress) { - string RemoteName; - if (rName != null) { - RemoteName = $" --origin {rName}"; - } else { - RemoteName = ""; - } - - if (extra == null) extra = ""; - - var errs = RunCommand(folder, $"-c credential.helper=manager clone --progress --verbose {RemoteName} --recurse-submodules {extra} {url} {lName}", line => { - if (line != null) onProgress?.Invoke(line); - }, true); - - if (errs != null) { - App.RaiseError(errs); - return false; - } - - return true; - } - - /// - /// Fetch remote changes - /// - /// - /// - /// - /// - public void Fetch(Remote remote, bool prune, Action onProgress, bool raiseError = true) { - isWatcherDisabled = true; - nextFetchingRemotes = DateTime.Now.AddMinutes(10).ToFileTime(); - - var args = "-c credential.helper=manager fetch --progress --verbose "; - - if (prune) args += "--prune "; - - if (remote == null) { - args += "--all"; - } else { - args += remote.Name; - } - - var errs = RunCommand(args, line => { - if (line != null) onProgress?.Invoke(line); - }, true); - - OnSubmoduleChanged?.Invoke(); - if (!raiseError) errs = null; - - nextFetchingRemotes = DateTime.Now.AddMinutes(10).ToFileTime(); - AssertCommand(errs); - } - - /// - /// Pull remote changes. - /// - /// remote - /// branch - /// Progress message handler. - /// Use rebase instead of merge. - /// Auto stash local changes. - /// Progress message handler. - public void Pull(string remote, string branch, Action onProgress, bool rebase = false, bool autostash = false) { - isWatcherDisabled = true; - nextFetchingRemotes = DateTime.Now.AddMinutes(10).ToFileTime(); - - var args = "-c credential.helper=manager pull --verbose --progress --tags "; - var needPopStash = false; - - if (rebase) args += "--rebase "; - if (autostash) { - if (rebase) { - args += "--autostash "; - } else { - var changes = LocalChanges(); - if (changes.Count > 0) { - var fatal = RunCommand("stash push -u -m \"PULL_AUTO_STASH\"", null); - if (fatal != null) { - App.RaiseError(fatal); - isWatcherDisabled = false; - return; - } - needPopStash = true; - } - } - } - - var errs = RunCommand(args + remote + " " + branch, line => { - if (line != null) onProgress?.Invoke(line); - }, true); - - OnSubmoduleChanged?.Invoke(); - AssertCommand(errs); - if (needPopStash && errs == null) RunCommand("stash pop -q stash@{0}", null); - } - - /// - /// Push local branch to remote. - /// - /// Remote - /// Local branch name - /// Remote branch name - /// Progress message handler. - /// Push tags - /// Create track reference - /// Force push - public void Push(string remote, string localBranch, string remoteBranch, Action onProgress, bool withTags = false, bool track = false, bool force = false) { - isWatcherDisabled = true; - - var args = "-c credential.helper=manager push --progress --verbose "; - - if (withTags) args += "--tags "; - if (track) args += "-u "; - if (force) args += "--force-with-lease "; - - var errs = RunCommand(args + remote + " " + localBranch + ":" + remoteBranch, line => { - if (line != null) onProgress?.Invoke(line); - }, true); - - AssertCommand(errs); - } - - /// - /// Apply patch. - /// - /// - /// - /// - public void Apply(string patch, bool ignoreSpaceChanges, string whitespaceMode) { - isWatcherDisabled = true; - - var args = "apply "; - if (ignoreSpaceChanges) args += "--ignore-whitespace "; - else args += $"--whitespace={whitespaceMode} "; - - var errs = RunCommand($"{args} \"{patch}\"", null); - if (errs != null) { - App.RaiseError(errs); - } else { - OnWorkingCopyChanged?.Invoke(); - } - - isWatcherDisabled = false; - } - - /// - /// Revert given commit. - /// - /// - /// - public void Revert(string commit, bool autoCommit) { - isWatcherDisabled = true; - - var errs = RunCommand($"revert {commit} --no-edit" + (autoCommit ? "" : " --no-commit"), null); - AssertCommand(errs); - } - - /// - /// Checkout - /// - /// Options. - public void Checkout(string option) { - isWatcherDisabled = true; - - var errs = RunCommand($"checkout {option}", null); - if (errs != null) { - App.RaiseError(errs); - } else { - Branches(true); - OnBranchChanged?.Invoke(); - OnCommitsChanged?.Invoke(); - OnWorkingCopyChanged?.Invoke(); - OnTagChanged?.Invoke(); - } - - isWatcherDisabled = false; - } - - /// - /// Merge given branch into current. - /// - /// - /// - public void Merge(string branch, string option) { - isWatcherDisabled = true; - - var errs = RunCommand($"merge {branch} {option}", null); - AssertCommand(errs); - } - - /// - /// Rebase current branch to revision - /// - /// - /// - public void Rebase(string revision, bool autoStash) { - isWatcherDisabled = true; - - var args = $"rebase "; - if (autoStash) args += "--autostash "; - args += revision; - - var errs = RunCommand(args, null); - AssertCommand(errs); - } - - /// - /// Reset. - /// - /// - /// - public void Reset(string revision, string mode = "") { - isWatcherDisabled = true; - - var errs = RunCommand($"reset {mode} {revision}", null); - AssertCommand(errs); - } - - /// - /// Cherry pick commit. - /// - /// - /// - public void CherryPick(string commit, bool noCommit) { - isWatcherDisabled = true; - - var args = "cherry-pick "; - args += noCommit ? "-n " : "--ff "; - args += commit; - - var errs = RunCommand(args, null); - AssertCommand(errs); - } - - /// - /// Stage(add) files to index. - /// - /// - public void Stage(params string[] files) { - isWatcherDisabled = true; - - if (files == null || files.Length == 0) { - var errs = RunCommand("add .", null); - if (errs != null) App.RaiseError(errs); - } else { - for (int i = 0; i < files.Length; i += 10) { - var args = "add --"; - var maxIdx = i + 10; - - for (int j = i; j < files.Length && j < maxIdx; j++) { - args += $" \"{files[j]}\""; - } - - var errs = RunCommand(args, null); - if (errs != null) { - App.RaiseError(errs); - break; - } - } - } - - OnWorkingCopyChanged?.Invoke(); - isWatcherDisabled = false; - } - - /// - /// Unstage files from index - /// - /// - public void Unstage(params string[] files) { - isWatcherDisabled = true; - - if (files == null || files.Length == 0) { - var errs = RunCommand("reset", null); - if (errs != null) App.RaiseError(errs); - } else { - for (int i = 0; i < files.Length; i += 10) { - var args = "reset --"; - var maxIdx = i + 10; - - for (int j = i; j < files.Length && j < maxIdx; j++) { - args += $" \"{files[j]}\""; - } - - var errs = RunCommand(args, null); - if (errs != null) { - App.RaiseError(errs); - break; - } - } - } - - OnWorkingCopyChanged?.Invoke(); - isWatcherDisabled = false; - } - - /// - /// Discard changes. - /// - /// - public void Discard(List changes) { - isWatcherDisabled = true; - - if (changes == null || changes.Count == 0) { - var errs = RunCommand("reset --hard HEAD", null); - if (errs != null) { - App.RaiseError(errs); - isWatcherDisabled = false; - return; - } - - RunCommand("clean -qfd", null); - } else { - foreach (var change in changes) { - if (change.WorkTree == Change.Status.Untracked || change.WorkTree == Change.Status.Added) { - RunCommand($"clean -qfd -- \"{change.Path}\"", null); - } else { - RunCommand($"checkout -f -- \"{change.Path}\"", null); - } - } - } - - OnWorkingCopyChanged?.Invoke(); - isWatcherDisabled = false; - } - - /// - /// Commit - /// - /// - /// - public bool DoCommit(string message, bool amend) { - isWatcherDisabled = true; - - var file = System.IO.Path.GetTempFileName(); - File.WriteAllText(file, message); - - var args = $"commit --file=\"{file}\""; - if (amend) args += " --amend --no-edit"; - var errs = RunCommand(args, null); - AssertCommand(errs); - - var branch = CurrentBranch(); - OnNavigateCommit?.Invoke(branch.Head); - return string.IsNullOrEmpty(errs); - } - - /// - /// Get all remotes of this repository. - /// - /// Force reload - /// Remote collection - public List Remotes(bool bForceReload = false) { - if (cachedRemotes.Count == 0 || bForceReload) { - cachedRemotes = Remote.Load(this); - } - - return cachedRemotes; - } - - /// - /// Local changes in working copy. - /// - /// Changes. - public List LocalChanges() { - List changes = new List(); - RunCommand("status -uall --ignore-submodules=dirty --porcelain", line => { - if (!string.IsNullOrEmpty(line)) { - var change = Change.Parse(line); - if (change != null) changes.Add(change); - } - }); - return changes; - } - - /// - /// Get total commit count. - /// - /// Number of total commits. - public int TotalCommits() { - int count = 0; - RunCommand("rev-list --all --count", line => { - if (!string.IsNullOrEmpty(line)) count = int.Parse(line.Trim()); - }); - return count; - } - - /// - /// Load commits. - /// - /// Extra limit arguments for `git log` - /// Commit collection - public List Commits(string limit = null) { - return Commit.Load(this, (limit == null ? "" : limit)); ; - } - - /// - /// Load all branches. - /// - /// Force reload. - /// Branches collection. - public List Branches(bool bForceReload = false) { - if (cachedBranches.Count == 0 || bForceReload) { - cachedBranches = Branch.Load(this); - } - - if (IsGitFlowEnabled()) { - foreach (var b in cachedBranches) { - if (b.IsLocal) { - if (b.Name.StartsWith(featurePrefix)) { - b.Kind = Branch.Type.Feature; - } else if (b.Name.StartsWith(releasePrefix)) { - b.Kind = Branch.Type.Release; - } else if (b.Name.StartsWith(hotfixPrefix)) { - b.Kind = Branch.Type.Hotfix; - } - } - } - } - - return cachedBranches; - } - - /// - /// Get all remote branches - /// - /// All remote branches - public List RemoteBranches() { - var ret = new List(); - foreach (var b in cachedBranches) { - if (!b.IsLocal) ret.Add(b); - } - return ret; - } - - /// - /// Get current branch - /// - /// - public Branch CurrentBranch() { - foreach (var b in cachedBranches) { - if (b.IsCurrent) return b; - } - - return null; - } - - /// - /// Load all tags. - /// - /// - /// - public List Tags(bool bForceReload = false) { - if (cachedTags.Count == 0 || bForceReload) { - cachedTags = Tag.Load(this); - } - - return cachedTags; - } - - /// - /// Get all stashes - /// - /// - public List Stashes() { - var reflog = new Regex(@"^Reflog: refs/(stash@\{\d+\}).*$"); - var stashes = new List(); - var current = null as Stash; - - var errs = RunCommand("stash list --pretty=raw", line => { - if (line.StartsWith("commit ")) { - if (current != null && !string.IsNullOrEmpty(current.Name)) stashes.Add(current); - current = new Stash() { SHA = line.Substring(7, 8) }; - return; - } - - if (current == null) return; - - if (line.StartsWith("Reflog: refs/stash@")) { - var match = reflog.Match(line); - if (match.Success) current.Name = match.Groups[1].Value; - } else if (line.StartsWith("Reflog message: ")) { - current.Message = line.Substring(16); - } else if (line.StartsWith("author ")) { - current.Author.Parse(line); - } - }); - - if (current != null) stashes.Add(current); - if (errs != null) App.RaiseError(errs); - return stashes; - } - - /// - /// Get all submodules - /// - /// - public List Submodules() { - var test = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$"); - var modules = new List(); - - var errs = RunCommand("submodule status", line => { - var match = test.Match(line); - if (!match.Success) return; - - modules.Add(match.Groups[1].Value); - }); - - return modules; - } - - /// - /// Add submodule - /// - /// - /// - /// - /// - public void AddSubmodule(string url, string localPath, bool recursive, Action onProgress) { - isWatcherDisabled = true; - - var errs = RunCommand($"submodule add {url} {localPath}", onProgress, true); - if (errs == null) { - if (recursive) RunCommand($"submodule update --init --recursive -- {localPath}", onProgress, true); - OnWorkingCopyChanged?.Invoke(); - OnSubmoduleChanged?.Invoke(); - } else { - App.RaiseError(errs); - } - - isWatcherDisabled = false; - } - - /// - /// Update submodule. - /// - public void UpdateSubmodule() { - isWatcherDisabled = true; - - var errs = RunCommand("submodule update --rebase --remote", null); - if (errs != null) { - App.RaiseError(errs); - } else { - OnWorkingCopyChanged?.Invoke(); - OnSubmoduleChanged?.Invoke(); - } - - isWatcherDisabled = false; - } - - /// - /// Delete submodule - /// - /// - public void DeleteSubmodule(string path) { - isWatcherDisabled = true; - - var errs = RunCommand($"submodule deinit -f {path}", null); - if (errs != null) { - App.RaiseError(errs); - } else { - errs = RunCommand($"rm -rf {path}", null); - if (errs != null) App.RaiseError(errs); - - OnWorkingCopyChanged?.Invoke(); - OnSubmoduleChanged?.Invoke(); - } - - isWatcherDisabled = false; - } - - /// - /// Blame file. - /// - /// - /// - /// - public Blame BlameFile(string file, string revision) { - var regex = new Regex(@"^\^?([0-9a-f]+)\s+.*\((.*)\s+(\d+)\s+[\-\+]?\d+\s+\d+\) (.*)"); - var blame = new Blame(); - - var errs = RunCommand($"blame -t {revision} -- \"{file}\"", line => { - if (blame.IsBinary) return; - if (string.IsNullOrEmpty(line)) return; - - if (line.IndexOf('\0') >= 0) { - blame.IsBinary = true; - blame.Lines.Clear(); - return; - } - - var match = regex.Match(line); - if (!match.Success) return; - - var commit = match.Groups[1].Value; - var author = match.Groups[2].Value; - var timestamp = int.Parse(match.Groups[3].Value); - var data = match.Groups[4].Value; - var when = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); - - var blameLine = new Blame.Line() { - CommitSHA = commit, - Author = author, - Time = when, - Content = data, - }; - - blame.Lines.Add(blameLine); - }); - - if (errs != null) App.RaiseError(errs); - return blame; - } - - /// - /// Get file size. - /// - /// - /// - /// - public long GetFileSize(string sha, string path) { - long size = 0; - - RunCommand($"cat-file -s {sha}:\"{path}\"", line => { - if (!long.TryParse(line, out size)) size = 0; - }); - - return size; - } - - /// - /// Detect if a file is managed by LFS. - /// - /// File path - /// - public bool IsLFSFiltered(string path) { - bool ok = false; - RunCommand($"check-attr -a -z \"{path}\"", line => { - ok = ok || line.Contains("filter\0lfs"); - }); - return ok; - } - - /// - /// Get LFS object information. - /// - /// - /// - /// - public LFSObject GetLFSObject(string sha, string path) { - LFSObject obj = new LFSObject(); - - RunCommand($"show {sha}:\"{path}\"", line => { - if (line.StartsWith("oid")) { - obj.OID = line.Substring(3).Replace("sha256:", "").Trim(); - } else if (line.StartsWith("size")) { - obj.Size = int.Parse(line.Substring(4).Trim()); - } - }); - - return obj; - } - #endregion - - #region METHOD_GITFLOW - /// - /// Check if git-flow feature enabled - /// - /// - public bool IsGitFlowEnabled() { - return !string.IsNullOrEmpty(featurePrefix) - && !string.IsNullOrEmpty(releasePrefix) - && !string.IsNullOrEmpty(hotfixPrefix); - } - - /// - /// Get git-flow branch prefix. - /// - /// - public string GetFeaturePrefix() { return featurePrefix; } - public string GetReleasePrefix() { return releasePrefix; } - public string GetHotfixPrefix() { return hotfixPrefix; } - - /// - /// Enable git-flow - /// - /// - /// - /// - /// - /// - /// - public void EnableGitFlow(string master, string develop, string feature, string release, string hotfix, string version = "") { - isWatcherDisabled = true; - - var branches = Branches(); - var masterBranch = branches.Find(b => b.Name == master); - var devBranch = branches.Find(b => b.Name == develop); - var refreshBranches = false; - - if (masterBranch == null) { - var errs = RunCommand($"branch --no-track {master}", null); - if (errs != null) { - App.RaiseError(errs); - isWatcherDisabled = false; - return; - } - - refreshBranches = true; - } - - if (devBranch == null) { - var errs = RunCommand($"branch --no-track {develop}", null); - if (errs != null) { - App.RaiseError(errs); - if (refreshBranches) { - Branches(true); - OnBranchChanged?.Invoke(); - OnCommitsChanged?.Invoke(); - OnWorkingCopyChanged?.Invoke(); - } - isWatcherDisabled = false; - return; - } - - refreshBranches = true; - } - - SetConfig("gitflow.branch.master", master); - SetConfig("gitflow.branch.develop", develop); - SetConfig("gitflow.prefix.feature", feature); - SetConfig("gitflow.prefix.bugfix", "bugfix"); - SetConfig("gitflow.prefix.release", release); - SetConfig("gitflow.prefix.hotfix", hotfix); - SetConfig("gitflow.prefix.support", "support"); - SetConfig("gitflow.prefix.versiontag", version); - - RunCommand("flow init -d", null); - - featurePrefix = GetConfig("gitflow.prefix.feature"); - releasePrefix = GetConfig("gitflow.prefix.release"); - hotfixPrefix = GetConfig("gitflow.prefix.hotfix"); - - if (!IsGitFlowEnabled()) App.RaiseError(App.Text("InitGitFlowFailed")); - - if (refreshBranches) { - Branches(true); - OnBranchChanged?.Invoke(); - OnCommitsChanged?.Invoke(); - OnWorkingCopyChanged?.Invoke(); - } - - isWatcherDisabled = false; - } - - /// - /// Start git-flow branch - /// - /// - /// - public void StartGitFlowBranch(Branch.Type type, string name) { - isWatcherDisabled = true; - - string args; - switch (type) { - case Branch.Type.Feature: args = $"flow feature start {name}"; break; - case Branch.Type.Release: args = $"flow release start {name}"; break; - case Branch.Type.Hotfix: args = $"flow hotfix start {name}"; break; - default: - App.RaiseError(App.Text("BadGitFlowType")); - return; - } - - var errs = RunCommand(args, null); - AssertCommand(errs); - } - - /// - /// Finish git-flow branch - /// - /// - public void FinishGitFlowBranch(Branch branch) { - isWatcherDisabled = true; - - string args; - switch (branch.Kind) { - case Branch.Type.Feature: - args = $"flow feature finish {branch.Name.Substring(featurePrefix.Length)}"; - break; - case Branch.Type.Release: - var releaseName = branch.Name.Substring(releasePrefix.Length); - args = $"flow release finish {releaseName} -m \"Release done\""; - break; - case Branch.Type.Hotfix: - var hotfixName = branch.Name.Substring(hotfixPrefix.Length); - args = $"flow hotfix finish {hotfixName} -m \"Hotfix done\""; - break; - default: - App.RaiseError(App.Text("BadGitFlowType")); - return; - } - - var errs = RunCommand(args, null); - AssertCommand(errs); - OnTagChanged?.Invoke(); - } - #endregion - - #region METHOD_COMMITMSG - public void RecordCommitMessage(string message) { - if (string.IsNullOrEmpty(message)) return; - - int exists = CommitMsgRecords.Count; - if (exists > 0) { - var last = CommitMsgRecords[0]; - if (last == message) return; - } - - if (exists >= 10) { - CommitMsgRecords.RemoveRange(9, exists - 9); - } - - CommitMsgRecords.Insert(0, message); - } - #endregion - } -} diff --git a/src/Git/Stash.cs b/src/Git/Stash.cs deleted file mode 100644 index d83f85f0..00000000 --- a/src/Git/Stash.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SourceGit.Git { - - /// - /// Git stash - /// - public class Stash { - - /// - /// SHA for this stash - /// - public string SHA { get; set; } - - /// - /// Name - /// - public string Name { get; set; } - - /// - /// Author - /// - public User Author { get; set; } = new User(); - - /// - /// Message - /// - public string Message { get; set; } - - /// - /// Stash push. - /// - /// - /// - /// - /// - public static void Push(Repository repo, bool includeUntracked, string message, List files) { - string specialFiles = ""; - - if (files.Count > 0) { - specialFiles = " --"; - foreach (var f in files) specialFiles += $" \"{f}\""; - } - - string args = "stash push "; - if (includeUntracked) args += "-u "; - if (!string.IsNullOrEmpty(message)) args += $"-m \"{message}\" "; - - var errs = repo.RunCommand(args + specialFiles, null); - if (errs != null) App.RaiseError(errs); - } - - /// - /// Get changed file list in this stash. - /// - /// - /// - public List GetChanges(Repository repo) { - List changes = new List(); - - var errs = repo.RunCommand($"diff --name-status --pretty=format: {SHA}^ {SHA}", line => { - var change = Change.Parse(line); - if (change != null) changes.Add(change); - }); - - if (errs != null) App.RaiseError(errs); - return changes; - } - - /// - /// Apply stash. - /// - /// - public void Apply(Repository repo) { - var errs = repo.RunCommand($"stash apply -q {Name}", null); - if (errs != null) App.RaiseError(errs); - } - - /// - /// Pop stash - /// - /// - public void Pop(Repository repo) { - var errs = repo.RunCommand($"stash pop -q {Name}", null); - if (errs != null) App.RaiseError(errs); - } - - /// - /// Drop stash - /// - /// - public void Drop(Repository repo) { - var errs = repo.RunCommand($"stash drop -q {Name}", null); - if (errs != null) App.RaiseError(errs); - } - } -} diff --git a/src/Git/Tag.cs b/src/Git/Tag.cs deleted file mode 100644 index fbfbf432..00000000 --- a/src/Git/Tag.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.RegularExpressions; - -namespace SourceGit.Git { - - /// - /// Git tag. - /// - public class Tag { - private static readonly Regex FORMAT = new Regex(@"\$(.*)\$(.*)\$(.*)"); - - /// - /// SHA - /// - public string SHA { get; set; } - - /// - /// Display name. - /// - public string Name { get; set; } - - /// - /// Enable filter in log histories. - /// - public bool IsFiltered { get; set; } - - /// - /// Load all tags - /// - /// - /// - public static List Load(Repository repo) { - var args = "for-each-ref --sort=-creatordate --format=\"$%(refname:short)$%(objectname)$%(*objectname)\" refs/tags"; - var tags = new List(); - - repo.RunCommand(args, line => { - var match = FORMAT.Match(line); - if (!match.Success) return; - - var name = match.Groups[1].Value; - var commit = match.Groups[2].Value; - var dereference = match.Groups[3].Value; - - if (string.IsNullOrEmpty(dereference)) { - tags.Add(new Tag() { - Name = name, - SHA = commit, - }); - } else { - tags.Add(new Tag() { - Name = name, - SHA = dereference, - }); - } - }); - - return tags; - } - - /// - /// Add new tag. - /// - /// - /// - /// - /// - public static void Add(Repository repo, string name, string startPoint, string message) { - var args = $"tag -a {name} {startPoint} "; - - if (!string.IsNullOrEmpty(message)) { - string temp = Path.GetTempFileName(); - File.WriteAllText(temp, message); - args += $"-F \"{temp}\""; - } else { - args += $"-m {name}"; - } - - var errs = repo.RunCommand(args, null); - if (errs != null) App.RaiseError(errs); - else repo.OnCommitsChanged?.Invoke(); - } - - /// - /// Delete tag. - /// - /// - /// - /// - public static void Delete(Repository repo, string name, bool push) { - var errs = repo.RunCommand($"tag --delete {name}", null); - if (errs != null) { - App.RaiseError(errs); - return; - } - - if (push) { - var remotes = repo.Remotes(); - foreach (var r in remotes) { - repo.RunCommand($"-c credential.helper=manager push --delete {r.Name} refs/tags/{name}", null); - } - } - - repo.LogFilters.Remove(name); - repo.OnCommitsChanged?.Invoke(); - } - - /// - /// Push tag to remote. - /// - /// - /// - /// - public static void Push(Repository repo, string name, string remote) { - var errs = repo.RunCommand($"-c credential.helper=manager push {remote} refs/tags/{name}", null); - if (errs != null) App.RaiseError(errs); - } - } -} diff --git a/src/Git/Version.cs b/src/Git/Version.cs deleted file mode 100644 index 02020610..00000000 --- a/src/Git/Version.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace SourceGit.Git { - - /// - /// Version information. - /// - public class Version { - [JsonPropertyName("id")] - public ulong Id { get; set; } - [JsonPropertyName("tag_name")] - public string TagName { get; set; } - [JsonPropertyName("target_commitish")] - public string CommitSHA { get; set; } - [JsonPropertyName("prerelease")] - public bool PreRelease { get; set; } - [JsonPropertyName("name")] - public string Name { get; set; } - [JsonPropertyName("body")] - public string Body { get; set; } - [JsonPropertyName("created_at")] - public DateTime CreatedAt { get; set; } - } -} diff --git a/src/Helpers/CommitGraph.cs b/src/Helpers/CommitGraph.cs deleted file mode 100644 index 3820daa6..00000000 --- a/src/Helpers/CommitGraph.cs +++ /dev/null @@ -1,378 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows; -using System.Windows.Media; - -namespace SourceGit.Helpers { - - /// - /// Tools to parse commit graph. - /// - public class CommitGraphData { - /// - /// Unit lengths for commit graph - /// - public static readonly double UNIT_WIDTH = 12; - public static readonly double HALF_WIDTH = 6; - public static readonly double UNIT_HEIGHT = 24; - public static readonly double HALF_HEIGHT = 12; - - /// - /// Colors - /// - public static Brush[] Colors = new Brush[] { - Brushes.Orange, - Brushes.ForestGreen, - Brushes.Gold, - Brushes.Magenta, - Brushes.Red, - Brushes.Gray, - Brushes.Turquoise, - Brushes.Olive, - }; - - /// - /// Data to draw lines. - /// - public class Line { - private double lastX = 0; - private double lastY = 0; - - /// - /// Parent commit id. - /// - public string Next { get; set; } - - /// - /// Is merged into this tree. - /// - public bool IsMerged { get; set; } - - /// - /// Points in line - /// - public List Points { get; set; } - - /// - /// Brush to draw line - /// - public Brush Brush { get; set; } - - /// - /// Current horizontal offset. - /// - public double HorizontalOffset => lastX; - - /// - /// Constructor. - /// - /// Parent commit id - /// Is merged in tree - /// Color index - /// Start point - public Line(string nextCommitId, bool isMerged, int colorIdx, Point startPoint) { - Next = nextCommitId; - IsMerged = isMerged; - Points = new List() { startPoint }; - Brush = Colors[colorIdx % Colors.Length]; - - lastX = startPoint.X; - lastY = startPoint.Y; - } - - /// - /// Line to. - /// - /// - /// - /// - public void AddPoint(double x, double y, bool isEnd = false) { - if (x > lastX) { - Points.Add(new Point(lastX, lastY)); - Points.Add(new Point(x, y - HALF_HEIGHT)); - } else if (x < lastX) { - Points.Add(new Point(lastX, lastY + HALF_HEIGHT)); - Points.Add(new Point(x, y)); - } - - lastX = x; - lastY = y; - - if (isEnd) { - var last = Points.Last(); - if (last.X != lastX || last.Y != lastY) Points.Add(new Point(lastX, lastY)); - } - } - } - - /// - /// Short link between two commits. - /// - public struct ShortLink { - public Point Start; - public Point Control; - public Point End; - public Brush Brush; - } - - /// - /// Dot - /// - public struct Dot { - public Point Center; - public Brush Color; - } - - /// - /// Independent lines in graph - /// - public List Lines { get; set; } = new List(); - - /// - /// Short links. - /// - public List Links { get; set; } = new List(); - - /// - /// All dots. - /// - public List Dots { get; set; } = new List(); - - /// - /// Parse commits. - /// - /// - /// - public static CommitGraphData Parse(List commits) { - CommitGraphData data = new CommitGraphData(); - - List unsolved = new List(); - List ended = new List(); - Dictionary currentMap = new Dictionary(); - double offsetY = -HALF_HEIGHT; - int colorIdx = 0; - - for (int i = 0; i < commits.Count; i++) { - Git.Commit commit = commits[i]; - Line major = null; - bool isMerged = commit.IsHEAD || commit.IsMerged; - int oldCount = unsolved.Count; - - // 更新Y坐标 - offsetY += UNIT_HEIGHT; - - // 找到第一个依赖于本提交的树,将其他依赖于本提交的树标记为终止,并对已存在的线路调整(防止线重合) - double offsetX = -HALF_WIDTH; - foreach (var l in unsolved) { - if (l.Next == commit.SHA) { - if (major == null) { - offsetX += UNIT_WIDTH; - major = l; - - if (commit.Parents.Count > 0) { - major.Next = commit.Parents[0]; - if (!currentMap.ContainsKey(major.Next)) currentMap.Add(major.Next, major); - } else { - major.Next = "ENDED"; - ended.Add(l); - } - - major.AddPoint(offsetX, offsetY); - } else { - ended.Add(l); - } - - isMerged = isMerged || l.IsMerged; - } else { - if (!currentMap.ContainsKey(l.Next)) currentMap.Add(l.Next, l); - offsetX += UNIT_WIDTH; - l.AddPoint(offsetX, offsetY); - } - } - - // 处理本提交为非当前分支HEAD的情况(创建新依赖线路) - if (major == null && commit.Parents.Count > 0) { - offsetX += UNIT_WIDTH; - major = new Line(commit.Parents[0], isMerged, colorIdx, new Point(offsetX, offsetY)); - unsolved.Add(major); - colorIdx++; - } - - // 确定本提交的点的位置 - Point position = new Point(offsetX, offsetY); - if (major != null) { - major.IsMerged = isMerged; - position.X = major.HorizontalOffset; - position.Y = offsetY; - data.Dots.Add(new Dot() { Center = position, Color = major.Brush }); - } else { - data.Dots.Add(new Dot() { Center = position, Color = Brushes.Orange }); - } - - // 处理本提交的其他依赖 - for (int j = 1; j < commit.Parents.Count; j++) { - var parent = commit.Parents[j]; - if (currentMap.ContainsKey(parent)) { - var l = currentMap[parent]; - var link = new ShortLink(); - - link.Start = position; - link.End = new Point(l.HorizontalOffset, offsetY + HALF_HEIGHT); - link.Control = new Point(link.End.X, link.Start.Y); - link.Brush = l.Brush; - data.Links.Add(link); - } else { - offsetX += UNIT_WIDTH; - unsolved.Add(new Line(commit.Parents[j], isMerged, colorIdx, position)); - colorIdx++; - } - } - - // 处理已终止的线 - foreach (var l in ended) { - l.AddPoint(position.X, position.Y, true); - data.Lines.Add(l); - unsolved.Remove(l); - } - - // 加入本次提交 - commit.IsMerged = isMerged; - commit.GraphOffset = Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH); - - // 清理临时数据 - ended.Clear(); - currentMap.Clear(); - } - - // 处理尚未终结的线 - for (int i = 0; i < unsolved.Count; i++) { - var path = unsolved[i]; - var endY = (commits.Count - 0.5) * UNIT_HEIGHT; - - if (path.Points.Count == 1 && path.Points[0].Y == endY) continue; - - path.AddPoint((i + 0.5) * UNIT_WIDTH, endY, true); - data.Lines.Add(path); - } - unsolved.Clear(); - - data.Lines.Sort((l, h) => l.Points[0].Y.CompareTo(h.Points[0].Y)); - return data; - } - } - - /// - /// Visual element to render commit graph - /// - public class CommitGraph : FrameworkElement { - private double offsetY; - private CommitGraphData data; - - public CommitGraph() { - Clear(); - } - - public void Clear() { - offsetY = 0; - data = null; - } - - public void SetCommits(List commits) { - data = CommitGraphData.Parse(commits); - Dispatcher.Invoke(() => InvalidateVisual()); - } - - public void SetOffset(double y) { - offsetY = y * CommitGraphData.UNIT_HEIGHT; - InvalidateVisual(); - } - - protected override void OnRender(DrawingContext dc) { - if (data == null) return; - - var startY = offsetY; - var endY = offsetY + ActualHeight; - - dc.PushTransform(new TranslateTransform(0, -offsetY)); - - // Draw all visible lines. - foreach (var path in data.Lines) { - var last = path.Points[0]; - var size = path.Points.Count; - - if (path.Points[size - 1].Y < startY) continue; - if (last.Y > endY) break; - - var geo = new StreamGeometry(); - var pen = new Pen(path.Brush, 2); - - using (var geoCtx = geo.Open()) { - geoCtx.BeginFigure(last, false, false); - - var ended = false; - for (int i = 1; i < size; i++) { - var cur = path.Points[i]; - - // Fix line NOT shown in graph if cur.Y is too large than current. - if (cur.Y > endY) { - cur.Y = endY; - ended = true; - } - - if (cur.X > last.X) { - geoCtx.QuadraticBezierTo(new Point(cur.X, last.Y), cur, true, false); - } else if (cur.X < last.X) { - if (i < size - 1) { - cur.Y += CommitGraphData.HALF_HEIGHT; - - var midY = (last.Y + cur.Y) / 2; - var midX = (last.X + cur.X) / 2; - geoCtx.PolyQuadraticBezierTo(new Point[] { - new Point(last.X, midY), - new Point(midX, midY), - new Point(cur.X, midY), - cur}, true, false); - } else { - geoCtx.QuadraticBezierTo(new Point(last.X, cur.Y), cur, true, false); - } - } else { - geoCtx.LineTo(cur, true, false); - } - - if (ended) break; - last = cur; - } - } - - geo.Freeze(); - dc.DrawGeometry(null, pen, geo); - } - - // Draw short links - foreach (var link in data.Links) { - if (link.End.Y < startY) continue; - if (link.Start.Y > endY) break; - - var geo = new StreamGeometry(); - var pen = new Pen(link.Brush, 2); - - using (var geoCtx = geo.Open()) { - geoCtx.BeginFigure(link.Start, false, false); - geoCtx.QuadraticBezierTo(link.Control, link.End, true, false); - } - - geo.Freeze(); - dc.DrawGeometry(null, pen, geo); - } - - // Draw visible points - foreach (var dot in data.Dots) { - if (dot.Center.Y < startY) continue; - if (dot.Center.Y > endY) break; - - dc.DrawEllipse(dot.Color, null, dot.Center, 3, 3); - } - } - } -} diff --git a/src/Helpers/TextBoxHelper.cs b/src/Helpers/TextBoxHelper.cs deleted file mode 100644 index dc3c2fb5..00000000 --- a/src/Helpers/TextBoxHelper.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; - -namespace SourceGit.Helpers { - - /// - /// Attached properties to TextBox. - /// - public static class TextBoxHelper { - - /// - /// Auto scroll on text changed or selection changed. - /// - public static readonly DependencyProperty AutoScrollProperty = DependencyProperty.RegisterAttached( - "AutoScroll", - typeof(bool), - typeof(TextBoxHelper), - new PropertyMetadata(false, OnAutoScrollChanged)); - - /// - /// Placeholder property - /// - public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached( - "Placeholder", - typeof(string), - typeof(TextBoxHelper), - new PropertyMetadata(string.Empty, OnPlaceholderChanged)); - - /// - /// Vertical alignment for placeholder. - /// - public static readonly DependencyProperty PlaceholderBaselineProperty = DependencyProperty.RegisterAttached( - "PlaceholderBaseline", - typeof(AlignmentY), - typeof(TextBoxHelper), - new PropertyMetadata(AlignmentY.Center)); - - /// - /// Property to store generated placeholder brush. - /// - public static readonly DependencyProperty PlaceholderBrushProperty = DependencyProperty.RegisterAttached( - "PlaceholderBrush", - typeof(Brush), - typeof(TextBoxHelper), - new PropertyMetadata(Brushes.Transparent)); - - /// - /// Setter for AutoScrollProperty - /// - /// - /// - public static void SetAutoScroll(UIElement element, bool enabled) { - element.SetValue(AutoScrollProperty, enabled); - } - - /// - /// Getter for AutoScrollProperty - /// - /// - /// - public static bool GetAutoScroll(UIElement element) { - return (bool)element.GetValue(AutoScrollProperty); - } - - /// - /// Triggered when AutoScroll property changed. - /// - /// - /// - public static void OnAutoScrollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - var textBox = d as TextBox; - if (textBox == null) return; - - textBox.SelectionChanged -= UpdateScrollOnSelectionChanged; - if ((bool)e.NewValue == true) { - textBox.SelectionChanged += UpdateScrollOnSelectionChanged; - } - } - - /// - /// Triggered when placeholder changed. - /// - /// - /// - private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - var textBox = d as TextBox; - if (textBox != null) textBox.Loaded += OnTextLoaded; - } - - /// - /// Setter for Placeholder property - /// - /// - /// - public static void SetPlaceholder(UIElement element, string value) { - element.SetValue(PlaceholderProperty, value); - } - - /// - /// Getter for Placeholder property - /// - /// - /// - public static string GetPlaceholder(UIElement element) { - return (string)element.GetValue(PlaceholderProperty); - } - - /// - /// Setter for PlaceholderBaseline property - /// - /// - /// - public static void SetPlaceholderBaseline(UIElement element, AlignmentY align) { - element.SetValue(PlaceholderBaselineProperty, align); - } - - /// - /// Setter for PlaceholderBaseline property. - /// - /// - /// - public static AlignmentY GetPlaceholderBaseline(UIElement element) { - return (AlignmentY)element.GetValue(PlaceholderBaselineProperty); - } - - /// - /// Setter for PlaceholderBrush property. - /// - /// - /// - public static void SetPlaceholderBrush(UIElement element, Brush value) { - element.SetValue(PlaceholderBrushProperty, value); - } - - /// - /// Getter for PlaceholderBrush property. - /// - /// - /// - public static Brush GetPlaceholderBrush(UIElement element) { - return (Brush)element.GetValue(PlaceholderBrushProperty); - } - - /// - /// Set placeholder as background when TextBox was loaded. - /// - /// - /// - private static void OnTextLoaded(object sender, RoutedEventArgs e) { - var textBox = sender as TextBox; - if (textBox == null) return; - - Label placeholder = new Label(); - placeholder.Content = textBox.GetValue(PlaceholderProperty); - - VisualBrush brush = new VisualBrush(); - brush.AlignmentX = AlignmentX.Left; - brush.AlignmentY = GetPlaceholderBaseline(textBox); - brush.TileMode = TileMode.None; - brush.Stretch = Stretch.None; - brush.Opacity = 0.3; - brush.Visual = placeholder; - - textBox.SetValue(PlaceholderBrushProperty, brush); - textBox.Background = brush; - textBox.TextChanged += UpdatePlaceholder; - UpdatePlaceholder(textBox, null); - } - - /// - /// Dynamically hide/show placeholder. - /// - /// - /// - private static void UpdatePlaceholder(object sender, RoutedEventArgs e) { - var textBox = sender as TextBox; - if (string.IsNullOrEmpty(textBox.Text)) { - textBox.Background = textBox.GetValue(PlaceholderBrushProperty) as Brush; - } else { - textBox.Background = Brushes.Transparent; - } - } - - /// - /// - /// - /// - /// - private static void UpdateScrollOnSelectionChanged(object sender, RoutedEventArgs e) { - var textBox = sender as TextBox; - if (textBox != null && textBox.IsFocused) { - if (Mouse.LeftButton == MouseButtonState.Pressed && textBox.SelectionLength > 0) { - var p = Mouse.GetPosition(textBox); - if (p.X <= 8) { - textBox.LineLeft(); - } else if (p.X >= textBox.ActualWidth - 8) { - textBox.LineRight(); - } - - if (p.Y <= 8) { - textBox.LineUp(); - } else if (p.Y >= textBox.ActualHeight - 8) { - textBox.LineDown(); - } - } else { - var rect = textBox.GetRectFromCharacterIndex(textBox.CaretIndex); - if (rect.Left <= 0) { - textBox.ScrollToHorizontalOffset(textBox.HorizontalOffset + rect.Left); - } else if (rect.Right >= textBox.ActualWidth) { - textBox.ScrollToHorizontalOffset(textBox.HorizontalOffset + rect.Right); - } - - if (rect.Top <= 0) { - textBox.ScrollToVerticalOffset(textBox.VerticalOffset + rect.Top); - } else if (rect.Bottom >= textBox.ActualHeight) { - textBox.ScrollToVerticalOffset(textBox.VerticalOffset + rect.Bottom); - } - } - } - } - } -} diff --git a/src/Helpers/TreeViewHelper.cs b/src/Helpers/TreeViewHelper.cs deleted file mode 100644 index 4eb10037..00000000 --- a/src/Helpers/TreeViewHelper.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; - -namespace SourceGit.Helpers { - - /// - /// Helper class to enable multi-selection of TreeView - /// - public static class TreeViewHelper { - - /// - /// Definition of EnableMultiSelection property. - /// - public static readonly DependencyProperty EnableMultiSelectionProperty = - DependencyProperty.RegisterAttached( - "EnableMultiSelection", - typeof(bool), - typeof(TreeViewHelper), - new FrameworkPropertyMetadata(false, OnEnableMultiSelectionChanged)); - - /// - /// Getter of EnableMultiSelection - /// - /// - /// - public static bool GetEnableMultiSelection(DependencyObject obj) { - return (bool)obj.GetValue(EnableMultiSelectionProperty); - } - - /// - /// Setter of EnableMultiSelection - /// - /// - /// - public static void SetEnableMultiSelection(DependencyObject obj, bool value) { - obj.SetValue(EnableMultiSelectionProperty, value); - } - - /// - /// Definition of SelectedItems - /// - public static readonly DependencyProperty SelectedItemsProperty = - DependencyProperty.RegisterAttached( - "SelectedItems", - typeof(ObservableCollection), - typeof(TreeViewHelper), - new FrameworkPropertyMetadata(null)); - - /// - /// Getter of SelectedItems - /// - /// - /// - public static ObservableCollection GetSelectedItems(DependencyObject obj) { - return (ObservableCollection)obj.GetValue(SelectedItemsProperty); - } - - /// - /// Setter of SelectedItems - /// - /// - /// - public static void SetSelectedItems(DependencyObject obj, ObservableCollection value) { - obj.SetValue(SelectedItemsProperty, value); - } - - /// - /// Definition of IsChecked property. - /// - public static readonly DependencyProperty IsCheckedProperty = - DependencyProperty.RegisterAttached( - "IsChecked", - typeof(bool), - typeof(TreeViewHelper), - new FrameworkPropertyMetadata(false)); - - /// - /// Getter of IsChecked Property. - /// - /// - /// - public static bool GetIsChecked(DependencyObject obj) { - return (bool)obj.GetValue(IsCheckedProperty); - } - - /// - /// Setter of IsChecked property - /// - /// - /// - public static void SetIsChecked(DependencyObject obj, bool value) { - obj.SetValue(IsCheckedProperty, value); - } - - /// - /// Definition of MultiSelectionChangedEvent - /// - public static readonly RoutedEvent MultiSelectionChangedEvent = - EventManager.RegisterRoutedEvent("MultiSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TreeViewHelper)); - - /// - /// Add handler for MultiSelectionChanged event. - /// - /// - /// - public static void AddMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) { - var tree = d as TreeView; - if (tree != null) tree.AddHandler(MultiSelectionChangedEvent, handler); - } - - /// - /// Remove handler for MultiSelectionChanged event. - /// - /// - /// - public static void RemoveMultiSelectionChangedHandler(DependencyObject d, RoutedEventHandler handler) { - var tree = d as TreeView; - if (tree != null) tree.RemoveHandler(MultiSelectionChangedEvent, handler); - } - - /// - /// Find ScrollViewer of a tree view - /// - /// - /// - public static ScrollViewer GetScrollViewer(FrameworkElement owner) { - if (owner == null) return null; - if (owner is ScrollViewer) return owner as ScrollViewer; - - int n = VisualTreeHelper.GetChildrenCount(owner); - for (int i = 0; i < n; i++) { - var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement; - var deep = GetScrollViewer(child); - if (deep != null) return deep; - } - - return null; - } - - /// - /// Select all items in tree. - /// - /// - public static void SelectWholeTree(TreeView tree) { - var selected = GetSelectedItems(tree); - selected.Clear(); - SelectAll(selected, tree); - tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent)); - } - - /// - /// Selected one item by DataContext - /// - /// - /// - public static void SelectOneByContext(TreeView tree, object obj) { - var item = FindTreeViewItemByDataContext(tree, obj); - if (item != null) { - var selected = GetSelectedItems(tree); - selected.Add(item); - item.SetValue(IsCheckedProperty, true); - tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent)); - } - } - - /// - /// Unselect the whole tree. - /// - /// - public static void UnselectTree(TreeView tree) { - var selected = GetSelectedItems(tree); - if (selected.Count == 0) return; - - foreach (var old in selected) old.SetValue(IsCheckedProperty, false); - selected.Clear(); - tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent)); - } - - /// - /// Hooks when EnableMultiSelection changed. - /// - /// - /// - private static void OnEnableMultiSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - var tree = d as TreeView; - if (tree != null && (bool)e.NewValue) { - tree.SetValue(SelectedItemsProperty, new ObservableCollection()); - tree.PreviewMouseDown += OnTreeMouseDown; - } - } - - /// - /// Preview mouse button select. - /// - /// - /// - private static void OnTreeMouseDown(object sender, MouseButtonEventArgs e) { - var tree = sender as TreeView; - if (tree == null) return; - - var hit = VisualTreeHelper.HitTest(tree, e.GetPosition(tree)); - if (hit == null || hit.VisualHit is null) return; - - var item = FindTreeViewItem(hit.VisualHit as UIElement); - if (item == null) return; - - var selected = GetSelectedItems(tree); - if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) { - if (GetIsChecked(item)) { - selected.Remove(item); - item.SetValue(IsCheckedProperty, false); - } else { - selected.Add(item); - item.SetValue(IsCheckedProperty, true); - } - } else if ((Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) && selected.Count > 0) { - var last = selected.Last(); - if (last == item) return; - - var lastPos = last.PointToScreen(new Point(0, 0)); - var curPos = item.PointToScreen(new Point(0, 0)); - if (lastPos.Y > curPos.Y) { - SelectRange(selected, tree, item, last); - } else { - SelectRange(selected, tree, last, item); - } - - selected.Add(item); - item.SetValue(IsCheckedProperty, true); - } else if (e.RightButton == MouseButtonState.Pressed) { - if (GetIsChecked(item)) return; - - foreach (var old in selected) old.SetValue(IsCheckedProperty, false); - selected.Clear(); - selected.Add(item); - item.SetValue(IsCheckedProperty, true); - } else { - if (selected.Count == 1 && selected[0] == item) return; - - foreach (var old in selected) old.SetValue(IsCheckedProperty, false); - selected.Clear(); - selected.Add(item); - item.SetValue(IsCheckedProperty, true); - } - - tree.RaiseEvent(new RoutedEventArgs(MultiSelectionChangedEvent)); - } - - /// - /// Find TreeViewItem by child element. - /// - /// - /// - /// - public static TreeViewItem FindTreeViewItem(DependencyObject child) { - if (child == null) return null; - if (child is TreeViewItem) return child as TreeViewItem; - if (child is TreeView) return null; - return FindTreeViewItem(VisualTreeHelper.GetParent(child)); - } - - /// - /// Find TreeViewItem by DataContext - /// - /// - /// - /// - private static TreeViewItem FindTreeViewItemByDataContext(ItemsControl control, object obj) { - if (control == null) return null; - if (control.DataContext == obj) return control as TreeViewItem; - - for (int i = 0; i < control.Items.Count; i++) { - var child = control.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl; - var found = FindTreeViewItemByDataContext(child, obj); - if (found != null) return found; - } - - return null; - } - - /// - /// Select all items. - /// - /// - /// - private static void SelectAll(ObservableCollection selected, ItemsControl control) { - for (int i = 0; i < control.Items.Count; i++) { - var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem; - if (child == null) continue; - - selected.Add(child); - child.SetValue(IsCheckedProperty, true); - SelectAll(selected, child); - } - } - - /// - /// Select range items between given. - /// - /// - /// - /// - /// - /// - private static int SelectRange(ObservableCollection selected, ItemsControl control, TreeViewItem from, TreeViewItem to, int matches = 0) { - for (int i = 0; i < control.Items.Count; i++) { - var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem; - if (child == null) continue; - - if (matches == 1) { - if (child == to) return 2; - selected.Add(child); - child.SetValue(IsCheckedProperty, true); - if (TryEndRangeSelection(selected, child, to)) return 2; - } else if (child == from) { - matches = 1; - if (TryEndRangeSelection(selected, child, to)) return 2; - } else { - matches = SelectRange(selected, child, from, to, matches); - if (matches == 2) return 2; - } - } - - return matches; - } - - private static bool TryEndRangeSelection(ObservableCollection selected, TreeViewItem control, TreeViewItem end) { - for (int i = 0; i < control.Items.Count; i++) { - var child = control.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem; - if (child == null) continue; - - if (child == end) { - return true; - } else { - selected.Add(child); - child.SetValue(IsCheckedProperty, true); - - var ended = TryEndRangeSelection(selected, child, end); - if (ended) return true; - } - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/Helpers/Validations.cs b/src/Helpers/Validations.cs deleted file mode 100644 index 5a1b7d36..00000000 --- a/src/Helpers/Validations.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Globalization; -using System.IO; -using System.Text.RegularExpressions; -using System.Windows.Controls; - -namespace SourceGit.Helpers { - - /// - /// Validate clone folder. - /// - public class CloneFolderRule : ValidationRule { - public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - var badPath = App.Text("BadCloneFolder"); - var path = value as string; - return Directory.Exists(path) ? ValidationResult.ValidResult : new ValidationResult(false, badPath); - } - } - - /// - /// Validate git remote URL - /// - public class RemoteUriRule : ValidationRule { - public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - var badUrl = App.Text("BadRemoteUri"); - return Git.Repository.IsValidUrl(value as string) ? ValidationResult.ValidResult : new ValidationResult(false, badUrl); - } - } - - /// - /// Validate tag name. - /// - public class RemoteNameRule : ValidationRule { - public Git.Repository Repo { get; set; } - public Git.Remote Old { get; set; } - - public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - var regex = new Regex(@"^[\w\-\.]+$"); - var name = value as string; - var remotes = Repo.Remotes(); - - if (string.IsNullOrEmpty(name)) return new ValidationResult(false, App.Text("EmptyRemoteName")); - if (!regex.IsMatch(name)) return new ValidationResult(false, App.Text("BadRemoteName")); - - if (Old == null || name != Old.Name) { - foreach (var t in remotes) { - if (t.Name == name) { - return new ValidationResult(false, App.Text("DuplicatedRemoteName")); - } - } - } - - return ValidationResult.ValidResult; - } - } - - /// - /// Validate branch name. - /// - public class BranchNameRule : ValidationRule { - public Git.Repository Repo { get; set; } - public string Prefix { get; set; } = ""; - - public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - var regex = new Regex(@"^[\w\-/\.]+$"); - var name = value as string; - var branches = Repo.Branches(); - - if (string.IsNullOrEmpty(name)) return new ValidationResult(false, App.Text("EmptyBranchName")); - if (!regex.IsMatch(name)) return new ValidationResult(false, App.Text("BadBranchName")); - - name = Prefix + name; - - foreach (var b in branches) { - if (b.Name == name) { - return new ValidationResult(false, App.Text("DuplicatedBranchName")); - } - } - - return ValidationResult.ValidResult; - } - } - - /// - /// Validate tag name. - /// - public class TagNameRule : ValidationRule { - public Git.Repository Repo { get; set; } - - public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - var regex = new Regex(@"^[\w\-\.]+$"); - var name = value as string; - var tags = Repo.Tags(); - - if (string.IsNullOrEmpty(name)) return new ValidationResult(false, App.Text("EmptyTagName")); - if (!regex.IsMatch(name)) return new ValidationResult(false, App.Text("BadTagName")); - - foreach (var t in tags) { - if (t.Name == name) { - return new ValidationResult(false, App.Text("DuplicatedTagName")); - } - } - - return ValidationResult.ValidResult; - } - } - - /// - /// Required for commit subject. - /// - public class CommitSubjectRequiredRule : ValidationRule { - public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - var subject = value as string; - return string.IsNullOrWhiteSpace(subject) ? new ValidationResult(false, App.Text("EmptyCommitMessage")) : ValidationResult.ValidResult; - } - } - - /// - /// Required for patch file. - /// - public class PatchFileRequiredRule : ValidationRule { - public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - var path = value as string; - var succ = !string.IsNullOrEmpty(path) && File.Exists(path); - return !succ ? new ValidationResult(false, App.Text("BadPatchFile")) : ValidationResult.ValidResult; - } - } - - /// - /// Required for submodule path. - /// - public class SubmodulePathRequiredRule : ValidationRule { - public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - var path = value as string; - if (string.IsNullOrEmpty(path)) return ValidationResult.ValidResult; - - var regex = new Regex(@"^[\w\-\._/]+$"); - var succ = regex.IsMatch(path.Trim()); - return !succ ? new ValidationResult(false, App.Text("BadSubmodulePath")) : ValidationResult.ValidResult; - } - } -} diff --git a/src/Models/AvatarServer.cs b/src/Models/AvatarServer.cs new file mode 100644 index 00000000..ab88fd0d --- /dev/null +++ b/src/Models/AvatarServer.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace SourceGit.Models { + + /// + /// 支持的头像服务器 + /// + public class AvatarServer { + public string Name { get; set; } + public string Url { get; set; } + + public static List Supported = new List() { + new AvatarServer("Gravatar官网", "https://www.gravatar.com/avatar/"), + new AvatarServer("Gravatar中国CDN", "https://cdn.s.loli.top/avatar/"), + }; + + public AvatarServer(string name, string url) { + Name = name; + Url = url; + } + } +} diff --git a/src/Models/BlameLine.cs b/src/Models/BlameLine.cs new file mode 100644 index 00000000..e97a4f87 --- /dev/null +++ b/src/Models/BlameLine.cs @@ -0,0 +1,12 @@ +namespace SourceGit.Models { + /// + /// 追溯中的行信息 + /// + public class BlameLine { + public string LineNumber { get; set; } + public string CommitSHA { get; set; } + public string Author { get; set; } + public string Time { get; set; } + public string Content { get; set; } + } +} diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs new file mode 100644 index 00000000..f6d63b50 --- /dev/null +++ b/src/Models/Branch.cs @@ -0,0 +1,16 @@ +namespace SourceGit.Models { + /// + /// 分支数据 + /// + public class Branch { + public string Name { get; set; } + public string FullName { get; set; } + public string Head { get; set; } + public string HeadSubject { get; set; } + public bool IsLocal { get; set; } + public bool IsCurrent { get; set; } + public string Upstream { get; set; } + public string UpstreamTrackStatus { get; set; } + public string Remote { get; set; } + } +} diff --git a/src/Models/CRLFOption.cs b/src/Models/CRLFOption.cs new file mode 100644 index 00000000..059b5060 --- /dev/null +++ b/src/Models/CRLFOption.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace SourceGit.Models { + + /// + /// 自动换行处理方式 + /// + public class CRLFOption { + public string Display { get; set; } + public string Value { get; set; } + public string Desc { get; set; } + + public static List Supported = new List() { + new CRLFOption("TRUE", "true", "Commit as LF, checkout as CRLF"), + new CRLFOption("INPUT", "input", "Only convert for commit"), + new CRLFOption("FALSE", "false", "Do NOT convert"), + }; + + public CRLFOption(string display, string value, string desc) { + Display = display; + Value = value; + Desc = desc; + } + } +} diff --git a/src/Models/Change.cs b/src/Models/Change.cs new file mode 100644 index 00000000..36a32e54 --- /dev/null +++ b/src/Models/Change.cs @@ -0,0 +1,74 @@ +namespace SourceGit.Models { + + /// + /// Git变更 + /// + public class Change { + + /// + /// 显示模式 + /// + public enum DisplayMode { + Tree, + List, + Grid, + } + + /// + /// 变更状态码 + /// + public enum Status { + None, + Modified, + Added, + Deleted, + Renamed, + Copied, + Unmerged, + Untracked, + } + + public Status Index { get; set; } + public Status WorkTree { get; set; } = Status.None; + public string Path { get; set; } = ""; + public string OriginalPath { get; set; } = ""; + + public bool IsAddedToIndex { + get { + if (Index == Status.None || Index == Status.Untracked) return false; + return true; + } + } + + public bool IsConflit { + get { + if (Index == Status.Unmerged || WorkTree == Status.Unmerged) return true; + if (Index == Status.Added && WorkTree == Status.Added) return true; + if (Index == Status.Deleted && WorkTree == Status.Deleted) return true; + return false; + } + } + + public void Set(Status index, Status workTree = Status.None) { + Index = index; + WorkTree = workTree; + + if (index == Status.Renamed || workTree == Status.Renamed) { + var idx = Path.IndexOf('\t'); + if (idx >= 0) { + OriginalPath = Path.Substring(0, idx); + Path = Path.Substring(idx + 1); + } else { + idx = Path.IndexOf(" -> "); + if (idx > 0) { + OriginalPath = Path.Substring(0, idx); + Path = Path.Substring(idx + 4); + } + } + } + + if (Path[0] == '"') Path = Path.Substring(1, Path.Length - 2); + if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"') OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2); + } + } +} diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs new file mode 100644 index 00000000..e78f655a --- /dev/null +++ b/src/Models/Commit.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Windows; + +namespace SourceGit.Models { + /// + /// 提交记录 + /// + public class Commit { + public string SHA { get; set; } = ""; + public string ShortSHA => SHA.Substring(0, 8); + public User Author { get; set; } = new User(); + public User Committer { get; set; } = new User(); + public string Subject { get; set; } = ""; + public string Message { get; set; } = ""; + public List Parents { get; set; } = new List(); + public List Decorators { get; set; } = new List(); + public bool HasDecorators => Decorators.Count > 0; + public bool IsMerged { get; set; } = false; + public Thickness Margin { get; set; } = new Thickness(0); + } +} diff --git a/src/Git/Decorator.cs b/src/Models/Decorator.cs similarity index 55% rename from src/Git/Decorator.cs rename to src/Models/Decorator.cs index d9131712..4197b6d0 100644 --- a/src/Git/Decorator.cs +++ b/src/Models/Decorator.cs @@ -1,7 +1,7 @@ -namespace SourceGit.Git { +namespace SourceGit.Models { /// - /// Decorator type. + /// 修饰类型 /// public enum DecoratorType { None, @@ -12,10 +12,10 @@ } /// - /// Commit decorator. + /// 提交的附加修饰 /// public class Decorator { - public DecoratorType Type { get; set; } - public string Name { get; set; } + public DecoratorType Type { get; set; } = DecoratorType.None; + public string Name { get; set; } = ""; } } diff --git a/src/Models/Exception.cs b/src/Models/Exception.cs new file mode 100644 index 00000000..99d5b92b --- /dev/null +++ b/src/Models/Exception.cs @@ -0,0 +1,15 @@ +using System; + +namespace SourceGit.Models { + + /// + /// 错误通知 + /// + public static class Exception { + public static Action Handler { get; set; } + + public static void Raise(string error) { + Handler?.Invoke(error); + } + } +} diff --git a/src/Models/FileSizeChange.cs b/src/Models/FileSizeChange.cs new file mode 100644 index 00000000..cced7392 --- /dev/null +++ b/src/Models/FileSizeChange.cs @@ -0,0 +1,9 @@ +namespace SourceGit.Models { + /// + /// 文件大小变化 + /// + public class FileSizeChange { + public long OldSize = 0; + public long NewSize = 0; + } +} diff --git a/src/Models/GitFlow.cs b/src/Models/GitFlow.cs new file mode 100644 index 00000000..07c787df --- /dev/null +++ b/src/Models/GitFlow.cs @@ -0,0 +1,36 @@ +namespace SourceGit.Models { + /// + /// GitFlow的分支类型 + /// + public enum GitFlowBranchType { + None, + Feature, + Release, + Hotfix, + } + + /// + /// GitFlow相关设置 + /// + public class GitFlow { + public string Feature { get; set; } + public string Release { get; set; } + public string Hotfix { get; set; } + + public bool IsEnabled { + get { + return !string.IsNullOrEmpty(Feature) + && !string.IsNullOrEmpty(Release) + && !string.IsNullOrEmpty(Hotfix); + } + } + + public GitFlowBranchType GetBranchType(string name) { + if (!IsEnabled) return GitFlowBranchType.None; + if (name.StartsWith(Feature)) return GitFlowBranchType.Feature; + if (name.StartsWith(Release)) return GitFlowBranchType.Release; + if (name.StartsWith(Hotfix)) return GitFlowBranchType.Hotfix; + return GitFlowBranchType.None; + } + } +} diff --git a/src/Models/Group.cs b/src/Models/Group.cs new file mode 100644 index 00000000..22fdf024 --- /dev/null +++ b/src/Models/Group.cs @@ -0,0 +1,12 @@ +namespace SourceGit.Models { + + /// + /// 仓库列表分组 + /// + public class Group { + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public string Parent { get; set; } = ""; + public bool IsExpanded { get; set; } = false; + } +} diff --git a/src/Models/LFSChange.cs b/src/Models/LFSChange.cs new file mode 100644 index 00000000..e77f78ac --- /dev/null +++ b/src/Models/LFSChange.cs @@ -0,0 +1,10 @@ +namespace SourceGit.Models { + /// + /// LFS对象变更 + /// + public class LFSChange { + public LFSObject Old; + public LFSObject New; + public bool IsValid => Old != null || New != null; + } +} diff --git a/src/Models/LFSObject.cs b/src/Models/LFSObject.cs new file mode 100644 index 00000000..4bbc08e6 --- /dev/null +++ b/src/Models/LFSObject.cs @@ -0,0 +1,9 @@ +namespace SourceGit.Models { + /// + /// LFS对象 + /// + public class LFSObject { + public string OID { get; set; } + public long Size { get; set; } + } +} diff --git a/src/Models/Locale.cs b/src/Models/Locale.cs new file mode 100644 index 00000000..4a94fbc3 --- /dev/null +++ b/src/Models/Locale.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace SourceGit.Models { + + /// + /// 支持的语言 + /// + public class Locale { + public string Name { get; set; } + public string Resource { get; set; } + + public static List Supported = new List() { + new Locale("English", "en_US"), + new Locale("简体中文", "zh_CN"), + }; + + public Locale(string name, string res) { + Name = name; + Resource = res; + } + } +} diff --git a/src/Models/MergeOption.cs b/src/Models/MergeOption.cs new file mode 100644 index 00000000..778c1a12 --- /dev/null +++ b/src/Models/MergeOption.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace SourceGit.Models { + /// + /// 合并方式 + /// + public class MergeOption { + public string Name { get; set; } + public string Desc { get; set; } + public string Arg { get; set; } + + public static List Supported = new List() { + new MergeOption("Default", "Fast-forward if possible", ""), + new MergeOption("No Fast-forward", "Always create a merge commit", "--no-ff"), + new MergeOption("Squash", "Use '--squash'", "--squash"), + new MergeOption("Don't commit", "Merge without commit", "--no-commit"), + }; + + public MergeOption(string n, string d, string a) { + Name = n; + Desc = d; + Arg = a; + } + } +} diff --git a/src/Models/MergeTool.cs b/src/Models/MergeTool.cs new file mode 100644 index 00000000..3c8f25b7 --- /dev/null +++ b/src/Models/MergeTool.cs @@ -0,0 +1,119 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.IO; + +namespace SourceGit.Models { + + /// + /// 外部合并工具 + /// + public class MergeTool { + public int Type { get; set; } + public string Name { get; set; } + public string Exec { get; set; } + public string Cmd { get; set; } + public Func Finder { get; set; } + + public static List Supported = new List() { + new MergeTool(0, "--", "", "", () => ""), + new MergeTool(1, "Visual Studio Code", "Code.exe", "-n --wait \"$MERGED\"", FindVSCode), + new MergeTool(2, "Visual Studio 2017/2019", "vsDiffMerge.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\" //m", FindVSMerge), + new MergeTool(3, "Tortoise Merge", "TortoiseMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", FindTortoiseMerge), + new MergeTool(4, "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", FindKDiff3), + new MergeTool(5, "Beyond Compare 4", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", FindBCompare), + }; + + public MergeTool(int type, string name, string exec, string cmd, Func finder) { + Type = type; + Name = name; + Exec = exec; + Cmd = cmd; + Finder = finder; + } + + private static string FindVSCode() { + var root = RegistryKey.OpenBaseKey( + RegistryHive.LocalMachine, + Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); + + var vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1"); + if (vscode != null) { + return vscode.GetValue("DisplayIcon") as string; + } + + vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1"); + if (vscode != null) { + return vscode.GetValue("DisplayIcon") as string; + } + + vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1"); + if (vscode != null) { + return vscode.GetValue("DisplayIcon") as string; + } + + vscode = root.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1"); + if (vscode != null) { + return vscode.GetValue("DisplayIcon") as string; + } + + return ""; + } + + private static string FindVSMerge() { + var dir = @"C:\Program Files (x86)\Microsoft Visual Studio"; + if (Directory.Exists($"{dir}\\2019")) { + dir += "\\2019"; + } else if (Directory.Exists($"{dir}\\2017")) { + dir += "\\2017"; + } else { + return ""; + } + + if (Directory.Exists($"{dir}\\Community")) { + dir += "\\Community"; + } else if (Directory.Exists($"{dir}\\Enterprise")) { + dir += "\\Enterprise"; + } else if (Directory.Exists($"{dir}\\Professional")) { + dir += "\\Professional"; + } else { + return ""; + } + + return $"{dir}\\Common7\\IDE\\CommonExtensions\\Microsoft\\TeamFoundation\\Team Explorer\\vsDiffMerge.exe"; + } + + private static string FindTortoiseMerge() { + var root = RegistryKey.OpenBaseKey( + RegistryHive.LocalMachine, + Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); + + var tortoiseSVN = root.OpenSubKey("SOFTWARE\\TortoiseSVN"); + if (tortoiseSVN == null) return ""; + return tortoiseSVN.GetValue("TMergePath") as string; + } + + private static string FindKDiff3() { + var root = RegistryKey.OpenBaseKey( + RegistryHive.LocalMachine, + Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); + + var kdiff = root.OpenSubKey(@"SOFTWARE\KDiff3\diff-ext"); + if (kdiff == null) return ""; + return kdiff.GetValue("diffcommand") as string; + } + + private static string FindBCompare() { + var root = RegistryKey.OpenBaseKey( + RegistryHive.LocalMachine, + Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); + + var bc = root.OpenSubKey(@"SOFTWARE\Scooter Software\Beyond Compare"); + if (bc == null) return ""; + + var exec = bc.GetValue("ExePath") as string; + var dir = Path.GetDirectoryName(exec); + return $"{dir}\\BComp.exe"; + } + } +} diff --git a/src/Models/Object.cs b/src/Models/Object.cs new file mode 100644 index 00000000..01552f8c --- /dev/null +++ b/src/Models/Object.cs @@ -0,0 +1,21 @@ +namespace SourceGit.Models { + /// + /// 提交中元素类型 + /// + public enum ObjectType { + None, + Blob, + Tree, + Tag, + Commit, + } + + /// + /// Git提交中的元素 + /// + public class Object { + public string SHA { get; set; } + public ObjectType Type { get; set; } + public string Path { get; set; } + } +} diff --git a/src/Models/Preference.cs b/src/Models/Preference.cs new file mode 100644 index 00000000..21ec082f --- /dev/null +++ b/src/Models/Preference.cs @@ -0,0 +1,314 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.IO; + +#if NET48 +using Newtonsoft.Json; +#else +using System.Text.Json; +using System.Text.Json.Serialization; +#endif + +namespace SourceGit.Models { + + /// + /// 程序配置 + /// + public class Preference { + private static readonly string SAVE_PATH = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "SourceGit", + "preference_v4.json"); + private static Preference instance = null; + + /// + /// 通用配置 + /// + public class GeneralInfo { + + /// + /// 显示语言 + /// + public string Locale { get; set; } = "en_US"; + + /// + /// 头像服务器 + /// + public string AvatarServer { get; set; } = "https://www.gravatar.com/avatar/"; + + /// + /// 是否启用深色主题 + /// + public bool UseDarkTheme { get; set; } = true; + + /// + /// 启用更新检测 + /// + public bool CheckForUpdate { get; set; } = true; + + /// + /// 上一次检测的时间(用于控制每天仅第一次启动软件时,检测) + /// + public int LastCheckDay { get; set; } = 0; + + /// + /// 启用自动拉取远程变更(每10分钟一次) + /// + public bool AutoFetchRemotes { get; set; } = true; + } + + /// + /// Git配置 + /// + public class GitInfo { + + /// + /// git.exe所在路径 + /// + public string Path { get; set; } + + /// + /// 默认克隆路径 + /// + public string DefaultCloneDir { get; set; } + } + + /// + /// 外部合并工具配置 + /// + public class MergeToolInfo { + + /// + /// 合并工具类型 + /// + public int Type { get; set; } = 0; + + /// + /// 合并工具可执行文件路径 + /// + public string Path { get; set; } + } + + /// + /// 使用设置 + /// + public class WindowInfo { + + /// + /// 最近一次设置的宽度 + /// + public double Width { get; set; } = 800; + + /// + /// 最近一次设置的高度 + /// + public double Height { get; set; } = 600; + + /// + /// 将提交信息面板与提交记录左右排布 + /// + public bool MoveCommitInfoRight { get; set; } = false; + + /// + /// 使用合并Diff视图 + /// + public bool UseCombinedDiff { get; set; } = false; + + /// + /// 未暂存视图中变更显示方式 + /// + public Change.DisplayMode ChangeInUnstaged { get; set; } = Change.DisplayMode.Tree; + + /// + /// 暂存视图中变更显示方式 + /// + public Change.DisplayMode ChangeInStaged { get; set; } = Change.DisplayMode.Tree; + + /// + /// 提交信息视图中变更显示方式 + /// + public Change.DisplayMode ChangeInCommitInfo { get; set; } = Change.DisplayMode.Tree; + } + + /// + /// 全局配置 + /// + [JsonIgnore] + public static Preference Instance { + get { + if (instance == null) return Load(); + return instance; + } + } + + /// + /// 检测配置是否 + /// + [JsonIgnore] + public bool IsReady { + get { + return !string.IsNullOrEmpty(Git.Path) && File.Exists(Git.Path); + } + } + + #region DATA + public GeneralInfo General { get; set; } = new GeneralInfo(); + public GitInfo Git { get; set; } = new GitInfo(); + public MergeToolInfo MergeTool { get; set; } = new MergeToolInfo(); + public WindowInfo Window { get; set; } = new WindowInfo(); + public List Groups { get; set; } = new List(); + public List Repositories { get; set; } = new List(); + #endregion + + #region LOAD_SAVE + public static Preference Load() { + if (!File.Exists(SAVE_PATH)) { + instance = new Preference(); + } else { +#if NET48 + instance = JsonConvert.DeserializeObject(File.ReadAllText(SAVE_PATH)); +#else + instance = JsonSerializer.Deserialize(File.ReadAllText(SAVE_PATH)); +#endif + } + + if (!instance.IsReady) { + var reg = RegistryKey.OpenBaseKey( + RegistryHive.LocalMachine, + Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32); + var git = reg.OpenSubKey("SOFTWARE\\GitForWindows"); + if (git != null) { + instance.Git.Path = Path.Combine(git.GetValue("InstallPath") as string, "bin", "git.exe"); + } + } + + return instance; + } + + public static void Save() { + var dir = Path.GetDirectoryName(SAVE_PATH); + if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); + +#if NET48 + var data = JsonConvert.SerializeObject(instance, Formatting.Indented); +#else + var data = JsonSerializer.Serialize(instance, new JsonSerializerOptions() { WriteIndented = true }); +#endif + File.WriteAllText(SAVE_PATH, data); + } + #endregion + + #region METHOD_ON_GROUPS + public Group AddGroup(string name, string parentId) { + var group = new Group() { + Name = name, + Id = Guid.NewGuid().ToString(), + Parent = parentId, + IsExpanded = false, + }; + + Groups.Add(group); + Groups.Sort((l, r) => l.Name.CompareTo(r.Name)); + + return group; + } + + public Group FindGroup(string id) { + foreach (var group in Groups) { + if (group.Id == id) return group; + } + return null; + } + + public void RenameGroup(string id, string newName) { + foreach (var group in Groups) { + if (group.Id == id) { + group.Name = newName; + break; + } + } + + Groups.Sort((l, r) => l.Name.CompareTo(r.Name)); + } + + public void RemoveGroup(string id) { + int removedIdx = -1; + + for (int i = 0; i < Groups.Count; i++) { + if (Groups[i].Id == id) { + removedIdx = i; + break; + } + } + + if (removedIdx >= 0) Groups.RemoveAt(removedIdx); + } + + public bool IsSubGroup(string parent, string subId) { + if (string.IsNullOrEmpty(parent)) return false; + if (parent == subId) return true; + + var g = FindGroup(subId); + if (g == null) return false; + + g = FindGroup(g.Parent); + while (g != null) { + if (g.Id == parent) return true; + g = FindGroup(g.Parent); + } + + return false; + } + #endregion + + #region METHOD_ON_REPOSITORIES + public Repository AddRepository(string path, string gitDir, string groupId) { + var repo = FindRepository(path); + if (repo != null) return repo; + + var dir = new DirectoryInfo(path); + repo = new Repository() { + Path = dir.FullName, + GitDir = gitDir, + Name = dir.Name, + GroupId = groupId, + }; + + Repositories.Add(repo); + Repositories.Sort((l, r) => l.Name.CompareTo(r.Name)); + return repo; + } + + public Repository FindRepository(string path) { + var dir = new DirectoryInfo(path); + foreach (var repo in Repositories) { + if (repo.Path == dir.FullName) return repo; + } + return null; + } + + public void RenameRepository(string path, string newName) { + var repo = FindRepository(path); + if (repo == null) return; + + repo.Name = newName; + Repositories.Sort((l, r) => l.Name.CompareTo(r.Name)); + } + + public void RemoveRepository(string path) { + var dir = new DirectoryInfo(path); + var removedIdx = -1; + + for (int i = 0; i < Repositories.Count; i++) { + if (Repositories[i].Path == dir.FullName) { + removedIdx = i; + break; + } + } + + if (removedIdx >= 0) Repositories.RemoveAt(removedIdx); + } + #endregion + } +} \ No newline at end of file diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs new file mode 100644 index 00000000..a4455975 --- /dev/null +++ b/src/Models/Remote.cs @@ -0,0 +1,10 @@ +namespace SourceGit.Models { + + /// + /// 远程 + /// + public class Remote { + public string Name { get; set; } + public string URL { get; set; } + } +} diff --git a/src/Models/Repository.cs b/src/Models/Repository.cs new file mode 100644 index 00000000..d900f12e --- /dev/null +++ b/src/Models/Repository.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; + +#if NET48 +using Newtonsoft.Json; +#else +using System.Text.Json.Serialization; +#endif + +namespace SourceGit.Models { + + /// + /// 仓库 + /// + public class Repository { + + #region PROPERTIES_SAVED + public string Name { get; set; } = ""; + public string Path { get; set; } = ""; + public string GitDir { get; set; } = ""; + public string GroupId { get; set; } = ""; + public int Bookmark { get; set; } = 0; + public List Filters { get; set; } = new List(); + public List CommitMessages { get; set; } = new List(); + #endregion + + #region PROPERTIES_RUNTIME + [JsonIgnore] public List Remotes = new List(); + [JsonIgnore] public List Branches = new List(); + [JsonIgnore] public GitFlow GitFlow = new GitFlow(); + #endregion + + public void PushCommitMessage(string message) { + if (string.IsNullOrEmpty(message)) return; + + int exists = CommitMessages.Count; + if (exists > 0) { + var last = CommitMessages[0]; + if (last == message) return; + } + + if (exists >= 10) { + CommitMessages.RemoveRange(9, exists - 9); + } + + CommitMessages.Insert(0, message); + } + } +} diff --git a/src/Models/ResetMode.cs b/src/Models/ResetMode.cs new file mode 100644 index 00000000..1fec8139 --- /dev/null +++ b/src/Models/ResetMode.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Windows.Media; + +namespace SourceGit.Models { + /// + /// 重置方式 + /// + public class ResetMode { + public string Name { get; set; } + public string Desc { get; set; } + public string Arg { get; set; } + public Brush Color { get; set; } + + public static List Supported = new List() { + new ResetMode("Soft", "Keep all changes. Stage differences", "--soft", Brushes.Green), + new ResetMode("Mixed", "Keep all changes. Unstage differences", "--mixed", Brushes.Yellow), + new ResetMode("Hard", "Discard all changes", "--hard", Brushes.Red), + }; + + public ResetMode(string n, string d, string a, Brush b) { + Name = n; + Desc = d; + Arg = a; + Color = b; + } + } +} diff --git a/src/Models/Stash.cs b/src/Models/Stash.cs new file mode 100644 index 00000000..aece8d3d --- /dev/null +++ b/src/Models/Stash.cs @@ -0,0 +1,11 @@ +namespace SourceGit.Models { + /// + /// 贮藏 + /// + public class Stash { + public string Name { get; set; } = ""; + public string SHA { get; set; } = ""; + public User Author { get; set; } = new User(); + public string Message { get; set; } = ""; + } +} diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs new file mode 100644 index 00000000..20d6f09b --- /dev/null +++ b/src/Models/Tag.cs @@ -0,0 +1,10 @@ +namespace SourceGit.Models { + /// + /// 标签 + /// + public class Tag { + public string Name { get; set; } + public string SHA { get; set; } + public bool IsFiltered { get; set; } + } +} diff --git a/src/Models/TextChanges.cs b/src/Models/TextChanges.cs new file mode 100644 index 00000000..f2700893 --- /dev/null +++ b/src/Models/TextChanges.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace SourceGit.Models { + /// + /// Diff文本文件变化 + /// + public class TextChanges { + + public enum LineMode { + None, + Normal, + Indicator, + Added, + Deleted, + } + + public class Line { + public LineMode Mode = LineMode.Normal; + public string Content = ""; + public string OldLine = ""; + public string NewLine = ""; + + public Line(LineMode mode, string content, string oldLine, string newLine) { + Mode = mode; + Content = content; + OldLine = oldLine; + NewLine = newLine; + } + } + + public bool IsBinary = false; + public List Lines = new List(); + } +} diff --git a/src/Models/TextLine.cs b/src/Models/TextLine.cs new file mode 100644 index 00000000..72084cc0 --- /dev/null +++ b/src/Models/TextLine.cs @@ -0,0 +1,9 @@ +namespace SourceGit.Models { + /// + /// 文件中的一行内容 + /// + public class TextLine { + public int Number { get; set; } + public string Data { get; set; } + } +} diff --git a/src/Git/User.cs b/src/Models/User.cs similarity index 51% rename from src/Git/User.cs rename to src/Models/User.cs index 0e1b4120..6eb358e0 100644 --- a/src/Git/User.cs +++ b/src/Models/User.cs @@ -1,35 +1,19 @@ -using System; +using System; using System.Text.RegularExpressions; -namespace SourceGit.Git { - +namespace SourceGit.Models { /// - /// Git user. + /// Git用户 /// public class User { - private static readonly Regex FORMAT = new Regex(@"\w+ (.*) <([\w\.\-_]+@[\w\.\-_]+)> (\d{10}) [\+\-]\d+"); + private static readonly Regex REG_FORMAT = new Regex(@"\w+ (.*) <(.*)> (\d{10}) [\+\-]\d+"); - /// - /// Name. - /// public string Name { get; set; } = ""; - - /// - /// Email. - /// public string Email { get; set; } = ""; - - /// - /// Operation time. - /// public string Time { get; set; } = ""; - /// - /// Parse user from raw string. - /// - /// Raw string public void Parse(string data) { - var match = FORMAT.Match(data); + var match = REG_FORMAT.Match(data); if (!match.Success) return; var time = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(int.Parse(match.Groups[3].Value)); diff --git a/src/Models/Version.cs b/src/Models/Version.cs new file mode 100644 index 00000000..adc9ada5 --- /dev/null +++ b/src/Models/Version.cs @@ -0,0 +1,63 @@ +using System; + +#if NET48 +using Newtonsoft.Json; +#else +using System.Text.Json; +using System.Text.Json.Serialization; +#endif + +namespace SourceGit.Models { + + /// + /// Gitee开放API中Release信息格式 + /// + public class Version { +#if NET48 + [JsonProperty(PropertyName = "id")] + public ulong Id { get; set; } + [JsonProperty(PropertyName = "tag_name")] + public string TagName { get; set; } + [JsonProperty(PropertyName = "target_commitish")] + public string CommitSHA { get; set; } + [JsonProperty(PropertyName = "prerelease")] + public bool PreRelease { get; set; } + [JsonProperty(PropertyName = "name")] + public string Name { get; set; } + [JsonProperty(PropertyName = "body")] + public string Body { get; set; } + [JsonProperty(PropertyName = "created_at")] + public DateTime CreatedAt { get; set; } +#else + [JsonPropertyName("id")] + public ulong Id { get; set; } + [JsonPropertyName("tag_name")] + public string TagName { get; set; } + [JsonPropertyName("target_commitish")] + public string CommitSHA { get; set; } + [JsonPropertyName("prerelease")] + public bool PreRelease { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("body")] + public string Body { get; set; } + [JsonPropertyName("created_at")] + public DateTime CreatedAt { get; set; } +#endif + public string PublishTime { + get { return CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); } + } + + public string IsPrerelease { + get { return PreRelease ? "YES" : "NO"; } + } + + public static Version Load(string data) { +#if NET48 + return JsonConvert.DeserializeObject(data); +#else + return JsonSerializer.Deserialize(data); +#endif + } + } +} diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs new file mode 100644 index 00000000..e8d6cfc0 --- /dev/null +++ b/src/Models/Watcher.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace SourceGit.Models { + + /// + /// 文件系统更新监视 + /// + public class Watcher { + /// + /// 打开仓库事件 + /// + public static event Action Opened; + + /// + /// 跳转到指定提交的事件 + /// + public event Action Navigate; + /// + /// 工作副本变更 + /// + public event Action WorkingCopyChanged; + /// + /// 分支数据变更 + /// + public event Action BranchChanged; + /// + /// 标签变更 + /// + public event Action TagChanged; + /// + /// 贮藏变更 + /// + public event Action StashChanged; + /// + /// 子模块变更 + /// + public event Action SubmoduleChanged; + + /// + /// 打开仓库事件 + /// + /// + public static void Open(Repository repo) { + if (all.ContainsKey(repo.Path)) return; + + var watcher = new Watcher(); + watcher.Start(repo.Path, repo.GitDir); + all.Add(repo.Path, watcher); + + Opened?.Invoke(repo); + } + + /// + /// 停止指定的监视器 + /// + /// + public static void Close(string repoPath) { + if (!all.ContainsKey(repoPath)) return; + all[repoPath].Stop(); + all.Remove(repoPath); + } + + /// + /// 取得一个仓库的监视器 + /// + /// + /// + public static Watcher Get(string repoPath) { + if (all.ContainsKey(repoPath)) return all[repoPath]; + return null; + } + + /// + /// 暂停或启用监听 + /// + /// + /// + public static void SetEnabled(string repoPath, bool enabled) { + if (all.ContainsKey(repoPath)) { + var watcher = all[repoPath]; + if (enabled) { + if (watcher.lockCount > 0) watcher.lockCount--; + } else { + watcher.lockCount++; + } + } + } + + /// + /// 跳转到指定的提交 + /// + /// + public void NavigateTo(string commit) { + Navigate?.Invoke(commit); + } + + /// + /// 强制刷新 + /// + public void Refresh() { + updateWC = 1; + updateBranch = 1; + updateSubmodules = 1; + updateStashes = 1; + updateTags = 1; + } + + /// + /// 仅强制更新本地变化 + /// + public void RefreshWC() { + updateWC = 0; + WorkingCopyChanged?.Invoke(); + } + + private void Start(string repo, string gitDir) { + wcWatcher = new FileSystemWatcher(); + wcWatcher.Path = repo; + wcWatcher.Filter = "*"; + wcWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.CreationTime; + wcWatcher.IncludeSubdirectories = true; + wcWatcher.Created += OnWorkingCopyChanged; + wcWatcher.Renamed += OnWorkingCopyChanged; + wcWatcher.Changed += OnWorkingCopyChanged; + wcWatcher.Deleted += OnWorkingCopyChanged; + wcWatcher.EnableRaisingEvents = true; + + repoWatcher = new FileSystemWatcher(); + repoWatcher.Path = gitDir; + repoWatcher.Filter = "*"; + repoWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.DirectoryName | NotifyFilters.FileName; + repoWatcher.IncludeSubdirectories = true; + repoWatcher.Created += OnRepositoryChanged; + repoWatcher.Renamed += OnRepositoryChanged; + repoWatcher.Changed += OnRepositoryChanged; + repoWatcher.Deleted += OnRepositoryChanged; + repoWatcher.EnableRaisingEvents = true; + + timer = new Timer(Tick, null, 100, 100); + } + + private void Stop() { + repoWatcher.EnableRaisingEvents = false; + repoWatcher.Dispose(); + repoWatcher = null; + + wcWatcher.EnableRaisingEvents = false; + wcWatcher.Dispose(); + wcWatcher = null; + + timer.Dispose(); + timer = null; + + Navigate = null; + WorkingCopyChanged = null; + BranchChanged = null; + TagChanged = null; + StashChanged = null; + SubmoduleChanged = null; + } + + private void OnRepositoryChanged(object o, FileSystemEventArgs e) { + if (string.IsNullOrEmpty(e.Name)) return; + + if (e.Name.StartsWith("modules", StringComparison.Ordinal)) { + updateSubmodules = DateTime.Now.AddSeconds(1).ToFileTime(); + } else if (e.Name.StartsWith("refs\\tags", StringComparison.Ordinal)) { + updateTags = DateTime.Now.AddSeconds(.5).ToFileTime(); + } else if (e.Name.StartsWith("refs\\stash", StringComparison.Ordinal)) { + updateStashes = DateTime.Now.AddSeconds(.5).ToFileTime(); + } else if (e.Name.Equals("HEAD", StringComparison.Ordinal) || + e.Name.EndsWith("_HEAD", StringComparison.Ordinal) || + e.Name.StartsWith("refs\\heads\\", StringComparison.Ordinal) || + e.Name.StartsWith("refs\\remotes\\", StringComparison.Ordinal)) { + updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime(); + } else if (e.Name.StartsWith("objects\\") || e.Name.Equals("index", StringComparison.Ordinal)) { + updateWC = DateTime.Now.AddSeconds(.5).ToFileTime(); + } + } + + private void OnWorkingCopyChanged(object o, FileSystemEventArgs e) { + if (string.IsNullOrEmpty(e.Name)) return; + if (e.Name == ".git" || e.Name.StartsWith(".git\\", StringComparison.Ordinal)) return; + + updateWC = DateTime.Now.AddSeconds(1).ToFileTime(); + } + + private void Tick(object sender) { + if (lockCount > 0) return; + + var now = DateTime.Now.ToFileTime(); + if (updateBranch > 0 && now > updateBranch) { + BranchChanged?.Invoke(); + WorkingCopyChanged?.Invoke(); + updateBranch = 0; + updateWC = 0; + } + + if (updateWC > 0 && now > updateWC) { + WorkingCopyChanged?.Invoke(); + updateWC = 0; + } + + if (updateSubmodules > 0 && now > updateSubmodules) { + SubmoduleChanged?.Invoke(); + updateSubmodules = 0; + } + + if (updateStashes > 0 && now > updateStashes) { + StashChanged?.Invoke(); + updateStashes = 0; + } + + if (updateTags > 0 && now > updateTags) { + TagChanged?.Invoke(); + updateTags = 0; + } + } + + #region PRIVATES + private static Dictionary all = new Dictionary(); + + private FileSystemWatcher repoWatcher = null; + private FileSystemWatcher wcWatcher = null; + private Timer timer = null; + private int lockCount = 0; + private long updateWC = 0; + private long updateBranch = 0; + private long updateSubmodules = 0; + private long updateStashes = 0; + private long updateTags = 0; + #endregion + } +} diff --git a/src/Models/WhitespaceOption.cs b/src/Models/WhitespaceOption.cs new file mode 100644 index 00000000..9e70437b --- /dev/null +++ b/src/Models/WhitespaceOption.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace SourceGit.Models { + /// + /// 应用补丁时空白字符的处理方式 + /// + public class WhitespaceOption { + public string Name { get; set; } + public string Desc { get; set; } + public string Arg { get; set; } + + public static List Supported = new List() { + new WhitespaceOption("Apply.NoWarn", "Apply.NoWarn.Desc", "nowarn"), + new WhitespaceOption("Apply.Warn", "Apply.Warn.Desc", "warn"), + new WhitespaceOption("Apply.Error", "Apply.Error.Desc", "error"), + new WhitespaceOption("Apply.ErrorAll", "Apply.ErrorAll.Desc", "error-all") + }; + + public WhitespaceOption(string n, string d, string a) { + Name = App.Text(n); + Desc = App.Text(d); + Arg = a; + } + } +} diff --git a/src/Resources/Controls.xaml b/src/Resources/Controls.xaml index 03450990..45515ddb 100644 --- a/src/Resources/Controls.xaml +++ b/src/Resources/Controls.xaml @@ -1,22 +1,23 @@ - - + + + - - + + \ No newline at end of file diff --git a/src/Resources/Icons.xaml b/src/Resources/Icons.xaml index 252c03f7..a31b8f8e 100644 --- a/src/Resources/Icons.xaml +++ b/src/Resources/Icons.xaml @@ -20,10 +20,11 @@ M701.9062029 677.41589899L589.90712068 565.41681675a148.33953321 148.33953321 0 1 0-24.97646381 26.55648342L676.07895931 703.12160261z m-346.38891409-199.50786053a114.97681148 114.97681148 0 1 1 114.85527151 114.97681148A115.09835147 115.09835147 0 0 1 355.45651882 477.90803846z M352 64h320L960 352v320L672 960h-320L64 672v-320L352 64z m161.28 362.688L344.128 256 259.584 341.312 428.736 512l-169.152 170.688L344.128 768 513.28 597.312 682.432 768l84.544-85.312L597.824 512l169.152-170.688L682.432 256 513.28 426.688z M899.1 869.6l-53-305.6H864c14.4 0 26-11.6 26-26V346c0-14.4-11.6-26-26-26H618V138c0-14.4-11.6-26-26-26H432c-14.4 0-26 11.6-26 26v182H160c-14.4 0-26 11.6-26 26v192c0 14.4 11.6 26 26 26h17.9l-53 305.6c-0.3 1.5-0.4 3-0.4 4.4 0 14.4 11.6 26 26 26h723c1.5 0 3-0.1 4.4-0.4 14.2-2.4 23.7-15.9 21.2-30zM204 390h272V182h72v208h272v104H204V390z m468 440V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H416V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H202.8l45.1-260H776l45.1 260H672z - - M51.2 204.8h102.4v102.4H51.2V204.8z m204.8 0h716.8v102.4H256V204.8zM51.2 460.8h102.4v102.4H51.2V460.8z m204.8 0h716.8v102.4H256V460.8z m-204.8 256h102.4v102.4H51.2v-102.4z m204.8 0h716.8v102.4H256v-102.4z - M912 737l0 150L362 887l0-100 0-50 0-150 0-150 0-150L112 287l0-150 450 0 0 150L412 287l0 150L912 437l0 150L412 587l0 150L912 737z - M256 256l220.16 0 0 220.16-220.16 0 0-220.16ZM547.84 256l220.16 0 0 220.16-220.16 0 0-220.16ZM256 547.84l220.16 0 0 220.16-220.16 0 0-220.16ZM547.84 547.84l220.16 0 0 220.16-220.16 0 0-220.16Z + M512 64C264.8 64 64 264.8 64 512s200.8 448 448 448 448-200.8 448-448S759.2 64 512 64z m238.4 641.6l-45.6 45.6L512 557.6 318.4 750.4l-45.6-45.6L467.2 512 273.6 318.4l45.6-45.6L512 467.2l193.6-193.6 45.6 45.6L557.6 512l192.8 193.6z + + M0 33.28h1024v159.573333H0zM0 432.213333h1024v159.573334H0zM0 831.146667h1024v159.573333H0z + M1024 609.6v-224H640v48H256V224h128V0H0v224h128v752h512v48h384V800H640v48H256V561.6h384v48z + M30.191 271.093l240.907 0 0-240.907-240.907 0 0 240.907zM391.544 271.093l240.907 0 0-240.907-240.907 0 0 240.907zM752.907 30.191l0 240.907 240.907 0 0-240.907-240.907 0zM30.191 632.456l240.907 0 0-240.907-240.907 0 0 240.907zM391.544 632.456l240.907 0 0-240.907-240.907 0 0 240.907zM752.907 632.456l240.907 0 0-240.907-240.907 0 0 240.907zM30.191 993.809l240.907 0 0-240.907-240.907 0 0 240.907zM391.544 993.809l240.907 0 0-240.907-240.907 0 0 240.907zM752.907 993.809l240.907 0 0-240.907-240.907 0 0 240.907z M509.44 546.304l270.848-270.912 90.56 90.56-347.52 349.056-0.832-0.768-13.056 13.056-362.624-361.28 91.136-91.264z M256 224l1e-8 115.2L512 544l255.99999999-204.8 1e-8-115.2-256 204.80000001L256 224zM512 684.8l-256-204.8L256 595.2 512 800 768 595.2l0-115.2L512 684.8z @@ -35,7 +36,7 @@ M716.287923 383.129734c0 38.399973-6.501995 75.979947-19.353986 111.819923l-10.649993 29.695979 22.323985 22.374985 229.57984 229.477841c44.544969 44.594969 44.544969 117.094919 0 161.638888a113.612921 113.612921 0 0 1-80.792943 33.483976 113.561921 113.561921 0 0 1-80.792944-33.483976L547.020041 708.607509l-22.322985-22.322985-29.695979 10.649993a331.62177 331.62177 0 0 1-111.871923 19.352986A333.516769 333.516769 0 0 1 49.970385 383.129734c0-39.014973 6.758995-77.158947 20.019986-113.612921L252.570245 452.095687l35.275975 35.327975 35.276976-35.327975 128.972911-128.972911 35.276975-35.224976-35.276975-35.327975-182.629874-182.579874A330.956771 330.956771 0 0 1 383.130154 50.021965 333.516769 333.516769 0 0 1 716.287923 383.129734zM231.578259 31.589978l-22.886984 9.881993a22.219985 22.219985 0 0 0-5.887996 4.197997 19.455987 19.455987 0 0 0 0 27.545981l215.039851 215.244851L288.41022 417.79171 77.824366 207.103856a25.957982 25.957982 0 0 0-17.202988-7.065995 22.783984 22.783984 0 0 0-21.503985 15.00199 400.537722 400.537722 0 0 0-7.526995 16.588988A381.644735 381.644735 0 0 0 0.00042 383.999734c0 211.711853 172.236881 383.999734 383.999734 383.999734 44.287969 0 87.551939-7.525995 128.97291-22.322985L743.116905 975.819323A163.481887 163.481887 0 0 0 859.493824 1023.99929c43.92997 0 85.299941-17.099988 116.377919-48.179967a164.812886 164.812886 0 0 0 0-232.753838L745.676903 512.972644C760.524893 471.551673 767.999888 428.338703 767.999888 383.999734 767.999888 172.287881 595.763007 0 384.000154 0c-52.735963 0-103.935928 10.649993-152.421895 31.589978z M928 499.86a21.16 21.16 0 0 0-18.72-19.88L858 471.68a10.84 10.84 0 0 1-9-9.34c-0.98-6.3-2-12.84-3.4-19.04a11.04 11.04 0 0 1 5.26-11.7l45.56-25.3a20.7 20.7 0 0 0 10-25.66l-7.9-21.8a20.64 20.64 0 0 0-24.26-13.02l-51.1 10a11.02 11.02 0 0 1-11.64-5.62c-2.98-5.58-6.22-11.26-9.6-16.84a11.2 11.2 0 0 1 0.88-13l34-39.28a20.84 20.84 0 0 0 0.78-27.52l-14.84-17.82a20.48 20.48 0 0 0-27.16-4l-44.74 26.86a10.78 10.78 0 0 1-12.78-1.26c-4.94-4.34-10-8.52-14.74-12.38a10.9 10.9 0 0 1-3.44-12.42l18.52-48.8a20.7 20.7 0 0 0-8.62-26.14l-20.18-11.78a20.6 20.6 0 0 0-26.9 5.66L650 192.56a9.2 9.2 0 0 1-11.2 3.44c-1.22-0.5-11.54-4.72-19.56-7.4a10.84 10.84 0 0 1-7.48-10.46l0.78-52.14a20.96 20.96 0 0 0-17.14-21.76l-22.9-4a20.9 20.9 0 0 0-23.5 14.34L532 164.2a10.92 10.92 0 0 1-10.72 7.3h-19.5a11 11 0 0 1-10.6-7.34l-16.92-49.34a20.92 20.92 0 0 0-23.54-14.5l-22.94 4a20.92 20.92 0 0 0-17.12 21.58l0.8 52.32a10.9 10.9 0 0 1-7.72 10.5c-4.58 1.78-14.52 5.58-19.04 7.26-4 1.44-8.36-0.14-11.88-4.2l-32.52-40A20.6 20.6 0 0 0 313.38 146l-20.12 11.66A20.72 20.72 0 0 0 284.62 184l18.5 48.68a11.08 11.08 0 0 1-3.4 12.46c-4.86 4-9.84 8-14.8 12.44a10.76 10.76 0 0 1-12.7 1.28L228 231.48a20.8 20.8 0 0 0-27.22 4L186 253.26a20.62 20.62 0 0 0 0.74 27.5L220.9 320a10.84 10.84 0 0 1 0.9 12.9c-3.42 5.44-6.68 11.16-9.64 16.88a11.06 11.06 0 0 1-11.72 5.64l-51.02-9.86a20.68 20.68 0 0 0-24.28 13.02l-8 21.76a20.76 20.76 0 0 0 10 25.7l45.56 25.3a10.78 10.78 0 0 1 5.3 11.84l-0.48 2.54c-1.04 5.58-2 10.86-2.92 16.48a10.96 10.96 0 0 1-8.92 9.28l-51.38 8.3A20.84 20.84 0 0 0 96 500.32v23.16A20.52 20.52 0 0 0 114.32 544l51.36 8.28a10.82 10.82 0 0 1 9 9.34c0.98 6.32 2 12.84 3.4 19.04a11.04 11.04 0 0 1-5.26 11.7l-45.54 25.34a20.7 20.7 0 0 0-10 25.66l8 21.8a20.66 20.66 0 0 0 24.26 13.02l51.1-9.9a10.98 10.98 0 0 1 11.64 5.62c3 5.6 6.22 11.26 9.6 16.84a11.16 11.16 0 0 1-0.88 13l-34 39.26a20.82 20.82 0 0 0-1 27.54l14.82 17.82a20.46 20.46 0 0 0 27.16 4l44.74-26.86a10.78 10.78 0 0 1 12.78 1.26c4.96 4.34 10 8.52 14.74 12.38a10.94 10.94 0 0 1 3.46 12.42l-18.54 48.8a20.7 20.7 0 0 0 8.62 26.14l20.22 11.68a20.6 20.6 0 0 0 26.9-5.64L374 831.84c2.8-3.46 7.2-5 10.46-3.68 6.96 2.88 11.62 4.5 19.88 7.26a10.88 10.88 0 0 1 7.5 10.46l-0.8 52.1a21 21 0 0 0 17.14 21.76l22.9 4a20.86 20.86 0 0 0 23.5-14.34l17-49.54a10.9 10.9 0 0 1 10.72-7.3h19.5a10.98 10.98 0 0 1 10.6 7.34l16.94 49.34a20.96 20.96 0 0 0 20 14.82 19.48 19.48 0 0 0 3.56-0.32l22.94-4a20.92 20.92 0 0 0 17.12-21.58l-0.8-52.32a10.86 10.86 0 0 1 7.5-10.4c7.68-2.58 13.08-4.66 17.82-6.5l1.2-0.46c6.2-2.14 9.2 0.46 10.94 2.62l33.5 41.26A20.6 20.6 0 0 0 710 878l20.14-11.66a20.7 20.7 0 0 0 8.62-26.2l-18.48-48.68a11.04 11.04 0 0 1 3.38-12.46c4.86-4 9.84-8 14.8-12.44a10.78 10.78 0 0 1 12.76-1.24l44.78 26.8a20.78 20.78 0 0 0 27.22-4l14.8-17.8a20.62 20.62 0 0 0-0.74-27.5l-34.12-39.34a10.84 10.84 0 0 1-0.9-12.9c3.42-5.42 6.68-11.14 9.64-16.88a11.1 11.1 0 0 1 11.72-5.64l50.96 9.94a20.68 20.68 0 0 0 24.28-13.02l7.9-21.76a20.74 20.74 0 0 0-10-25.68l-45.6-25.34a10.8 10.8 0 0 1-5.22-11.78l0.48-2.54c1.04-5.58 2-10.86 2.92-16.48a10.96 10.96 0 0 1 8.92-9.28l51.38-8.28a20.86 20.86 0 0 0 18.36-20.56v-23.42z m-564.9 188a31.6 31.6 0 0 1-50.94 5.32 270.12 270.12 0 0 1 0.84-363.3 31.62 31.62 0 0 1 51 5.54l91.3 160.7a31.7 31.7 0 0 1 0 31.48zM512 782.22a269.5 269.5 0 0 1-56.62-6 31.62 31.62 0 0 1-20.46-46.72l92-160a31.58 31.58 0 0 1 27.4-15.86h184.28a31.6 31.6 0 0 1 30.2 41.06c-34.98 108.64-136.8 187.52-256.8 187.52z m15.02-327.8L436 294.14a31.62 31.62 0 0 1 20.62-46.6 268 268 0 0 1 55.38-5.76c120 0 221.82 78.88 256.74 187.58a31.6 31.6 0 0 1-30.2 41.06h-184a31.56 31.56 0 0 1-27.52-16z - M549.76 627.2h-80.96v-21.44a141.76 141.76 0 0 1 13.12-64 198.08 198.08 0 0 1 52.16-57.28 389.76 389.76 0 0 0 46.72-41.6 55.68 55.68 0 0 0 11.84-34.24 57.92 57.92 0 0 0-20.8-44.8 81.28 81.28 0 0 0-56-18.56 85.12 85.12 0 0 0-56.96 19.52 103.36 103.36 0 0 0-32 59.2l-82.24-10.24a136.32 136.32 0 0 1 48.64-96A171.84 171.84 0 0 1 512 276.48a178.24 178.24 0 0 1 122.56 40.32 121.92 121.92 0 0 1 45.44 93.76 103.04 103.04 0 0 1-16.64 56 366.4 366.4 0 0 1-71.36 72A136.32 136.32 0 0 0 556.16 576a128 128 0 0 0-6.4 51.2z m-81.28 120.32v-89.28h89.28v89.28zM512 64a448 448 0 1 0 448 448A448 448 0 0 0 512 64z m0 832a384 384 0 0 1 0-768 389.12 389.12 0 0 1 384 384 389.12 389.12 0 0 1-384 384z + M549.76 627.2h-80.96v-21.44a141.76 141.76 0 0 1 13.12-64 198.08 198.08 0 0 1 52.16-57.28 389.76 389.76 0 0 0 46.72-41.6 55.68 55.68 0 0 0 11.84-34.24 57.92 57.92 0 0 0-20.8-44.8 81.28 81.28 0 0 0-56-18.56 85.12 85.12 0 0 0-56.96 19.52 103.36 103.36 0 0 0-32 59.2l-82.24-10.24a136.32 136.32 0 0 1 48.64-96A171.84 171.84 0 0 1 512 276.48a178.24 178.24 0 0 1 122.56 40.32 121.92 121.92 0 0 1 45.44 93.76 103.04 103.04 0 0 1-16.64 56 366.4 366.4 0 0 1-71.36 72A136.32 136.32 0 0 0 556.16 576a128 128 0 0 0-6.4 51.2z m-81.28 120.32v-89.28h89.28v89.28zM512 64a448 448 0 1 0 448 448A448 448 0 0 0 512 64z m0 832a384 384 0 0 1 0-768 389.12 389.12 0 0 1 384 384 389.12 389.12 0 0 1-384 384z M64 864h896V288h-396.224a64 64 0 0 1-57.242667-35.376L460.224 160H64v704z m-64 32V128a32 32 0 0 1 32-32h448a32 32 0 0 1 28.624 17.690667L563.776 224H992a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H32a32 32 0 0 1-32-32z M448 64l128 128h448v768H0V64z M832 960l192-512H192L0 960zM128 384L0 960V128h288l128 128h416v128z @@ -48,7 +49,7 @@ M432 0h160c26.6 0 48 21.4 48 48v336h175.4c35.6 0 53.4 43 28.2 68.2L539.4 756.6c-15 15-39.6 15-54.6 0L180.2 452.2c-25.2-25.2-7.4-68.2 28.2-68.2H384V48c0-26.6 21.4-48 48-48z m592 752v224c0 26.6-21.4 48-48 48H48c-26.6 0-48-21.4-48-48V752c0-26.6 21.4-48 48-48h293.4l98 98c40.2 40.2 105 40.2 145.2 0l98-98H976c26.6 0 48 21.4 48 48z m-248 176c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z m128 0c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z M592 768h-160c-26.6 0-48-21.4-48-48V384h-175.4c-35.6 0-53.4-43-28.2-68.2L484.6 11.4c15-15 39.6-15 54.6 0l304.4 304.4c25.2 25.2 7.4 68.2-28.2 68.2H640v336c0 26.6-21.4 48-48 48z m432-16v224c0 26.6-21.4 48-48 48H48c-26.6 0-48-21.4-48-48V752c0-26.6 21.4-48 48-48h272v16c0 61.8 50.2 112 112 112h160c61.8 0 112-50.2 112-112v-16h272c26.6 0 48 21.4 48 48z m-248 176c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z m128 0c0-22-18-40-40-40s-40 18-40 40 18 40 40 40 40-18 40-40z M961.3 319.6L512 577.3 62.7 319.6 512 62l449.3 257.6zM512 628.4L185.4 441.6 62.7 512 512 769.6 961.3 512l-122.7-70.4L512 628.4zM512 820.8L185.4 634 62.7 704.3 512 962l449.3-257.7L838.6 634 512 820.8z - M295.328 472l143.184 276.032S671.184 186.992 1038.096 0c-8.944 133.568-44.8 249.328 17.904 391.792C894.912 427.408 563.792 828.112 456.4 1024 304.272 837.008 125.296 694.544 0 650.016z + M1024 682.6496V1024H0v-341.3504h1024zM1024 256v341.3504H0V256h1024zM362.8032 0C430.8992 0 455.168 16.896 455.168 64.0512v106.5984H85.3504V64.0512C85.3504 16.9472 109.6704 0 177.7664 0h185.0368zM849.92 0c68.096 0 88.7808 15.872 88.7808 62.976v107.6736h-369.7664V63.0272c0-47.104 24.832-63.0272 92.928-63.0272h188.0576z M144 112h736c17.673 0 32 14.327 32 32v736c0 17.673-14.327 32-32 32H144c-17.673 0-32-14.327-32-32V144c0-17.673 14.327-32 32-32z m112 211.24v72.43a8.81 8.81 0 0 0 3.35 7L386.09 509 259.35 615.37a9.32 9.32 0 0 0-3.35 7v72.43a9.2 9.2 0 0 0 15.15 7L492.7 516.04a9.29 9.29 0 0 0 0-14.2l-221.55-185.6a9.2 9.2 0 0 0-15.15 7zM521.57 624a9.82 9.82 0 0 0-9.57 10v60a9.82 9.82 0 0 0 9.57 10h236.86a9.82 9.82 0 0 0 9.57-10v-60a9.82 9.82 0 0 0-9.57-10H521.57z M508.928 556.125091l92.904727 148.759273h124.462546l-79.639273-79.173819 49.245091-49.524363 164.584727 163.700363-164.631273 163.002182-49.152-49.617454 79.36-78.568728h-162.955636l-95.650909-153.227636 41.472-65.349818z m186.973091-394.705455l164.584727 163.700364-164.631273 163.002182-49.152-49.617455L726.109091 359.936H529.687273l-135.540364 223.976727H139.636364v-69.818182h215.133091l135.586909-223.976727h235.938909l-79.639273-79.173818 49.245091-49.524364z diff --git a/src/Resources/Locales/en_US.xaml b/src/Resources/Locales/en_US.xaml index cdd06432..19b9cdb0 100644 --- a/src/Resources/Locales/en_US.xaml +++ b/src/Resources/Locales/en_US.xaml @@ -14,6 +14,7 @@ {0} Bytes FILTER Optional. + ‡Directory|*.this.directory URL : Git Repository URL @@ -39,8 +40,7 @@ Similar to 'error', but shows more Blame - SOURCEGIT - BLAME - Use right mouse button to view commit information. + Right click to see commit info COMMIT SHA AUTHOR MODIFY TIME @@ -60,8 +60,8 @@ Clone Remote Repository Repository URL : Git Repository URL - Parent Folder : - Folder to contain this repository + Parent Folder : + Folder to contain this repository Local Name : Repository name. Optional. Remote Name : @@ -79,11 +79,9 @@ CHANGED CHANGES Search Files ... - SWITCH TO LIST/TREE VIEW FILES Configure - CREDENTIAL User : User name for this repository Email : @@ -226,7 +224,7 @@ NO CHANGES OR ONLY EOL CHANGES BINARY DIFF OLD : - New : + NEW : LFS OBJECT CHANGE Copy @@ -244,10 +242,10 @@ File History - CHANGE FILES DISPLAY MODE - Show as Grid - Show as List - Show as Tree + CHANGE DISPLAY MODE + Show as Grid + Show as List + Show as Tree SELECT FOLDER SELECTED : @@ -275,28 +273,29 @@ MOVE DOWN Source Git - NEW PAGE PREFERENCE ABOUT ERROR - New Page - Welcome Page + + NEW PAGE + New Page + Welcome Page Merge Branch Source Branch : Into : Merge Option : - Welcome to SourceGit :) - Open Local Repository - Clone Remote Repository - REPOSITORIES - DRAG-DROP YOUR FOLDER - Open or init local repository - Add Folder - Add Sub-Folder - Rename - Delete + Welcome to SourceGit :) + Open Local Repository + Clone Remote Repository + REPOSITORIES + DRAG-DROP YOUR FOLDER + Open or init local repository + Add Folder + Add Sub-Folder + Rename + Delete Pull Pull (Fetch & Merge) @@ -310,7 +309,7 @@ Push Changes To Remote Local Branch : Remote : - To : + Remote Branch : Push all tags Force push @@ -349,18 +348,19 @@ RESTART REQUIRED Language : Avatar Server : - Use light theme + Use dark theme Check for update Fetch remotes automatically - GIT INSTANCE + GIT SETTING Install Path : Input path for git.exe Default Clone Dir : Default path to clone repo into - GLOBAL SETTING - Name : - Email : - Auto CRLF + User Name : + Global git user name + User Email : + Global git user email + Enable Auto CRLF : MERGE TOOL Merger : Install Path : @@ -417,12 +417,14 @@ Git has NOT been configured.\nPlease to go [Preference] and configure it first. Path[{0}] not exists! Can NOT locate bash.exe. Make sure bash.exe exists under the same folder with git.exe - BINARY FILE BLAME NOT SUPPORTED!!! + BINARY FILE NOT SUPPORTED!!! + BLAME ON THIS FILE IS NOT SUPPORTED!!! GIT_DIR for this repository NOT FOUND! Initialize Git-flow failed! Bad git-flow branch type! EXISTS and FULL ACCESS CONTROL needed Remote git URL not supported + Bad local repository name Remote name can NOT be null Bad name for remote. Regex: ^[\\w\\-\\.]+$ Duplicated remote name! diff --git a/src/Resources/Locales/zh_CN.xaml b/src/Resources/Locales/zh_CN.xaml index 54eab305..11129439 100644 --- a/src/Resources/Locales/zh_CN.xaml +++ b/src/Resources/Locales/zh_CN.xaml @@ -14,6 +14,7 @@ {0} 字节 过滤 选填 + ‡路径|*.this.directory 仓库地址 : 远程仓库地址 @@ -23,7 +24,7 @@ 关于软件 SourceGit - 开源Git图形客户端 - 应用补丁 + 补丁 应用补丁 补丁文件 : 选择补丁文件 @@ -39,7 +40,6 @@ 与【错误】级别相似,但输出内容更多 逐行追溯 - 追溯 右键点击查看所选行修改记录 提交指纹 修改者 @@ -60,8 +60,8 @@ 克隆远程仓库 远程仓库 : 远程仓库地址 - 父级目录 : - 选择存放本仓库的父级文件夹路径 + 父级目录 : + 选择存放本仓库的父级文件夹路径 本地仓库名 : 本地仓库目录的名字,选填 远程名 : @@ -79,11 +79,9 @@ 变更列表 变更对比 查找文件... - 切换树形/列表模式 文件列表 - 配置 - 仓库凭证 + 仓库配置 用户 : 应用于本仓库的用户名 邮箱 : @@ -244,10 +242,10 @@ 文件历史 - 切换显示模式 - 网格模式 - 列表模式 - 树形模式 + 切换变更显示模式 + 网格模式 + 列表模式 + 树形模式 选择目录... 当前选择 : @@ -275,28 +273,29 @@ 向下移动 Source Git - 新建空白页 偏好设置 关于 出错了 - 新标签页 - 起始页 - 合并分支 + 新建空白页 + 新标签页 + 起始页 + + 合并分支 合并分支 : 目标分支 : 合并方式 : - 欢迎使用本软件 - 打开本地仓库 - 克隆远程仓库 - 仓库列表 - 支持拖放操作 - 打开/初始化本地仓库 - 新建分组 - 新建子分组 - 重命名 - 删除 + 欢迎使用本软件 + 打开本地仓库 + 克隆远程仓库 + 仓库列表 + 支持拖放操作 + 打开/初始化本地仓库 + 新建分组 + 新建子分组 + 重命名 + 删除 拉回 拉回(拉取并合并) @@ -349,7 +348,7 @@ 需要重启软件 显示语言 : 头像服务 : - 启用浅色主题 + 启用—暗色主题 启用检测更新 启用定时自动拉取远程更新 GIT配置 @@ -357,10 +356,11 @@ 填写git.exe所在位置 默认克隆路径 : 默认的仓库本地存放位置 - 全局设置 - 用户名 : - 邮箱 : - 自动换行转换 : + 用户名 : + 默认GIT用户名 + 邮箱 : + 默认GIT用户邮箱 + 自动换行转换 : 外部合并工具 工具 : 安装路径 : @@ -418,11 +418,13 @@ 路径({0})不存在或不可读取! 无法找到bash.exe,请确保其在git.exe同目录中! 二进制文件不支持该操作!!! + 选中文件不支持该操作!!! 获取仓库GIT_DIR失败! 初始化GIT FLOW失败! 不支持的GIT FLOW分支! 目录不存在或不可写!!! 非法的远程仓库地址! + 非法的本地仓库地址! 远程仓库地址不可为空 远程仓库地址包含非法字符!仅支持字母、数字、下划线、横线或英文点号! 远程仓库名已存在! diff --git a/src/Resources/Styles/Border.xaml b/src/Resources/Styles/Border.xaml deleted file mode 100644 index a6f9bcd5..00000000 --- a/src/Resources/Styles/Border.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - \ No newline at end of file diff --git a/src/Resources/Styles/Button.xaml b/src/Resources/Styles/Button.xaml index 64dda4c3..7c588d40 100644 --- a/src/Resources/Styles/Button.xaml +++ b/src/Resources/Styles/Button.xaml @@ -1,10 +1,10 @@ - - - - - \ No newline at end of file diff --git a/src/Resources/Styles/DataGrid.xaml b/src/Resources/Styles/DataGrid.xaml index abb1dd7b..8dcd90dc 100644 --- a/src/Resources/Styles/DataGrid.xaml +++ b/src/Resources/Styles/DataGrid.xaml @@ -3,33 +3,30 @@ diff --git a/src/Resources/Styles/IconButton.xaml b/src/Resources/Styles/IconButton.xaml new file mode 100644 index 00000000..31f4e801 --- /dev/null +++ b/src/Resources/Styles/IconButton.xaml @@ -0,0 +1,43 @@ + + + \ No newline at end of file diff --git a/src/Resources/Styles/Label.xaml b/src/Resources/Styles/Label.xaml deleted file mode 100644 index b24a8e48..00000000 --- a/src/Resources/Styles/Label.xaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/Resources/Styles/ListBox.xaml b/src/Resources/Styles/ListBox.xaml new file mode 100644 index 00000000..ee55ea0d --- /dev/null +++ b/src/Resources/Styles/ListBox.xaml @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/src/Resources/Styles/ListView.xaml b/src/Resources/Styles/ListView.xaml index b38e71af..b7acf25b 100644 --- a/src/Resources/Styles/ListView.xaml +++ b/src/Resources/Styles/ListView.xaml @@ -1,8 +1,7 @@ - - - - - - + + - - diff --git a/src/Resources/Styles/TextBlock.xaml b/src/Resources/Styles/TextBlock.xaml new file mode 100644 index 00000000..90993c2d --- /dev/null +++ b/src/Resources/Styles/TextBlock.xaml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/src/Resources/Styles/TextBox.xaml b/src/Resources/Styles/TextBox.xaml index dcee8a6f..d3de67ed 100644 --- a/src/Resources/Styles/TextBox.xaml +++ b/src/Resources/Styles/TextBox.xaml @@ -1,8 +1,6 @@ - - + xmlns:controls="clr-namespace:SourceGit.Views.Controls"> @@ -48,7 +46,6 @@ - @@ -76,17 +73,18 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/UI/About.xaml.cs b/src/UI/About.xaml.cs deleted file mode 100644 index 46b3f4d9..00000000 --- a/src/UI/About.xaml.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Diagnostics; -using System.Reflection; -using System.Windows; -using System.Windows.Navigation; - -namespace SourceGit.UI { - - /// - /// About dialog - /// - public partial class About : Window { - - /// - /// Constructor - /// - public About() { - InitializeComponent(); - - var asm = Assembly.GetExecutingAssembly().GetName(); - var framework = AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName; - var dotnetVer = framework.Substring(framework.IndexOf("=") + 1); - - version.Content = $"VERSION : v{asm.Version.Major}.{asm.Version.Minor} .NET : {dotnetVer}"; - } - - /// - /// Open source code link - /// - /// - /// - private void OpenSource(object sender, RequestNavigateEventArgs e) { - //Process.Start(e.Uri.AbsoluteUri); - Process.Start(new ProcessStartInfo("cmd", $"/c start {e.Uri.AbsoluteUri}") { CreateNoWindow = true }); - e.Handled = true; - } - - /// - /// Close this dialog - /// - private void Quit(object sender, RoutedEventArgs e) { - Close(); - } - } -} diff --git a/src/UI/AddSubmodule.xaml b/src/UI/AddSubmodule.xaml deleted file mode 100644 index 7e76a091..00000000 --- a/src/UI/AddSubmodule.xaml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/UI/Blame.xaml.cs b/src/UI/Blame.xaml.cs deleted file mode 100644 index 9b1d1478..00000000 --- a/src/UI/Blame.xaml.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Animation; - -namespace SourceGit.UI { - - /// - /// Viewer to show git-blame - /// - public partial class Blame : Window { - private Git.Repository repo = null; - private string lastSHA = null; - private int lastBG = 1; - - /// - /// Background color for blocks. - /// - public static Brush[] BG = new Brush[] { - Brushes.Transparent, - new SolidColorBrush(Color.FromArgb(128, 0, 0, 0)) - }; - - /// - /// Record - /// - public class Record { - public Git.Blame.Line Line { get; set; } - public Brush BG { get; set; } - public int LineNumber { get; set; } - } - - /// - /// Constructor - /// - /// - /// - /// - public Blame(Git.Repository open, string file, string revision) { - InitializeComponent(); - - // Move to center. - var parent = App.Current.MainWindow; - Left = parent.Left + (parent.Width - Width) * 0.5; - Top = parent.Top + (parent.Height - Height) * 0.5; - - // Show loading. - DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1)); - anim.RepeatBehavior = RepeatBehavior.Forever; - loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim); - loading.Visibility = Visibility.Visible; - - // Layout content - blameFile.Content = $"{file}@{revision.Substring(0, 8)}"; - repo = open; - - Task.Run(() => { - var result = repo.BlameFile(file, revision); - var records = new List(); - - if (result.IsBinary) { - var error = new Record(); - error.Line = new Git.Blame.Line() { Content = App.Text("BinaryNotSupported"), CommitSHA = null }; - error.BG = Brushes.Red; - error.LineNumber = 0; - records.Add(error); - } else { - int count = 1; - foreach (var line in result.Lines) { - var r = new Record(); - r.Line = line; - r.BG = GetBG(line); - r.LineNumber = count; - - records.Add(r); - count++; - } - } - - Dispatcher.Invoke(() => { - loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null); - loading.Visibility = Visibility.Collapsed; - - var formatted = new FormattedText( - $"{records.Count}", - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - new Typeface(blame.FontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal), - 12.0, - Brushes.Black, - VisualTreeHelper.GetDpi(this).PixelsPerDip); - - var lineNumberWidth = formatted.Width + 16; - var minWidth = area.ActualWidth - lineNumberWidth; - - if (records.Count * 16 > area.ActualHeight) minWidth -= 8; - - blame.Columns[0].Width = lineNumberWidth; - blame.Columns[1].MinWidth = minWidth; - blame.ItemsSource = records; - blame.UpdateLayout(); - }); - }); - } - - /// - /// Get background brush. - /// - /// - /// - private Brush GetBG(Git.Blame.Line line) { - if (lastSHA != line.CommitSHA) { - lastSHA = line.CommitSHA; - lastBG = 1 - lastBG; - } - - return BG[lastBG]; - } - - /// - /// Click logo - /// - /// - /// - private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) { - var element = e.OriginalSource as FrameworkElement; - if (element == null) return; - - var pos = PointToScreen(new Point(0, 33)); - SystemCommands.ShowSystemMenu(this, pos); - } - - /// - /// Minimize - /// - private void Minimize(object sender, RoutedEventArgs e) { - SystemCommands.MinimizeWindow(this); - } - - /// - /// Maximize/Restore - /// - private void MaximizeOrRestore(object sender, RoutedEventArgs e) { - if (WindowState == WindowState.Normal) { - SystemCommands.MaximizeWindow(this); - } else { - SystemCommands.RestoreWindow(this); - } - } - - /// - /// Quit - /// - private void Quit(object sender, RoutedEventArgs e) { - Close(); - } - - /// - /// Content size changed. - /// - /// - /// - private void OnSizeChanged(object sender, SizeChangedEventArgs e) { - var total = area.ActualWidth; - var offset = blame.NonFrozenColumnsViewportHorizontalOffset; - var minWidth = total - offset; - - var scroller = GetVisualChild(blame); - if (scroller != null && scroller.ComputedVerticalScrollBarVisibility == Visibility.Visible) minWidth -= 8; - - blame.Columns[1].MinWidth = minWidth; - blame.Columns[1].Width = DataGridLength.SizeToCells; - blame.UpdateLayout(); - } - - /// - /// Context menu opening. - /// - /// - /// - private void OnBlameContextMenuOpening(object sender, ContextMenuEventArgs ev) { - var item = sender as DataGridRow; - if (item == null) return; - - var record = item.DataContext as Record; - if (record == null || record.Line.CommitSHA == null) return; - - Hyperlink link = new Hyperlink(new Run(record.Line.CommitSHA)); - link.ToolTip = App.Text("Goto"); - link.Click += (o, e) => { - repo.OnNavigateCommit?.Invoke(record.Line.CommitSHA); - e.Handled = true; - }; - - commitID.Content = link; - authorName.Content = record.Line.Author; - authorTime.Content = record.Line.Time; - popup.IsOpen = true; - ev.Handled = true; - } - - /// - /// Prevent auto scroll. - /// - /// - /// - private void OnBlameRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { - e.Handled = true; - } - - /// - /// Find child element of type. - /// - /// - /// - /// - private T GetVisualChild(DependencyObject parent) where T : Visual { - T child = null; - - int count = VisualTreeHelper.GetChildrenCount(parent); - for (int i = 0; i < count; i++) { - Visual v = (Visual)VisualTreeHelper.GetChild(parent, i); - child = v as T; - - if (child == null) { - child = GetVisualChild(v); - } - - if (child != null) { - break; - } - } - - return child; - } - } -} diff --git a/src/UI/CherryPick.xaml b/src/UI/CherryPick.xaml deleted file mode 100644 index b6081361..00000000 --- a/src/UI/CherryPick.xaml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - -