diff --git a/.gitattributes b/.gitattributes index 69139978..7410eb08 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,78 +1,12 @@ -# Auto detect text files and perform LF normalization -# https://www.davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ * text=auto - -# -# The above will handle all files NOT found below -# - -# Documents -*.bibtex text diff=bibtex -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain *.md text -*.tex text diff=tex -*.adoc text -*.textile text -*.mustache text -*.csv text -*.tab text -*.tsv text -*.txt text -*.sql text - -# Graphics *.png binary -*.jpg binary -*.jpeg binary -*.gif binary -*.tif binary -*.tiff binary *.ico binary -# SVG treated as an asset (binary) by default. -*.svg text -# If you want to treat it as binary, -# use the following line instead. -# *.svg binary -*.eps binary - -# Scripts -*.bash text eol=lf -*.fish text eol=lf *.sh text eol=lf -# These are explicitly windows files and should use crlf *.bat text eol=crlf *.cmd text eol=crlf *.ps1 text eol=crlf - -# Serialisation *.json text -*.toml text -*.xml text -*.yaml text -*.yml text - -# Archives -*.7z binary -*.gz binary -*.tar binary -*.tgz binary -*.zip binary - -# Text files where line endings should be preserved -*.patch -text - -# -# Exclude files from exporting -# .gitattributes export-ignore .gitignore export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0b5677c5..95bde92d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,425 +1,13 @@ -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ +.vscode/ +.idea/ -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Nuke Build - Uncomment if you are using it -.nuke/temp - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json +*.sln.docstates +*.user +*.suo *.code-workspace -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml - -### Linux ### - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### macOS ### -# General .DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 @@ -428,175 +16,14 @@ Icon .VolumeIcon.icns .com.apple.timemachine.donotpresent -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### macOS Patch ### -# iCloud generated files -*.icloud - -### Rider ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -.idea/ - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### VisualStudioCode ### -!.vscode/*.code-snippets - -# Local History for Visual Studio Code - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -### Windows ### -# Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db -# Dump file -*.stackdump +bin/ +obj/ -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files - -# Windows shortcuts -*.lnk - -### Specifics ### - -# Specials -*.zip -archives -package-lock.json -*.private.env.json -**/**/[Dd]ata/*.json -**/**/[Dd]ata/*.csv - -# SpecFlow -*.feature.cs - -# Azurite -*azurite*.json - -# Build Folders -[Pp]ublish -[Oo]utput -[Ss]cripts -[Tt]ests/[Rr]esults - -# LibraryManager -**/lib - -# BuildBundlerMinifier -*.min.* -*.map - -# Sass Output -**/css - -# SQLite files -*.db -*.sqlite3 -*.sqlite -*.db-journal -*.sqlite3-journal -*.sqlite-journal -*.db-shm -*.db-wal - -# SourceGit output files build/resources/ build/SourceGit/ build/SourceGit.app/ @@ -605,3 +32,4 @@ build/*.tar.gz build/*.deb build/*.rpm build/*.AppImage +SourceGit.app/ \ No newline at end of file diff --git a/VERSION b/VERSION index a05d8762..eed4bd77 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.31 \ No newline at end of file +8.32 \ No newline at end of file diff --git a/build/README.md b/build/README.md new file mode 100644 index 00000000..a75f4d73 --- /dev/null +++ b/build/README.md @@ -0,0 +1,15 @@ +# build + +> [!WARNING] +> The files under the `build` folder is used for `Github Action` only, **NOT** for end users. + +## How to build this project manually + +1. Make sure [.NET SDK 8](https://dotnet.microsoft.com/en-us/download) is installed on your machine. +2. Clone this project +3. Run the follow command under the project root dir +```sh +dotnet publish -c Release -r $RUNTIME_IDENTIFIER -o $DESTINATION_FOLDER src/SourceGit.csproj +``` +> [!NOTE] +> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replece the `$DESTINATION_FOLDER` with the real path that will store the output executable files. diff --git a/build/resources/app/App.plist b/build/resources/app/App.plist index a9852019..ba6f40a2 100644 --- a/build/resources/app/App.plist +++ b/build/resources/app/App.plist @@ -12,11 +12,6 @@ SOURCE_GIT_VERSION.0 LSMinimumSystemVersion 11.0 - LSEnvironment - - PATH - /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin - CFBundleExecutable SourceGit CFBundleInfoDictionaryVersion diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 090f060e..682ec5fc 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -516,27 +516,24 @@ namespace SourceGit private bool TryLaunchedAsAskpass(IClassicDesktopStyleApplicationLifetime desktop) { + var launchAsAskpass = Environment.GetEnvironmentVariable("SOURCEGIT_LAUNCH_AS_ASKPASS"); + if (launchAsAskpass is not "TRUE") + return false; + var args = desktop.Args; - if (args == null || args.Length != 1) - return false; + if (args?.Length > 0) + { + desktop.MainWindow = new Views.Askpass(args[0]); + return true; + } - var param = args[0]; - if (Directory.Exists(param)) - return false; - - if (!param.StartsWith("enter passphrase", StringComparison.OrdinalIgnoreCase) && - !param.Contains(" password", StringComparison.OrdinalIgnoreCase)) - return false; - - desktop.MainWindow = new Views.Askpass(param); - return true; + return false; } private void TryLaunchedAsNormal(IClassicDesktopStyleApplicationLifetime desktop) { Native.OS.SetupEnternalTools(); Models.AvatarManager.Instance.Start(); - Models.AutoFetchManager.Instance.Start(); string startupRepo = null; if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0])) diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index c30ba59b..55fc6d17 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -40,59 +40,7 @@ namespace SourceGit.Commands public bool Exec() { - var start = new ProcessStartInfo(); - start.FileName = Native.OS.GitExecutable; - start.Arguments = "--no-pager -c core.quotepath=off "; - start.UseShellExecute = false; - start.CreateNoWindow = true; - start.RedirectStandardOutput = true; - start.RedirectStandardError = true; - start.StandardOutputEncoding = Encoding.UTF8; - start.StandardErrorEncoding = Encoding.UTF8; - - // Force using this app as SSH askpass program - var selfExecFile = Process.GetCurrentProcess().MainModule!.FileName; - if (!OperatingSystem.IsLinux()) - start.Environment.Add("DISPLAY", "required"); - start.Environment.Add("SSH_ASKPASS", selfExecFile); // Can not use parameter here, because it invoked by SSH with `exec` - start.Environment.Add("SSH_ASKPASS_REQUIRE", "prefer"); - - // If an SSH private key was provided, sets the environment. - if (!string.IsNullOrEmpty(SSHKey)) - { - start.Environment.Add("GIT_SSH_COMMAND", $"ssh -o StrictHostKeyChecking=accept-new -i '{SSHKey}'"); - } - else - { - start.Environment.Add("GIT_SSH_COMMAND", $"ssh -o StrictHostKeyChecking=accept-new"); - start.Arguments += "-c credential.helper=manager "; - } - - // Force using en_US.UTF-8 locale to avoid GCM crash - if (OperatingSystem.IsLinux()) - start.Environment.Add("LANG", "en_US.UTF-8"); - - // Force using this app as git editor. - switch (Editor) - { - case EditorType.CoreEditor: - start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --core-editor\" "; - break; - case EditorType.RebaseEditor: - start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --rebase-message-editor\" -c sequence.editor=\"\\\"{selfExecFile}\\\" --rebase-todo-editor\" -c rebase.abbreviateCommands=true "; - break; - default: - start.Arguments += "-c core.editor=true "; - break; - } - - // Append command args - start.Arguments += Args; - - // Working directory - if (!string.IsNullOrEmpty(WorkingDirectory)) - start.WorkingDirectory = WorkingDirectory; - + var start = CreateGitStartInfo(); var errs = new List(); var proc = new Process() { StartInfo = start }; var isCancelled = false; @@ -178,28 +126,15 @@ namespace SourceGit.Commands } return false; } - else - { - return true; - } + + return true; } public ReadToEndResult ReadToEnd() { - var start = new ProcessStartInfo(); - start.FileName = Native.OS.GitExecutable; - 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(WorkingDirectory)) - start.WorkingDirectory = WorkingDirectory; - + var start = CreateGitStartInfo(); var proc = new Process() { StartInfo = start }; + try { proc.Start(); @@ -227,7 +162,68 @@ namespace SourceGit.Commands return rs; } - protected virtual void OnReadline(string line) { } + protected virtual void OnReadline(string line) + { + // Implemented by derived class + } + + private ProcessStartInfo CreateGitStartInfo() + { + var start = new ProcessStartInfo(); + start.FileName = Native.OS.GitExecutable; + start.Arguments = "--no-pager -c core.quotepath=off -c credential.helper=manager "; + start.UseShellExecute = false; + start.CreateNoWindow = true; + start.RedirectStandardOutput = true; + start.RedirectStandardError = true; + start.StandardOutputEncoding = Encoding.UTF8; + start.StandardErrorEncoding = Encoding.UTF8; + + // Force using this app as SSH askpass program + var selfExecFile = Process.GetCurrentProcess().MainModule!.FileName; + if (!OperatingSystem.IsLinux()) + start.Environment.Add("DISPLAY", "required"); + start.Environment.Add("SSH_ASKPASS", selfExecFile); // Can not use parameter here, because it invoked by SSH with `exec` + start.Environment.Add("SSH_ASKPASS_REQUIRE", "prefer"); + start.Environment.Add("SOURCEGIT_LAUNCH_AS_ASKPASS", "TRUE"); + + // If an SSH private key was provided, sets the environment. + if (!string.IsNullOrEmpty(SSHKey)) + start.Environment.Add("GIT_SSH_COMMAND", $"ssh -o StrictHostKeyChecking=accept-new -i '{SSHKey}'"); + else + start.Environment.Add("GIT_SSH_COMMAND", $"ssh -o StrictHostKeyChecking=accept-new"); + + // Force using en_US.UTF-8 locale to avoid GCM crash + if (OperatingSystem.IsLinux()) + start.Environment.Add("LANG", "en_US.UTF-8"); + + // Fix sometimes `LSEnvironment` not working on macOS + if (OperatingSystem.IsMacOS()) + start.Environment.Add("PATH", "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"); + + // Force using this app as git editor. + switch (Editor) + { + case EditorType.CoreEditor: + start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --core-editor\" "; + break; + case EditorType.RebaseEditor: + start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --rebase-message-editor\" -c sequence.editor=\"\\\"{selfExecFile}\\\" --rebase-todo-editor\" -c rebase.abbreviateCommands=true "; + break; + default: + start.Arguments += "-c core.editor=true "; + break; + } + + // Append command args + start.Arguments += Args; + + // Working directory + if (!string.IsNullOrEmpty(WorkingDirectory)) + start.WorkingDirectory = WorkingDirectory; + + return start; + } [GeneratedRegex(@"\d+%")] private static partial Regex REG_PROGRESS(); diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs index 07622821..94b7fde9 100644 --- a/src/Commands/Fetch.cs +++ b/src/Commands/Fetch.cs @@ -22,8 +22,6 @@ namespace SourceGit.Commands Args += "--force "; Args += remote; - - Models.AutoFetchManager.Instance.MarkFetched(repo); } public Fetch(string repo, string remote, string localBranch, string remoteBranch, Action outputHandler) diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index 5f9d31a8..ee82ce88 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -19,29 +19,35 @@ namespace SourceGit.Commands public List Result() { - Exec(); + var branches = new List(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return branches; - foreach (var b in _needQueryTrackStatus) - b.TrackStatus = new QueryTrackStatus(WorkingDirectory, b.Name, b.Upstream).Result(); + var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var b = ParseLine(line); + if (b != null) + branches.Add(b); + } - return _branches; + return branches; } - protected override void OnReadline(string line) + private Models.Branch ParseLine(string line) { var parts = line.Split('$'); if (parts.Length != 5) - return; + return null; var branch = new Models.Branch(); var refName = parts[0]; if (refName.EndsWith("/HEAD", StringComparison.Ordinal)) - return; + return null; - if (refName.StartsWith(PREFIX_DETACHED_AT, StringComparison.Ordinal) || refName.StartsWith(PREFIX_DETACHED_FROM, StringComparison.Ordinal)) - { - branch.IsDetachedHead = true; - } + branch.IsDetachedHead = refName.StartsWith(PREFIX_DETACHED_AT, StringComparison.Ordinal) || + refName.StartsWith(PREFIX_DETACHED_FROM, StringComparison.Ordinal); if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) { @@ -53,7 +59,7 @@ namespace SourceGit.Commands var name = refName.Substring(PREFIX_REMOTE.Length); var shortNameIdx = name.IndexOf('/', StringComparison.Ordinal); if (shortNameIdx < 0) - return; + return null; branch.Remote = name.Substring(0, shortNameIdx); branch.Name = name.Substring(branch.Remote.Length + 1); @@ -71,14 +77,11 @@ namespace SourceGit.Commands branch.Upstream = parts[3]; if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal)) - _needQueryTrackStatus.Add(branch); + branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result(); else branch.TrackStatus = new Models.BranchTrackStatus(); - _branches.Add(branch); + return branch; } - - private readonly List _branches = new List(); - private List _needQueryTrackStatus = new List(); } } diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 8a48eff3..76894412 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -14,21 +14,23 @@ namespace SourceGit.Commands _findFirstMerged = needFindHead; } - public QueryCommits(string repo, string filter, Models.CommitSearchMethod method) + public QueryCommits(string repo, string filter, Models.CommitSearchMethod method, bool onlyCurrentBranch) { - string search; + string search = onlyCurrentBranch ? string.Empty : "--branches --remotes "; if (method == Models.CommitSearchMethod.ByUser) { - search = $"-i --author=\"{filter}\" --committer=\"{filter}\""; + search += $"-i --author=\"{filter}\" --committer=\"{filter}\""; } else if (method == Models.CommitSearchMethod.ByFile) { - search = $"-- \"{filter}\""; + search += $"-- \"{filter}\""; } else { var argsBuilder = new StringBuilder(); + argsBuilder.Append(search); + var words = filter.Split(new[] { ' ', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries); foreach (var word in words) { @@ -36,12 +38,13 @@ namespace SourceGit.Commands argsBuilder.Append($"--grep=\"{escaped}\" "); } argsBuilder.Append("--all-match -i"); + search = argsBuilder.ToString(); } WorkingDirectory = repo; Context = repo; - Args = $"log -1000 --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s --branches --remotes " + search; + Args = $"log -1000 --date-order --no-show-signature --decorate=full --pretty=format:%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search; _findFirstMerged = false; } diff --git a/src/Commands/QueryTags.cs b/src/Commands/QueryTags.cs index 2abbe277..7da324de 100644 --- a/src/Commands/QueryTags.cs +++ b/src/Commands/QueryTags.cs @@ -14,31 +14,43 @@ namespace SourceGit.Commands public List Result() { - Exec(); - return _loaded; + var tags = new List(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return tags; + + var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var tag = ParseLine(line); + if (tag != null) + tags.Add(tag); + } + + return tags; } - protected override void OnReadline(string line) + private Models.Tag ParseLine(string line) { var subs = line.Split('$', StringSplitOptions.RemoveEmptyEntries); if (subs.Length == 2) { - _loaded.Add(new Models.Tag() + return new Models.Tag() { Name = subs[0].Substring(10), SHA = subs[1], - }); + }; } else if (subs.Length == 3) { - _loaded.Add(new Models.Tag() + return new Models.Tag() { Name = subs[0].Substring(10), SHA = subs[2], - }); + }; } - } - private readonly List _loaded = new List(); + return null; + } } } diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs index 6b2296ca..adfcc574 100644 --- a/src/Commands/Statistics.cs +++ b/src/Commands/Statistics.cs @@ -6,21 +6,35 @@ namespace SourceGit.Commands { public Statistics(string repo) { - _statistics = new Models.Statistics(); - WorkingDirectory = repo; Context = repo; - Args = $"log --date-order --branches --remotes --since=\"{_statistics.Since()}\" --pretty=format:\"%ct$%an\""; + Args = $"log --date-order --branches --remotes -40000 --pretty=format:\"%ct$%aN\""; } public Models.Statistics Result() { - Exec(); - _statistics.Complete(); - return _statistics; + var statistics = new Models.Statistics(); + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return statistics; + + var start = 0; + var end = rs.StdOut.IndexOf('\n', start); + while (end > 0) + { + ParseLine(statistics, rs.StdOut.Substring(start, end - start)); + start = end + 1; + end = rs.StdOut.IndexOf('\n', start); + } + + if (start < rs.StdOut.Length) + ParseLine(statistics, rs.StdOut.Substring(start)); + + statistics.Complete(); + return statistics; } - protected override void OnReadline(string line) + private void ParseLine(Models.Statistics statistics, string line) { var dateEndIdx = line.IndexOf('$', StringComparison.Ordinal); if (dateEndIdx == -1) @@ -28,9 +42,7 @@ namespace SourceGit.Commands var dateStr = line.Substring(0, dateEndIdx); if (double.TryParse(dateStr, out var date)) - _statistics.AddCommit(line.Substring(dateEndIdx + 1), date); + statistics.AddCommit(line.Substring(dateEndIdx + 1), date); } - - private readonly Models.Statistics _statistics = null; } } diff --git a/src/Converters/ListConverters.cs b/src/Converters/ListConverters.cs index fef0f584..81cac8b7 100644 --- a/src/Converters/ListConverters.cs +++ b/src/Converters/ListConverters.cs @@ -10,6 +10,9 @@ namespace SourceGit.Converters public static readonly FuncValueConverter ToCount = new FuncValueConverter(v => v == null ? " (0)" : $" ({v.Count})"); + public static readonly FuncValueConverter IsNullOrEmpty = + new FuncValueConverter(v => v == null || v.Count == 0); + public static readonly FuncValueConverter IsNotNullOrEmpty = new FuncValueConverter(v => v != null && v.Count > 0); diff --git a/src/Converters/PathConverters.cs b/src/Converters/PathConverters.cs index 95da6c79..dd7cfa49 100644 --- a/src/Converters/PathConverters.cs +++ b/src/Converters/PathConverters.cs @@ -18,7 +18,7 @@ namespace SourceGit.Converters { if (OperatingSystem.IsWindows()) return v; - + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length; if (v.StartsWith(home, StringComparison.Ordinal)) diff --git a/src/Models/AutoFetchManager.cs b/src/Models/AutoFetchManager.cs deleted file mode 100644 index 313f5f64..00000000 --- a/src/Models/AutoFetchManager.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace SourceGit.Models -{ - public class AutoFetchManager - { - public static AutoFetchManager Instance - { - get - { - if (_instance == null) - _instance = new AutoFetchManager(); - - return _instance; - } - } - - public class Job - { - public string IndexLockFile = string.Empty; - public Commands.Fetch Cmd = null; - public DateTime NextRunTimepoint = DateTime.MinValue; - } - - public bool IsEnabled - { - get; - set; - } = false; - - public int Interval - { - get => _interval; - set - { - _interval = Math.Max(1, value); - - lock (_lock) - { - foreach (var job in _jobs) - job.Value.NextRunTimepoint = DateTime.Now.AddMinutes(_interval * 1.0); - } - } - } - - private static AutoFetchManager _instance = null; - private Dictionary _jobs = new Dictionary(); - private object _lock = new object(); - private int _interval = 10; - - public void Start() - { - Task.Run(() => - { - while (true) - { - if (!IsEnabled) - { - Thread.Sleep(10000); - continue; - } - - var now = DateTime.Now; - var uptodate = new List(); - lock (_lock) - { - foreach (var job in _jobs) - { - if (job.Value.NextRunTimepoint.Subtract(now).TotalSeconds <= 0) - uptodate.Add(job.Value); - } - } - - foreach (var job in uptodate) - { - if (!File.Exists(job.IndexLockFile)) - { - job.Cmd.Exec(); - job.NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)); - } - } - - Thread.Sleep(2000); - } - - // ReSharper disable once FunctionNeverReturns - }); - } - - public void AddRepository(string repo, string gitDir) - { - var job = new Job - { - IndexLockFile = Path.Combine(gitDir, "index.lock"), - Cmd = new Commands.Fetch(repo, "--all", true, false, null) { RaiseError = false }, - NextRunTimepoint = DateTime.Now.AddMinutes(Convert.ToDouble(Interval)), - }; - - lock (_lock) - { - _jobs[repo] = job; - } - } - - public void RemoveRepository(string repo) - { - lock (_lock) - { - _jobs.Remove(repo); - } - } - - public void MarkFetched(string repo) - { - lock (_lock) - { - if (_jobs.TryGetValue(repo, out var value)) - value.NextRunTimepoint = DateTime.Now.AddMinutes(Interval * 1.0); - } - } - } -} diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index fde8011c..3872fcc6 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -8,85 +8,27 @@ namespace SourceGit.Models { public class CommitGraph { - public class Path + public static List Pens { get; } = []; + + public static void SetDefaultPens(double thickness = 2) { - public List Points = new List(); - public int Color = 0; + SetPens(s_defaultPenColors, thickness); } - public class PathHelper + public static void SetPens(List colors, double thickness) { - public string Next; - public bool IsMerged; - public double LastX; - public double LastY; - public double EndY; - public Path Path; + Pens.Clear(); - public PathHelper(string next, bool isMerged, int color, Point start) - { - Next = next; - IsMerged = isMerged; - LastX = start.X; - LastY = start.Y; - EndY = LastY; + foreach (var c in colors) + Pens.Add(new Pen(c.ToUInt32(), thickness)); - Path = new Path(); - Path.Color = color; - Path.Points.Add(start); - } + s_penCount = colors.Count; + } - public PathHelper(string next, bool isMerged, int color, Point start, Point to) - { - Next = next; - IsMerged = isMerged; - LastX = to.X; - LastY = to.Y; - EndY = LastY; - - Path = new Path(); - Path.Color = color; - Path.Points.Add(start); - Path.Points.Add(to); - } - - public void Add(double x, double y, double halfHeight, bool isEnd = false) - { - if (x > LastX) - { - Add(new Point(LastX, LastY)); - Add(new Point(x, y - halfHeight)); - if (isEnd) - Add(new Point(x, y)); - } - else if (x < LastX) - { - var testY = LastY + halfHeight; - if (y > testY) - Add(new Point(LastX, testY)); - - if (!isEnd) - y += halfHeight; - - Add(new Point(x, y)); - } - else if (isEnd) - { - Add(new Point(x, y)); - } - - LastX = x; - LastY = y; - } - - private void Add(Point p) - { - if (EndY < p.Y) - { - Path.Points.Add(p); - EndY = p.Y; - } - } + public class Path(int color) + { + public List Points { get; } = []; + public int Color { get; } = color; } public class Link @@ -111,82 +53,57 @@ namespace SourceGit.Models public int Color; } - public List Paths { get; set; } = new List(); - public List Links { get; set; } = new List(); - public List Dots { get; set; } = new List(); - - public static List Pens - { - get; - private set; - } = new List(); - - public static void SetDefaultPens(double thickness = 2) - { - SetPens(_defaultPenColors, thickness); - } - - public static void SetPens(List colors, double thickness) - { - Pens.Clear(); - - foreach (var c in colors) - Pens.Add(new Pen(c.ToUInt32(), thickness)); - - _penCount = colors.Count; - } + public List Paths { get; } = []; + public List Links { get; } = []; + public List Dots { get; } = []; public static CommitGraph Parse(List commits, bool firstParentOnlyEnabled) { - double UNIT_WIDTH = 12; - double HALF_WIDTH = 6; - double UNIT_HEIGHT = 28; - double HALF_HEIGHT = 14; - double H_MARGIN = 2; + const double unitWidth = 12; + const double halfWidth = 6; + const double unitHeight = 28; + const double halfHeight = 14; var temp = new CommitGraph(); var unsolved = new List(); - var mapUnsolved = new Dictionary(); var ended = new List(); - var offsetY = -HALF_HEIGHT; + var offsetY = -halfHeight; var colorIdx = 0; foreach (var commit in commits) { var major = null as PathHelper; var isMerged = commit.IsMerged; - var oldCount = unsolved.Count; // Update current y offset - offsetY += UNIT_HEIGHT; + offsetY += unitHeight; // Find first curves that links to this commit and marks others that links to this commit ended. - double offsetX = H_MARGIN - HALF_WIDTH; + var offsetX = 4 - halfWidth; + var maxOffsetOld = unsolved.Count > 0 ? unsolved[^1].LastX : offsetX + unitWidth; foreach (var l in unsolved) { - if (l.Next == commit.SHA) + if (l.Next.Equals(commit.SHA, StringComparison.Ordinal)) { if (major == null) { - offsetX += UNIT_WIDTH; + offsetX += unitWidth; major = l; if (commit.Parents.Count > 0) { major.Next = commit.Parents[0]; - if (!mapUnsolved.ContainsKey(major.Next)) - mapUnsolved.Add(major.Next, major); + major.Goto(offsetX, offsetY, halfHeight); } else { - major.Next = "ENDED"; + major.End(offsetX, offsetY, halfHeight); ended.Add(l); } - - major.Add(offsetX, offsetY, HALF_HEIGHT); } else { + l.End(major.LastX, offsetY, halfHeight); ended.Add(l); } @@ -195,17 +112,20 @@ namespace SourceGit.Models } else { - if (!mapUnsolved.ContainsKey(l.Next)) - mapUnsolved.Add(l.Next, l); - offsetX += UNIT_WIDTH; - l.Add(offsetX, offsetY, HALF_HEIGHT); + offsetX += unitWidth; + l.Pass(offsetX, offsetY, halfHeight); } } + // Remove ended curves from unsolved + foreach (var l in ended) + unsolved.Remove(l); + ended.Clear(); + // Create new curve for branch head if (major == null) { - offsetX += UNIT_WIDTH; + offsetX += unitWidth; if (commit.Parents.Count > 0) { @@ -214,18 +134,13 @@ namespace SourceGit.Models temp.Paths.Add(major.Path); } - colorIdx = (colorIdx + 1) % _penCount; + colorIdx = (colorIdx + 1) % s_penCount; } // Calculate link position of this commit. - Point position = new Point(offsetX, offsetY); - int dotColor = 0; - if (major != null) - { - position = new Point(major.LastX, offsetY); - dotColor = major.Path.Color; - } - Dot anchor = new Dot() { Center = position, Color = dotColor }; + var position = new Point(major?.LastX ?? offsetX, offsetY); + var dotColor = major?.Path.Color ?? 0; + var anchor = new Dot() { Center = position, Color = dotColor }; if (commit.IsCurrentHead) anchor.Type = DotType.Head; else if (commit.Parents.Count > 1) @@ -239,69 +154,176 @@ namespace SourceGit.Models { for (int j = 1; j < commit.Parents.Count; j++) { - var parent = commit.Parents[j]; - if (mapUnsolved.TryGetValue(parent, out var value)) + var parentHash = commit.Parents[j]; + var parent = unsolved.Find(x => x.Next.Equals(parentHash, StringComparison.Ordinal)); + if (parent != null) { // Try to change the merge state of linked graph - var l = value; if (isMerged) - l.IsMerged = true; + parent.IsMerged = true; - var link = new Link(); - link.Start = position; - link.End = new Point(l.LastX, offsetY + HALF_HEIGHT); - link.Control = new Point(link.End.X, link.Start.Y); - link.Color = l.Path.Color; - - temp.Links.Add(link); + temp.Links.Add(new Link + { + Start = position, + End = new Point(parent.LastX, offsetY + halfHeight), + Control = new Point(parent.LastX, position.Y), + Color = parent.Path.Color + }); } else { - offsetX += UNIT_WIDTH; + offsetX += unitWidth; // Create new curve for parent commit that not includes before - var l = new PathHelper(parent, isMerged, colorIdx, position, new Point(offsetX, position.Y + HALF_HEIGHT)); + var l = new PathHelper(parentHash, isMerged, colorIdx, position, new Point(offsetX, position.Y + halfHeight)); unsolved.Add(l); temp.Paths.Add(l.Path); - colorIdx = (colorIdx + 1) % _penCount; + colorIdx = (colorIdx + 1) % s_penCount; } } } - // Remove ended curves from unsolved - foreach (var l in ended) - { - l.Add(position.X, position.Y, HALF_HEIGHT, true); - unsolved.Remove(l); - } - // Margins & merge state (used by Views.Histories). commit.IsMerged = isMerged; - commit.Margin = new Thickness(Math.Max(offsetX + HALF_WIDTH, oldCount * UNIT_WIDTH + H_MARGIN) + H_MARGIN, 0, 0, 0); - - // Clean up - ended.Clear(); - mapUnsolved.Clear(); + commit.Margin = new Thickness(Math.Max(offsetX, maxOffsetOld) + halfWidth + 2, 0, 0, 0); } // Deal with curves haven't ended yet. - for (int i = 0; i < unsolved.Count; i++) + for (var i = 0; i < unsolved.Count; i++) { var path = unsolved[i]; - var endY = (commits.Count - 0.5) * UNIT_HEIGHT; + var endY = (commits.Count - 0.5) * unitHeight; if (path.Path.Points.Count == 1 && Math.Abs(path.Path.Points[0].Y - endY) < 0.0001) continue; - path.Add((i + 0.5) * UNIT_WIDTH + H_MARGIN, endY + HALF_HEIGHT, HALF_HEIGHT, true); + path.End((i + 0.5) * unitWidth + 4, endY + halfHeight, halfHeight); } unsolved.Clear(); return temp; } - private static int _penCount = 0; - private static readonly List _defaultPenColors = [ + private class PathHelper + { + public Path Path { get; } + public string Next { get; set; } + public bool IsMerged { get; set; } + public double LastX { get; private set; } + + public PathHelper(string next, bool isMerged, int color, Point start) + { + Next = next; + IsMerged = isMerged; + LastX = start.X; + _lastY = start.Y; + + Path = new Path(color); + Path.Points.Add(start); + } + + public PathHelper(string next, bool isMerged, int color, Point start, Point to) + { + Next = next; + IsMerged = isMerged; + LastX = to.X; + _lastY = to.Y; + + Path = new Path(color); + Path.Points.Add(start); + Path.Points.Add(to); + } + + /// + /// A path that just passed this row. + /// + /// + /// + /// + public void Pass(double x, double y, double halfHeight) + { + if (x > LastX) + { + Add(LastX, _lastY); + Add(x, y - halfHeight); + } + else if (x < LastX) + { + Add(LastX, y - halfHeight); + y += halfHeight; + Add(x, y); + } + + LastX = x; + _lastY = y; + } + + /// + /// A path that has commit in this row but not ended + /// + /// + /// + /// + public void Goto(double x, double y, double halfHeight) + { + if (x > LastX) + { + Add(LastX, _lastY); + Add(x, y - halfHeight); + } + else if (x < LastX) + { + var minY = y - halfHeight; + if (minY > _lastY) + minY -= halfHeight; + + Add(LastX, minY); + Add(x, y); + } + + LastX = x; + _lastY = y; + } + + /// + /// A path that has commit in this row and end. + /// + /// + /// + /// + public void End(double x, double y, double halfHeight) + { + if (x > LastX) + { + Add(LastX, _lastY); + Add(x, y - halfHeight); + } + else if (x < LastX) + { + Add(LastX, y - halfHeight); + } + + Add(x, y); + + LastX = x; + _lastY = y; + } + + private void Add(double x, double y) + { + if (_endY < y) + { + Path.Points.Add(new Point(x, y)); + _endY = y; + } + } + + private double _lastY = 0; + private double _endY = 0; + } + + private static int s_penCount = 0; + private static readonly List s_defaultPenColors = [ Colors.Orange, Colors.ForestGreen, Colors.Gold, diff --git a/src/Models/IRepository.cs b/src/Models/IRepository.cs new file mode 100644 index 00000000..12b1adba --- /dev/null +++ b/src/Models/IRepository.cs @@ -0,0 +1,16 @@ +namespace SourceGit.Models +{ + public interface IRepository + { + string FullPath { get; set; } + string GitDir { get; set; } + + void RefreshBranches(); + void RefreshWorktrees(); + void RefreshTags(); + void RefreshCommits(); + void RefreshSubmodules(); + void RefreshWorkingCopyChanges(); + void RefreshStashes(); + } +} diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 06e60a87..244fc673 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -94,6 +94,18 @@ namespace SourceGit.Models set; } = new AvaloniaList(); + public bool EnableAutoFetch + { + get; + set; + } = false; + + public int AutoFetchInterval + { + get; + set; + } = 10; + public void PushCommitMessage(string message) { var existIdx = CommitMessages.IndexOf(message); diff --git a/src/Models/ResetMode.cs b/src/Models/ResetMode.cs index f2bfb5ef..735533c2 100644 --- a/src/Models/ResetMode.cs +++ b/src/Models/ResetMode.cs @@ -8,6 +8,8 @@ namespace SourceGit.Models [ new ResetMode("Soft", "Keep all changes. Stage differences", "--soft", Brushes.Green), new ResetMode("Mixed", "Keep all changes. Unstage differences", "--mixed", Brushes.Orange), + new ResetMode("Merge", "Reset while keeping unmerged changes", "--merge", Brushes.Purple), + new ResetMode("Keep", "Reset while keeping local modifications", "--keep", Brushes.Purple), new ResetMode("Hard", "Discard all changes", "--hard", Brushes.Red), ]; diff --git a/src/Models/Statistics.cs b/src/Models/Statistics.cs index 21f1e65d..b669eb55 100644 --- a/src/Models/Statistics.cs +++ b/src/Models/Statistics.cs @@ -1,122 +1,172 @@ using System; using System.Collections.Generic; +using LiveChartsCore; +using LiveChartsCore.Defaults; +using LiveChartsCore.SkiaSharpView; +using LiveChartsCore.SkiaSharpView.Painting; + +using SkiaSharp; + namespace SourceGit.Models { - public class StatisticsSample(string name) + public enum StaticsticsMode + { + All, + ThisMonth, + ThisWeek, + } + + public class StaticsticsAuthor(string name, int count) { public string Name { get; set; } = name; - public int Count { get; set; } = 0; + public int Count { get; set; } = count; + } + + public class StaticsticsSample(DateTime time, int count) + { + public DateTime Time { get; set; } = time; + public int Count { get; set; } = count; } public class StatisticsReport { + public static readonly string[] WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; + public int Total { get; set; } = 0; - public List Samples { get; set; } = new List(); - public List ByAuthor { get; set; } = new List(); + public List Authors { get; set; } = new List(); + public List Series { get; set; } = new List(); + public List XAxes { get; set; } = new List(); + public List YAxes { get; set; } = new List(); - public void AddCommit(int index, string author) + public StatisticsReport(StaticsticsMode mode, DateTime start) { - Total++; - Samples[index].Count++; + _mode = mode; - if (_mapUsers.TryGetValue(author, out var value)) + YAxes = [new Axis() { - value.Count++; + TextSize = 10, + MinLimit = 0, + SeparatorsPaint = new SolidColorPaint(new SKColor(0x40808080)) { StrokeThickness = 1 } + }]; + + if (mode == StaticsticsMode.ThisWeek) + { + for (int i = 0; i < 7; i++) + _mapSamples.Add(start.AddDays(i), 0); + + XAxes.Add(new DateTimeAxis(TimeSpan.FromDays(1), v => WEEKDAYS[(int)v.DayOfWeek]) { TextSize = 10 }); + } + else if (mode == StaticsticsMode.ThisMonth) + { + var now = DateTime.Now; + var maxDays = DateTime.DaysInMonth(now.Year, now.Month); + for (int i = 0; i < maxDays; i++) + _mapSamples.Add(start.AddDays(i), 0); + + XAxes.Add(new DateTimeAxis(TimeSpan.FromDays(1), v => $"{v:MM/dd}") { TextSize = 10 }); } else { - var sample = new StatisticsSample(author); - sample.Count++; - - _mapUsers.Add(author, sample); - ByAuthor.Add(sample); + XAxes.Add(new DateTimeAxis(TimeSpan.FromDays(30), v => $"{v:yyyy/MM}") { TextSize = 10 }); } } + public void AddCommit(DateTime time, string author) + { + Total++; + + var normalized = DateTime.MinValue; + if (_mode == StaticsticsMode.ThisWeek || _mode == StaticsticsMode.ThisMonth) + normalized = time.Date; + else + normalized = new DateTime(time.Year, time.Month, 1).ToLocalTime(); + + if (_mapSamples.TryGetValue(normalized, out var vs)) + _mapSamples[normalized] = vs + 1; + else + _mapSamples.Add(normalized, 1); + + if (_mapUsers.TryGetValue(author, out var vu)) + _mapUsers[author] = vu + 1; + else + _mapUsers.Add(author, 1); + } + public void Complete() { - ByAuthor.Sort((l, r) => r.Count - l.Count); + var samples = new List(); + foreach (var kv in _mapSamples) + samples.Add(new DateTimePoint(kv.Key, kv.Value)); + + Series.Add( + new ColumnSeries() + { + Values = samples, + Stroke = null, + Fill = null, + Padding = 1, + } + ); + + foreach (var kv in _mapUsers) + Authors.Add(new StaticsticsAuthor(kv.Key, kv.Value)); + + Authors.Sort((l, r) => r.Count - l.Count); + _mapUsers.Clear(); + _mapSamples.Clear(); } - private readonly Dictionary _mapUsers = new Dictionary(); + public void ChangeColor(uint color) + { + if (Series is [ColumnSeries series]) + series.Fill = new SolidColorPaint(new SKColor(color)); + } + + private StaticsticsMode _mode = StaticsticsMode.All; + private Dictionary _mapUsers = new Dictionary(); + private Dictionary _mapSamples = new Dictionary(); } public class Statistics { - public StatisticsReport Year { get; set; } = new StatisticsReport(); - public StatisticsReport Month { get; set; } = new StatisticsReport(); - public StatisticsReport Week { get; set; } = new StatisticsReport(); + public StatisticsReport All { get; set; } + public StatisticsReport Month { get; set; } + public StatisticsReport Week { get; set; } public Statistics() { - _today = DateTime.Today; - _thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24 - _today.Hour * 3600 - _today.Minute * 60 - _today.Second); - _thisWeekEnd = _thisWeekStart.AddDays(7); + _today = DateTime.Now.ToLocalTime().Date; + _thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24); + _thisMonthStart = _today.AddDays(1 - _today.Day); - for (int i = 0; i < 12; i++) - Year.Samples.Add(new StatisticsSample("")); - - var monthDays = DateTime.DaysInMonth(_today.Year, _today.Month); - for (int i = 0; i < monthDays; i++) - Month.Samples.Add(new StatisticsSample($"{i + 1}")); - - string[] weekDayNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; - for (int i = 0; i < weekDayNames.Length; i++) - Week.Samples.Add(new StatisticsSample(weekDayNames[i])); - } - - public string Since() - { - return _today.AddMonths(-11).ToString("yyyy-MM-01 00:00:00"); + All = new StatisticsReport(StaticsticsMode.All, DateTime.MinValue); + Month = new StatisticsReport(StaticsticsMode.ThisMonth, _thisMonthStart); + Week = new StatisticsReport(StaticsticsMode.ThisWeek, _thisWeekStart); } public void AddCommit(string author, double timestamp) { var time = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); - if (time.CompareTo(_thisWeekStart) >= 0 && time.CompareTo(_thisWeekEnd) < 0) - Week.AddCommit((int)time.DayOfWeek, author); + if (time >= _thisWeekStart) + Week.AddCommit(time, author); - if (time.Month == _today.Month) - Month.AddCommit(time.Day - 1, author); + if (time >= _thisMonthStart) + Month.AddCommit(time, author); - Year.AddCommit(time.Month - 1, author); + All.AddCommit(time, author); } public void Complete() { - Year.Complete(); + All.Complete(); Month.Complete(); Week.Complete(); - - // Year is start from 11 months ago from now. - var thisYear = _today.Year; - var start = _today.AddMonths(-11); - if (start.Month == 1) - { - for (int i = 0; i < 12; i++) - Year.Samples[i].Name = $"{thisYear}/{i + 1:00}"; - } - else - { - var lastYearIdx = start.Month - 1; - var lastYearMonths = Year.Samples.GetRange(lastYearIdx, 12 - lastYearIdx); - for (int i = 0; i < lastYearMonths.Count; i++) - lastYearMonths[i].Name = $"{thisYear - 1}/{lastYearIdx + i + 1:00}"; - - var thisYearMonths = Year.Samples.GetRange(0, lastYearIdx); - for (int i = 0; i < thisYearMonths.Count; i++) - thisYearMonths[i].Name = $"{thisYear}/{i + 1:00}"; - - Year.Samples.Clear(); - Year.Samples.AddRange(lastYearMonths); - Year.Samples.AddRange(thisYearMonths); - } } private readonly DateTime _today; + private readonly DateTime _thisMonthStart; private readonly DateTime _thisWeekStart; - private readonly DateTime _thisWeekEnd; } } diff --git a/src/Models/TextMateHelper.cs b/src/Models/TextMateHelper.cs index f8e5acf2..2e38e4dd 100644 --- a/src/Models/TextMateHelper.cs +++ b/src/Models/TextMateHelper.cs @@ -38,7 +38,7 @@ namespace SourceGit.Models extension = ".sh"; else if (extension == ".kt" || extension == ".kts") extension = ".kotlin"; - + foreach (var grammar in s_extraGrammars) { if (grammar.Extension.Equals(extension, StringComparison.OrdinalIgnoreCase)) @@ -87,7 +87,7 @@ namespace SourceGit.Models public ICollection GetInjections(string scopeName) => _backend.GetInjections(scopeName); public IRawGrammar GetGrammar(string scopeName) => GrammarUtility.GetGrammar(scopeName, _backend); public string GetScope(string filename) => GrammarUtility.GetScope(filename, _backend); - + private readonly RegistryOptions _backend = new(defaultTheme); } @@ -95,8 +95,8 @@ namespace SourceGit.Models { public static TextMate.Installation CreateForEditor(TextEditor editor) { - return editor.InstallTextMate(Application.Current?.ActualThemeVariant == ThemeVariant.Dark ? - new RegistryOptionsWrapper(ThemeName.DarkPlus) : + return editor.InstallTextMate(Application.Current?.ActualThemeVariant == ThemeVariant.Dark ? + new RegistryOptionsWrapper(ThemeName.DarkPlus) : new RegistryOptionsWrapper(ThemeName.LightPlus)); } diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 6cd77a15..6665250c 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -6,20 +6,6 @@ using System.Threading.Tasks; namespace SourceGit.Models { - public interface IRepository - { - string FullPath { get; set; } - string GitDir { get; set; } - - void RefreshBranches(); - void RefreshWorktrees(); - void RefreshTags(); - void RefreshCommits(); - void RefreshSubmodules(); - void RefreshWorkingCopyChanges(); - void RefreshStashes(); - } - public class Watcher : IDisposable { public Watcher(IRepository repo) diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index b02112cc..6ca0bbb0 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -201,9 +201,9 @@ namespace SourceGit.Native private void FixWindowFrameOnWin10(Window w) { - if (w.WindowState != WindowState.Normal) + if (w.WindowState == WindowState.Maximized || w.WindowState == WindowState.FullScreen) w.SystemDecorations = SystemDecorations.Full; - else + else if (w.WindowState == WindowState.Normal) w.SystemDecorations = SystemDecorations.BorderOnly; } diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index ef3e13aa..8650e068 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -14,6 +14,7 @@ M797 829a49 49 0 1049 49 49 49 0 00-49-49zm147-114A49 49 0 10992 764a49 49 0 00-49-49zM928 861a49 49 0 1049 49A49 49 0 00928 861zm-5-586L992 205 851 64l-71 71a67 67 0 00-94 0l235 235a67 67 0 000-94zm-853 128a32 32 0 00-32 50 1291 1291 0 0075 112L288 552c20 0 25 21 8 37l-93 86a1282 1282 0 00120 114l100-32c19-6 28 15 14 34l-40 55c26 19 53 36 82 53a89 89 0 00115-20 1391 1391 0 00256-485l-188-188s-306 224-595 198z M1280 704c0 141-115 256-256 256H288C129 960 0 831 0 672c0-126 80-232 192-272A327 327 0 01192 384c0-177 143-320 320-320 119 0 222 64 277 160C820 204 857 192 896 192c106 0 192 86 192 192 0 24-5 48-13 69C1192 477 1280 580 1280 704zm-493-128H656V352c0-18-14-32-32-32h-96c-18 0-32 14-32 32v224h-131c-29 0-43 34-23 55l211 211c12 12 33 12 45 0l211-211c20-20 6-55-23-55z M853 102H171C133 102 102 133 102 171v683C102 891 133 922 171 922h683C891 922 922 891 922 853V171C922 133 891 102 853 102zM390 600l-48 48L205 512l137-137 48 48L301 512l88 88zM465 819l-66-18L559 205l66 18L465 819zm218-171L634 600 723 512l-88-88 48-48L819 512 683 649z + M128 854h768v86H128zM390 797c13 13 29 19 48 19s35-6 45-19l291-288c26-22 26-64 0-90L435 83l-61 61L426 192l-272 269c-22 22-22 64 0 90l237 246zm93-544 211 211-32 32H240l243-243zM707 694c0 48 38 86 86 86 48 0 86-38 86-86 0-22-10-45-26-61L794 576l-61 61c-13 13-26 35-26 58z M796 471A292 292 0 00512 256a293 293 0 00-284 215H0v144h228A293 293 0 00512 832a291 291 0 00284-217H1024V471h-228M512 688A146 146 0 01366 544A145 145 0 01512 400c80 0 146 63 146 144A146 146 0 01512 688 M645 448l64 64 220-221L704 64l-64 64 115 115H128v90h628zM375 576l-64-64-220 224L314 960l64-64-116-115H896v-90H262z M608 0q48 0 88 23t63 63 23 87v70h55q35 0 67 14t57 38 38 57 14 67V831q0 34-14 66t-38 57-57 38-67 13H426q-34 0-66-13t-57-38-38-57-14-66v-70h-56q-34 0-66-14t-57-38-38-57-13-67V174q0-47 23-87T109 23 196 0h412m175 244H426q-46 0-86 22T278 328t-26 85v348H608q47 0 86-22t63-62 25-85l1-348m-269 318q18 0 31 13t13 31-13 31-31 13-31-13-13-31 13-31 31-13m0-212q13 0 22 9t11 22v125q0 14-9 23t-22 10-23-7-11-22l-1-126q0-13 10-23t23-10z diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 8ec97547..eac15bca 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -99,6 +99,7 @@ SCHLIESSEN Editor Diesen Commit cherry-picken + Mehrere cherry-picken Commit auschecken Mit HEAD vergleichen Mit Worktree vergleichen @@ -128,6 +129,7 @@ VORGÄNGER REFS SHA + Im Browser öffnen Commit-Nachricht Details Repository Einstellungen @@ -137,6 +139,8 @@ Email Adresse Email Adresse GIT + Remotes automatisch fetchen + Minute(n) TICKETSYSTEM Beispiel für Github-Regel hinzufügen Beispiel für Jira-Regel hinzufügen @@ -290,6 +294,7 @@ Sperren anzeigen Keine gesperrten Dateien Sperre + Zeige nur meine Sperren LFS Sperren Entsperren Erzwinge entsperren @@ -410,9 +415,6 @@ Commit-Historie Längenvorgabe für Commit-Nachrichten GIT - Remotes automatisch fetchen - Auto-Fetch Intervall - Minute(n) Aktiviere Auto-CRLF Clone Standardordner Benutzer Email @@ -430,6 +432,10 @@ Installationspfad zum GPG Programm Benutzer Signierungsschlüssel GPG Benutzer Signierungsschlüssel + EINBINDUNGEN + SHELL/TERMINAL + Shell/Terminal + Pfad Remote löschen Ziel: Worktrees löschen @@ -486,6 +492,7 @@ Eindeutiger Name für diesen Branch Branch: ABBRECHEN + Änderungen automatisch von Remote fetchen... Aufräumen (GC & Prune) Führt `git gc` auf diesem Repository aus. Alles löschen @@ -505,7 +512,6 @@ REMOTE HINZUFÜGEN KONFLIKTE BEHEBEN Commit suchen - Suche über Dateiname Commit-Nachricht SHA @@ -546,6 +552,7 @@ Software Update Es sind momentan kein Updates verfügbar. Squash Commits + In: SSH privater Schlüssel: Pfad zum privaten SSH Schlüssel START @@ -567,9 +574,9 @@ COMMITTER MONAT WOCHE - JAHR COMMITS: AUTOREN: + ÜBERSICHT SUBMODULE Submodul hinzufügen Relativen Pfad kopieren @@ -616,7 +623,6 @@ COMMIT COMMIT & PUSH Template/Historie - STRG + Enter KONFLIKTE ERKANNT DATEI KONFLIKTE GELÖST NICHT-VERFOLGTE DATEIEN INKLUDIEREN diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index e9717845..ca320f4d 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -136,6 +136,8 @@ Email Address Email address GIT + Fetch remotes automatically + Minute(s) ISSUE TRACKER Add Sample Github Rule Add Sample Jira Rule @@ -289,6 +291,7 @@ Show Locks No Locked Files Lock + Show only my locks LFS Locks Unlock Force Unlock @@ -324,6 +327,7 @@ REPOSITORY Commit staged changes Commit and push staged changes + Stage all changes and commit Discard selected changes Dashboard mode (Default) Force to reload this repository @@ -409,9 +413,6 @@ History Commits Subject Guide Length GIT - Fetch remotes automatically - Auto Fetch Interval - Minute(s) Enable Auto CRLF Default Clone Dir User Email @@ -489,6 +490,7 @@ Unique name for this branch Branch: ABORT + Auto fetching changes from remotes... Cleanup(GC & Prune) Run `git gc` command for this repository. Clear all @@ -508,11 +510,11 @@ ADD REMOTE RESOLVE Search Commit - Search By File Message SHA Author & Committer + Current Branch Show Tags as Tree Statistics SUBMODULES @@ -571,9 +573,9 @@ COMMITTER MONTH WEEK - YEAR COMMITS: AUTHORS: + OVERVIEW SUBMODULES Add Submodule Copy Relative Path @@ -620,7 +622,8 @@ COMMIT COMMIT & PUSH Template/Histories - CTRL + Enter + Trigger click event + Stage all changes and commit CONFLICTS DETECTED FILE CONFLICTS ARE RESOLVED INCLUDE UNTRACKED FILES diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 40df6d99..c90a19cb 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -2,178 +2,181 @@ - A propos - A propos de SourceGit + + À propos • Compilé avec © 2024 sourcegit-scm • TextEditor de - - • La police Monospace vient de + • Les polices Monospace proviennent de + À propos de SourceGit • Le code source est disponible sur - Client Git gratuit et open source + Client Git Open Source et Gratuit Ajouter un Worktree - What to Checkout: - Branche existante - Créer une nouvelle branche - Location: + Emplacement : Chemin vers ce worktree. Relatif supporté. Nom de branche: - Optionnel. Par défaut le nom du dossier de destination. - Track Branch: - Tracking remote branch - Patch + Optionnel. Nom du dossier de destination par défaut. + Suivre la branche : + Suivi de la branche distante + What to Checkout: + Créer une nouvelle branche + Branche existante + Appliquer Erreur Soulever les erreurs et refuser d'appliquer le patch - Toutes erreurs - Similaire à 'error', mais plus détaillé + Toutes les erreurs + Similaire à 'Erreur', mais plus détaillé Fichier de patch : Selectionner le fichier .patch à appliquer - Ignorer les espaces blancs + Ignorer les changements d'espaces blancs Pas d'avertissement - Désactive l'avertissement des espaces blancs terminaux + Désactiver l'avertissement sur les espaces blancs terminaux Appliquer le patch - Warn - Affiche les avertissements Outputs warnings for a few such errors, mais applique - Espace blanc: - Archive... - Sauvegarder l'archive vers : - Selectionner le chemin de l'archive - Révision: - Archive + Avertissement + Affiche des avertissements pour ce type d'erreurs tout en appliquant le patch + Espaces blancs : + Archiver... + Enregistrer l'archive sous : + Sélectionnez le chemin du fichier d'archive + Révision : + Archiver SourceGit Askpass FICHIERS PRÉSUMÉS INCHANGÉS PAS DE FICHIERS PRÉSUMÉS INCHANGÉS - REMOVE + SUPPRIMER FICHIER BINAIRE NON SUPPORTÉ !!! - Blame - BLAME SUR CE FICHIER NON SUPPORTÉ !!! + Blâme + LE BLÂME SUR CE FICHIER N'EST PAS SUPPORTÉ!!! Checkout ${0}$... Comparer avec la branche Comparer avec HEAD - Comparer avec le Worktree + Comparer avec le worktree Copier le nom de la branche Supprimer ${0}$... Supprimer {0} branches sélectionnées Rejeter tous les changements - Fast-Forward to ${0}$ - Git Flow - Finir ${0}$ - Merger ${0}$ dans ${1}$... - Pull ${0}$ - Pull ${0}$ dans ${1}$... - Push ${0}$ - Rebase ${0}$ sur ${1}$... + Fast-Forward vers ${0}$ + Git Flow - Terminer ${0}$ + Fusionner ${0}$ dans ${1}$... + Tirer ${0}$ + Tirer ${0}$ dans ${1}$... + Pousser ${0}$ + Rebaser ${0}$ sur ${1}$... Renommer ${0}$... Définir la branche de suivi - Unset Upstream + Ne plus suivre la branche distante Comparer les branches Octets ANNULER - Reset vers cette révision - Reset vers la révision parente + Réinitialiser à la révision parente + Réinitialiser à cette révision CHANGER LE MODE D'AFFICHAGE Afficher comme liste de dossiers/fichiers Afficher comme liste de chemins Afficher comme arborescence Checkout Branch - Checkout Commit - Avertissement: un checkout vers un commit aboutiera vers un HEAD détaché + Checkout ce commit Commit : - Branche : - Changement locaux: - Rejeter + Avertissement: un checkout vers un commit aboutiera vers un HEAD détaché + Changements locaux : + Annuler Ne rien faire - Stash et Réappliquer - Cherry-Pick - Commit(s) : + Mettre en stash et réappliquer + Branche : + Cherry-Pick de ce commit + Commit : Commit tous les changements - Vider les Stashes - Voulez-vous vider tous les stashes. Êtes-vous sûr de vouloir continuer ? - Cloner le dépôt distant + Cherry Pick + Supprimer les stashes + Vous essayez de supprimer tous les stashes. Êtes-vous sûr de vouloir continuer ? Paramètres supplémentaires : Arguments additionnels au clônage. Optionnel. Nom local : Nom de dépôt. Optionnel. Dossier parent : URL du dépôt : + Cloner le dépôt distant FERMER Éditeur + Changer de commit Cherry-Pick ce commit - Checkout ce commit Comparer avec HEAD - Comparer avec le Worktree - Copier les infos + Comparer avec le worktree + Copier les informations Copier le SHA Rebase interactif de ${0}$ ici - Rebase ${0}$ ici - Reset ${0}$ ici + Rebaser ${0}$ ici + Réinitialiser ${0}$ ici Annuler le commit Reformuler - Sauver en tant que patch... + Enregistrer en tant que patch... Squash dans le parent CHANGEMENTS - Chercher des changements... + Rechercher les changements... FICHIERS - Fichiers LFS + Fichier LFS Sous-module - INFORMATION + INFORMATIONS AUTEUR CHANGÉ COMMITTER + Vérifier les références contenant ce commit + LE COMMIT EST CONTENU PAR Afficher seulement les 100 premiers changements. Voir tous les changements dans l'onglet CHANGEMENTS. MESSAGE PARENTS REFS SHA - Enter le sujet du commit Description - Repository Configurer + Entrez le message du commit + Configurer le dépôt MODÈLE DE COMMIT - Nom de modèle: Contenu de modèle: - Addresse e-mail - Addresse e-mail + Nom de modèle: + Adresse e-mail + Adresse e-mail GIT - ISSUE TRACKER - Add Sample Github Rule - Add Sample Jira Rule + Fetch les dépôts distants automatiquement + minute(s) + SUIVI DES PROBLÈMES + Ajouter une règle d'exemple Github + Ajouter une règle d'exemple Jira Nouvelle règle Issue Regex Expression: Nom de règle : + Veuillez utiliser $1, $2 pour accéder aux valeurs des groupes regex. URL résultant: - - Please use $1, $2 to access regex groups values. - HTTP Proxy - HTTP proxy used by this repository + Proxy HTTP + Proxy HTTP utilisé par ce dépôt Nom d'utilisateur Nom d'utilisateur pour ce dépôt Copier + Copier tout le texte + Copier le nom de fichier COPIER LE MESSAGE Copier le chemin - Copier le nom de fichier Créer une branche... Basé sur : - - Check out the created branch + Passer à la branche créée Changements locaux : Rejeter Ne rien faire Stash & Réappliquer - Nouveau nom de branche: - Enter le nom de branche. + Nom de la nouvelle branche : + Entrez le nom de la branche. Créer une branche locale Créer un tag... Nouveau tag à : Signature GPG - Message de tag : + Message du tag : Optionnel. - Nom de tag : - Format Recommendé : v1.0.0-alpha - Push vers tous les arpès création + Nom du tag : + Format recommandé : v1.0.0-alpha + Pousser sur tous les dépôts distants après création Créer un nouveau tag - Kind: + Type : annoté - - lightweight + léger Maintenir Ctrl pour commencer directement Couper Supprimer la branche @@ -187,39 +190,39 @@ Cible : Confirmer la suppression du groupe Confirmer la suppression du dépôt - Supprimer le Sous-module + Supprimer le sous-module Chemin du sous-module : Supprimer le tag - Tag: - Delete from remote repositories + Tag : + Supprimer des dépôts distants DIFF BINAIRE NOUVEAU ANCIEN Copier Mode de fichier changé - LFS OBJECT CHANGE + CHANGEMENT D'OBJET LFS Différence suivante PAS DE CHANGEMENT OU SEULEMENT EN FIN DE LIGNE Différence précédente + Afficher les caractères invisibles Diff côte-à-côte SOUS-MODULE NOUVEAU + Permuter Coloration syntaxique Retour à la ligne - Ouvrir dans l'outil de merge + Ouvrir dans l'outil de fusion Réduit le nombre de ligne visibles Augmente le nombre de ligne visibles - SÉLECTIONNER UN FICHIER POUR VOIR LES CHANGEMENTS - Afficher les caractères invisibles - Permuter - Ouvrir dans l'outil de merge + SÉLECTIONNEZ UN FICHIER POUR VOIR LES CHANGEMENTS + Ouvrir dans l'outil de fusion Rejeter les changements Tous les changements dans la copie de travail. Changements : {0} changements seront rejetés Vous ne pouvez pas annuler cette action !!! - Bookmark: - Nouveau nom: + Signet : + Nouveau nom : Cible : Éditer le groupe sélectionné Éditer le dépôt sélectionné @@ -227,28 +230,28 @@ Fetch Fetch toutes les branches distantes Fetch sans les tags - Purger les branches distantes mortes + Élaguer les branches mortes distantes Remote : - Fetch des changements distants + Récupérer les changements distants Présumer inchangé Rejeter... Rejeter {0} fichiers... Rejeter les changements dans les lignes sélectionnées - Ouvrir l'outil de merge externe - Savegarder le patch sous... - Stage - - Stage {0} files - Stage Changes in Selected Line(s) + Ouvrir l'outil de fusion externe + Enregistrer en tant que patch... + Indexer + Indexer {0} fichiers + Indexer les changements dans les lignes sélectionnées Stash... Stash {0} fichiers... - Unstage - Unstage {0} files - Unstage Changes in Selected Line(s) - Use Theirs (checkout --theirs) - Use Mine (checkout --ours) - File History - FILTER + Désindexer + Désindexer {0} fichiers + Désindexer les changements dans les lignes sélectionnées + Utiliser les miennes (checkout --ours) + Utiliser les leurs (checkout --theirs) + Historique du fichier + CONTENU + FILTRER Git-Flow Development Branch: Feature: @@ -397,9 +400,6 @@ Historique de commits Guide de longueur du sujet GIT - Fetch les dépôts distants automatiquement - Intervalle de fetch auto - minute(s) Activer auto CRLF Répertoire de clônage par défaut E-mail utilsateur @@ -492,7 +492,6 @@ ADD REMOTE RESOLVE Search Commit - Search By File Message SHA @@ -552,7 +551,6 @@ COMMITTER MONTH WEEK - YEAR COMMITS: AUTHORS: SUBMODULES @@ -599,7 +597,6 @@ COMMIT COMMIT & PUSH Modèles/Historiques - CTRL + Entrée CONFLITS DÉTECTÉS LES CONFLITS DE FICHIER SONT RÉSOLUS INCLURE LES FICHIERS NON-SUIVIS diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 96cdda3e..90341b44 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -132,6 +132,8 @@ Endereço de Email Endereço de email GIT + Buscar remotos automaticamente + Minuto(s) RASTREADOR DE PROBLEMAS Adicionar Regra de Exemplo do Github Adicionar Regra de Exemplo do Jira @@ -391,9 +393,6 @@ Commits do Histórico Comprimento do Guia de Assunto GIT - Buscar remotos automaticamente - Intervalo de Busca Automática - Minuto(s) Habilitar Auto CRLF Diretório Padrão de Clone E-mail do Usuário @@ -485,7 +484,6 @@ ADICIONAR REMOTO RESOLVER Pesquisar Commit - Pesquisar Por Arquivo Mensagem SHA @@ -545,7 +543,6 @@ COMMITTER MÊS SEMANA - ANO COMMITS: AUTORES: SUBMÓDULOS @@ -592,7 +589,6 @@ COMMIT COMMIT & PUSH Template/Histories - CTRL + Enter CONFLITOS DETECTADOS CONFLITOS DE ARQUIVOS RESOLVIDOS INCLUIR ARQUIVOS NÃO RASTREADOS diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 56b01b0b..edfc8b08 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -1,4 +1,7 @@ + + + О программе О SourceGit • Сборка с @@ -118,7 +121,7 @@ ИНФОРМАЦИЯ АВТОР ИЗМЕНЁННЫЙ - ФИКСАТОР + ИСПОЛНИТЕЛЬ Проверить ссылки, содержащие эту фиксацию ФИКСАЦИЯ СОДЕРЖИТСЯ В Отображаются только первые 100 изменений. Смотрите все изменения на вкладке ИЗМЕНЕНИЯ. @@ -136,6 +139,8 @@ Адрес электронной почты Адрес электронной почты GIT + Автоматическое извлечение внешних хранилищ + Минут(а/ы) ОТСЛЕЖИВАНИЕ ПРОБЛЕМ Добавить пример правила для Git Добавить пример правила Jira @@ -176,8 +181,8 @@ Выложить на все внешние хранилища после создания Создать новый тег Добрый: - аннотированный - лёгкий + Аннотированный + Лёгкий Удерживайте Ctrl, чтобы начать непосредственно Вырезать Удалить ветку @@ -289,10 +294,11 @@ Показать блокировки Нет заблокированных файлов Блокировка + Показывать только мои блокировки Блокировки ХБФ - Разблокировка - Принудительная разблокировка - Обрезка + Разблокировать + Принудительно разблокировать + Обрезать Запустите `git lfs prune", чтобы удалить старые файлы ХБФ из локального хранилища Забрать Забрать объекты ХБФ @@ -324,6 +330,7 @@ ХРАНИЛИЩЕ Фиксация подготовленных изменений Фиксировать и выложить подготовленные изменения + Подготовить все изменения и зафиксировать Отклонить выбранные изменения Режим доски (по-умолчанию) Принудительно перезагрузить этот репозиторий @@ -342,10 +349,10 @@ Отклонить Инициализировать хранилище Путь: - Выполняется частичный забор. Нажмите 'Отбой' для восстановления заголовка. - Выполняет запрос слияния. Нажмите 'Отбой' для восстановления заголовка. - Выполняется перенос. Нажмите 'Отбой' для восстановления заголовка. - Выполняется возврат. Нажмите 'Отбой' для восстановления заголовка. + Выполняется частичный забор. Нажмите 'Отказ' для восстановления заголовка. + Выполняет запрос слияния. Нажмите 'Отказ' для восстановления заголовка. + Выполняется перенос. Нажмите 'Отказ' для восстановления заголовка. + Выполняется возврат. Нажмите 'Отказ' для восстановления заголовка. Интерактивное перемещение целевая ветка: На: @@ -409,9 +416,6 @@ История фиксаций Длина темы фиксации GIT - Автоматическое извлечение внешних хранилищ - Интервал автоматического извлечения - Минут(а/ы) Включить автозавершение CRLF Каталог клонирования по-умолчанию Электроная почта пользователя @@ -488,14 +492,15 @@ Новое имя: Уникальное имя для данной ветки Ветка: - ОТБОЙ + Отказ + Автоматическое извлечение изменений с внешних хранилищ... Очистка (Сбор мусора и удаление) Запустить команду `git gc` для данного хранилища. Очистить всё Настройка этого хранилища ПРОДОЛЖИТЬ Открыть в файловом менеджере - поиск веток, тегов и подмодулей + Поиск веток, тегов и подмодулей ОТФИЛЬТРОВАНО ОТ: ЛОКАЛЬНЫЕ ВЕТКИ Навигация по заголовку @@ -508,11 +513,11 @@ ДОБАВИТЬ ВНЕШНЕЕ ХРАНИЛИЩЕ РАЗРЕШИТЬ Поиск фиксации - Поиск по Файл Сообщение SHA - Автор и фиксатор + Автор и исполнитель + Текущая ветка Показать теги как дерево Статистики ПОДМОДУЛИ @@ -523,7 +528,7 @@ Открыть в терминале РАБОЧИЕ ДЕРЕВЬЯ ДОБАВИТЬ РАБОЧЕЕ ДЕРЕВО - ОБРЕЗКА + ОБРЕЗАТЬ Адрес хранилища Git Сбросить текущую втеку до версии Режим сброса: @@ -568,12 +573,12 @@ ОТЛОЖЕННЫЕ Статистики ФИКСАЦИИ - ФИКСАТОРЫ + ИСПОЛНИТЕЛИ МЕСЯЦ НЕДЕЛЯ - ГОД ФИКСАЦИИ: АВТОРЫ: + ОБЗОР ПОДМОДУЛИ Добавить подмодули Копировать относительный путь @@ -581,7 +586,7 @@ Открыть подмодуль хранилища Относительный путь: Относительный каталог для хранения подмодуля. - удалить подмодуль + Удалить подмодуль ОК Копировать имя тега Удалить ${0}$... @@ -617,10 +622,11 @@ Изменить Автоподготовка Теперь вы можете подготовитть этот файл. - ФИКСАЦИЯ - ФИКСАЦИЯ и ОТПРАВКА + ЗАФИКСИРОВАТЬ + ЗАФИКСИРОВАТЬ и ОТПРАВИТЬ Шаблон/Истории - CTRL + Enter + Запуск события щелчка + Подготовить все изменения и зафиксировать ОБНАРУЖЕНЫ КОНФЛИКТЫ КОНФЛИКТЫ ФАЙЛОВ РАЗРЕШЕНЫ ВКЛЮЧИТЬ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 6d3474b6..5b753a08 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -139,6 +139,8 @@ 电子邮箱 邮箱地址 GIT配置 + 启用定时自动拉取远程更新 + 分钟 ISSUE追踪 新增匹配Github Issue规则 新增匹配Jira规则 @@ -292,6 +294,7 @@ 显示LFS对象锁 没有锁定的LFS文件 锁定 + 仅显示被我锁定的文件 LFS对象锁状态 解锁 强制解锁 @@ -327,6 +330,7 @@ 仓库页面快捷键 提交暂存区更改 提交暂存区更改并推送 + 自动暂存全部变更并提交 丢弃选中的更改 切换左边栏为分支/标签等显示模式(默认) 重新加载仓库状态 @@ -408,9 +412,6 @@ 最大历史提交数 SUBJECT字数检测 GIT配置 - 启用定时自动拉取远程更新 - 自动拉取间隔 - 分钟 自动换行转换 默认克隆路径 邮箱 @@ -487,6 +488,7 @@ 新的分支名不能与现有分支名相同 分支 : 终止合并 + 自动拉取远端变更中... 清理本仓库(GC) 本操作将执行`git gc`命令。 清空过滤规则 @@ -506,11 +508,11 @@ 添加远程 解决冲突 查找提交 - 搜索途径 文件 提交信息 提交指纹 作者及提交者 + 仅在当前分支中查找 以树型结构展示 提交统计 子模块列表 @@ -569,9 +571,9 @@ 提交者 本月 本周 - 最近一年 提交次数: 贡献者人数: + 总览 子模块 添加子模块 复制路径 @@ -618,7 +620,8 @@ 提交 提交并推送 历史输入/模板 - CTRL + Enter + 触发点击事件 + 自动暂存所有变更并提交 检测到冲突 文件冲突已解决 显示未跟踪文件 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 7d31e33e..733b3f39 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -139,6 +139,8 @@ 電子郵件 電子郵件地址 Git 設定 + 啟用定時自動提取 (fetch) 遠端更新 + 分鐘 Issue 追蹤 新增符合 GitHub Issue 規則 新增符合 Jira 規則 @@ -292,6 +294,7 @@ 顯示 LFS 物件鎖 沒有鎖定的 LFS 物件 鎖定 + 僅顯示被我鎖定的檔案 LFS 物件鎖 解鎖 強制解鎖 @@ -327,6 +330,7 @@ 存放庫頁面快速鍵 提交暫存區變更 提交暫存區變更並推送 + 自動暫存全部變更並提交 捨棄選取的變更 切換左邊欄為分支/標籤等顯示模式 (預設) 強制重新載入存放庫 @@ -412,9 +416,6 @@ 最大歷史提交數 提交標題字數偵測 Git 設定 - 啟用定時自動提取 (fetch) 遠端更新 - 自動提取間隔 - 分鐘 自動換行轉換 預設複製 (clone) 路徑 電子郵件 @@ -492,6 +493,7 @@ 新的分支名稱不能與現有分支名稱相同 分支: 中止 + 自動提取遠端變更中... 清理本存放庫 (GC) 本操作將執行 `git gc` 命令。 清空篩選規則 @@ -511,11 +513,11 @@ 新增遠端 解決衝突 搜尋提交 - 搜尋方式 檔案 提交訊息 提交編號 作者及提交者 + 僅搜尋當前分支 以樹型結構展示 提交統計 子模組列表 @@ -574,9 +576,9 @@ 提交者 本月 本週 - 最近一年 提交次數: 貢獻者人數: + 總覽 子模組 新增子模組 複製路徑 @@ -623,7 +625,8 @@ 提 交 提交並推送 歷史輸入/範本 - CTRL + Enter + 觸發點擊事件 + 自動暫存全部變更並提交 檢測到衝突 檔案衝突已解決 顯示未追蹤檔案 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 6de1a479..f0743baf 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -192,6 +192,7 @@ + @@ -962,12 +963,13 @@ - + + diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 53bc0176..036dcb57 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -45,7 +45,8 @@ - + + diff --git a/src/ViewModels/AddRemote.cs b/src/ViewModels/AddRemote.cs index 104a4ed6..f2e71f4c 100644 --- a/src/ViewModels/AddRemote.cs +++ b/src/ViewModels/AddRemote.cs @@ -104,6 +104,7 @@ namespace SourceGit.ViewModels } CallUIThread(() => { + _repo.MarkFetched(); _repo.MarkBranchesDirtyManually(); _repo.SetWatcherEnabled(true); }); diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index e3e65da0..cf04784e 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -15,10 +15,15 @@ namespace SourceGit.ViewModels public string Name { get; private set; } = string.Empty; public object Backend { get; private set; } = null; public int Depth { get; set; } = 0; - public bool IsFiltered { get; set; } = false; public bool IsSelected { get; set; } = false; public List Children { get; private set; } = new List(); + public bool IsFiltered + { + get => _isFiltered; + set => SetProperty(ref _isFiltered, value); + } + public bool IsExpanded { get => _isExpanded; @@ -46,6 +51,7 @@ namespace SourceGit.ViewModels get => Backend is Models.Branch b ? b.FriendlyName : null; } + private bool _isFiltered = false; private bool _isExpanded = false; private CornerRadius _cornerRadius = new CornerRadius(4); diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index 68311b15..5661e2ed 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -62,7 +62,16 @@ namespace SourceGit.ViewModels rs = new Commands.Stash(_repo.FullPath).Pop("stash@{0}"); } - CallUIThread(() => _repo.SetWatcherEnabled(true)); + CallUIThread(() => + { + var b = _repo.Branches.Find(x => x.IsLocal && x.Name == Branch); + if (b != null) + _repo.AutoAddBranchFilterPostCheckout(b); + + _repo.MarkBranchesDirtyManually(); + _repo.SetWatcherEnabled(true); + }); + return rs; }); } diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index c89baacf..3692be10 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -321,6 +321,9 @@ namespace SourceGit.ViewModels menu.Items.Add(resetToFirstParent); menu.Items.Add(new MenuItem { Header = "-" }); + if (File.Exists(Path.Combine(fullPath))) + TryToAddContextMenuItemsForGitLFS(menu, change.Path); + var copyPath = new MenuItem(); copyPath.Header = App.Text("CopyPath"); copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); @@ -346,6 +349,7 @@ namespace SourceGit.ViewModels public ContextMenu CreateRevisionFileContextMenu(Models.Object file) { + var menu = new ContextMenu(); var fullPath = Path.Combine(_repo.FullPath, file.Path); var explore = new MenuItem(); explore.Header = App.Text("RevealFile"); @@ -385,6 +389,10 @@ namespace SourceGit.ViewModels ev.Handled = true; }; + menu.Items.Add(explore); + menu.Items.Add(saveAs); + menu.Items.Add(new MenuItem() { Header = "-" }); + var history = new MenuItem(); history.Header = App.Text("FileHistory"); history.Icon = App.CreateMenuIcon("Icons.Histories"); @@ -406,6 +414,10 @@ namespace SourceGit.ViewModels ev.Handled = true; }; + menu.Items.Add(history); + menu.Items.Add(blame); + menu.Items.Add(new MenuItem() { Header = "-" }); + var resetToThisRevision = new MenuItem(); resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); @@ -428,6 +440,13 @@ namespace SourceGit.ViewModels ev.Handled = true; }; + menu.Items.Add(resetToThisRevision); + menu.Items.Add(resetToFirstParent); + menu.Items.Add(new MenuItem() { Header = "-" }); + + if (File.Exists(Path.Combine(fullPath))) + TryToAddContextMenuItemsForGitLFS(menu, file.Path); + var copyPath = new MenuItem(); copyPath.Header = App.Text("CopyPath"); copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); @@ -446,16 +465,6 @@ namespace SourceGit.ViewModels e.Handled = true; }; - var menu = new ContextMenu(); - menu.Items.Add(explore); - menu.Items.Add(saveAs); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(history); - menu.Items.Add(blame); - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(resetToThisRevision); - menu.Items.Add(resetToFirstParent); - menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(copyPath); menu.Items.Add(copyFileName); return menu; @@ -532,6 +541,90 @@ namespace SourceGit.ViewModels } } + private void TryToAddContextMenuItemsForGitLFS(ContextMenu menu, string path) + { + var lfsEnabled = new Commands.LFS(_repo.FullPath).IsEnabled(); + if (!lfsEnabled) + return; + + var lfs = new MenuItem(); + lfs.Header = App.Text("GitLFS"); + lfs.Icon = App.CreateMenuIcon("Icons.LFS"); + + var lfsLock = new MenuItem(); + lfsLock.Header = App.Text("GitLFS.Locks.Lock"); + lfsLock.Icon = App.CreateMenuIcon("Icons.Lock"); + lfsLock.IsEnabled = _repo.Remotes.Count > 0; + if (_repo.Remotes.Count == 1) + { + lfsLock.Click += async (_, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(_repo.Remotes[0].Name, path)); + if (succ) + App.SendNotification(_repo.FullPath, $"Lock file \"{path}\" successfully!"); + + e.Handled = true; + }; + } + else + { + foreach (var remote in _repo.Remotes) + { + var remoteName = remote.Name; + var lockRemote = new MenuItem(); + lockRemote.Header = remoteName; + lockRemote.Click += async (_, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(remoteName, path)); + if (succ) + App.SendNotification(_repo.FullPath, $"Lock file \"{path}\" successfully!"); + + e.Handled = true; + }; + lfsLock.Items.Add(lockRemote); + } + } + lfs.Items.Add(lfsLock); + + var lfsUnlock = new MenuItem(); + lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock"); + lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock"); + lfsUnlock.IsEnabled = _repo.Remotes.Count > 0; + if (_repo.Remotes.Count == 1) + { + lfsUnlock.Click += async (_, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(_repo.Remotes[0].Name, path, false)); + if (succ) + App.SendNotification(_repo.FullPath, $"Unlock file \"{path}\" successfully!"); + + e.Handled = true; + }; + } + else + { + foreach (var remote in _repo.Remotes) + { + var remoteName = remote.Name; + var unlockRemote = new MenuItem(); + unlockRemote.Header = remoteName; + unlockRemote.Click += async (_, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(remoteName, path, false)); + if (succ) + App.SendNotification(_repo.FullPath, $"Unlock file \"{path}\" successfully!"); + + e.Handled = true; + }; + lfsUnlock.Items.Add(unlockRemote); + } + } + lfs.Items.Add(lfsUnlock); + + menu.Items.Add(lfs); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + [GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")] private static partial Regex REG_LFS_FORMAT(); diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index e6d2edc1..2c925c03 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -126,6 +126,15 @@ namespace SourceGit.ViewModels CallUIThread(() => { + if (CheckoutAfterCreated) + { + _repo.AutoAddBranchFilterPostCheckout(new Models.Branch() + { + FullName = $"refs/heads/{_name}", + Upstream = BasedOn is Models.Branch { IsLocal: false } remoteBranch ? remoteBranch.FullName : string.Empty, + }); + } + _repo.MarkBranchesDirtyManually(); _repo.SetWatcherEnabled(true); }); diff --git a/src/ViewModels/Fetch.cs b/src/ViewModels/Fetch.cs index fa1986b3..e00669bf 100644 --- a/src/ViewModels/Fetch.cs +++ b/src/ViewModels/Fetch.cs @@ -62,7 +62,12 @@ namespace SourceGit.ViewModels new Commands.Fetch(_repo.FullPath, SelectedRemote.Name, Prune, NoTags, SetProgressDescription).Exec(); } - CallUIThread(() => _repo.SetWatcherEnabled(true)); + CallUIThread(() => + { + _repo.MarkFetched(); + _repo.SetWatcherEnabled(true); + }); + return true; }); } diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 9563896b..035cadfd 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -41,12 +41,12 @@ namespace SourceGit.ViewModels } } - public int ViewMode + public bool IsViewContent { - get => _viewMode; + get => _isViewContent; set { - if (SetProperty(ref _viewMode, value)) + if (SetProperty(ref _isViewContent, value)) RefreshViewContent(); } } @@ -93,10 +93,10 @@ namespace SourceGit.ViewModels return; } - if (_viewMode == 0) - SetViewContentAsDiff(); - else + if (_isViewContent) SetViewContentAsRevisionFile(); + else + SetViewContentAsDiff(); } private void SetViewContentAsRevisionFile() @@ -197,7 +197,7 @@ namespace SourceGit.ViewModels private bool _isLoading = true; private List _commits = null; private Models.Commit _selectedCommit = null; - private int _viewMode = 0; + private bool _isViewContent = false; private object _viewContent = null; } } diff --git a/src/ViewModels/LFSLocks.cs b/src/ViewModels/LFSLocks.cs index 02b3e9a6..a09e7d1e 100644 --- a/src/ViewModels/LFSLocks.cs +++ b/src/ViewModels/LFSLocks.cs @@ -1,6 +1,6 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; -using Avalonia.Collections; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -9,40 +9,49 @@ namespace SourceGit.ViewModels { public class LFSLocks : ObservableObject { + public bool HasValidUserName + { + get; + private set; + } = false; + public bool IsLoading { get => _isLoading; private set => SetProperty(ref _isLoading, value); } - public bool IsEmpty + public bool ShowOnlyMyLocks { - get => _isEmpty; - private set => SetProperty(ref _isEmpty, value); + get => _showOnlyMyLocks; + set + { + if (SetProperty(ref _showOnlyMyLocks, value)) + UpdateVisibleLocks(); + } } - public AvaloniaList Locks + public List VisibleLocks { - get; - private set; + get => _visibleLocks; + private set => SetProperty(ref _visibleLocks, value); } public LFSLocks(string repo, string remote) { _repo = repo; _remote = remote; - Locks = new AvaloniaList(); + _userName = new Commands.Config(repo).Get("user.name"); + + HasValidUserName = !string.IsNullOrEmpty(_userName); Task.Run(() => { - var collect = new Commands.LFS(_repo).Locks(_remote); + _cachedLocks = new Commands.LFS(_repo).Locks(_remote); Dispatcher.UIThread.Invoke(() => { - if (collect.Count > 0) - Locks.AddRange(collect); - + UpdateVisibleLocks(); IsLoading = false; - IsEmpty = collect.Count == 0; }); }); } @@ -59,17 +68,41 @@ namespace SourceGit.ViewModels Dispatcher.UIThread.Invoke(() => { if (succ) - Locks.Remove(lfsLock); + { + _cachedLocks.Remove(lfsLock); + UpdateVisibleLocks(); + } IsLoading = false; - IsEmpty = Locks.Count == 0; }); }); } + private void UpdateVisibleLocks() + { + if (!_showOnlyMyLocks) + { + VisibleLocks = _cachedLocks; + } + else + { + var visible = new List(); + foreach (var lfsLock in _cachedLocks) + { + if (lfsLock.User == _userName) + visible.Add(lfsLock); + } + + VisibleLocks = visible; + } + } + private string _repo; private string _remote; private bool _isLoading = true; - private bool _isEmpty = false; + private List _cachedLocks = []; + private List _visibleLocks = []; + private bool _showOnlyMyLocks = false; + private string _userName; } } diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 15c22c16..5abf50e8 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -32,8 +32,8 @@ namespace SourceGit.ViewModels { PopupHost.Active = value; - if (!_ignoreIndexChange && value is { Data: Repository }) - ActiveWorkspace.ActiveIdx = Pages.IndexOf(value); + if (!_ignoreIndexChange && value is { Data: Repository repo }) + ActiveWorkspace.ActiveIdx = ActiveWorkspace.Repositories.IndexOf(repo.FullPath); } } } @@ -131,10 +131,22 @@ namespace SourceGit.ViewModels public void MoveTab(LauncherPage from, LauncherPage to) { + _ignoreIndexChange = true; + var fromIdx = Pages.IndexOf(from); var toIdx = Pages.IndexOf(to); Pages.Move(fromIdx, toIdx); ActivePage = from; + + ActiveWorkspace.Repositories.Clear(); + foreach (var p in Pages) + { + if (p.Data is Repository r) + ActiveWorkspace.Repositories.Add(r.FullPath); + } + ActiveWorkspace.ActiveIdx = ActiveWorkspace.Repositories.IndexOf(from.Node.Id); + + _ignoreIndexChange = false; } public void GotoNextTab() @@ -164,8 +176,9 @@ namespace SourceGit.ViewModels var last = Pages[0]; if (last.Data is Repository repo) { - ActiveWorkspace.Repositories.Remove(repo.FullPath); - Models.AutoFetchManager.Instance.RemoveRepository(repo.FullPath); + ActiveWorkspace.Repositories.Clear(); + ActiveWorkspace.ActiveIdx = 0; + repo.Close(); Welcome.Instance.ClearSearchFilter(); @@ -180,6 +193,7 @@ namespace SourceGit.ViewModels App.Quit(0); } + _ignoreIndexChange = false; return; } @@ -213,6 +227,8 @@ namespace SourceGit.ViewModels if (Pages.Count == 1) return; + _ignoreIndexChange = true; + var id = ActivePage.Node.Id; foreach (var one in Pages) { @@ -221,12 +237,17 @@ namespace SourceGit.ViewModels } Pages = new AvaloniaList { ActivePage }; + ActiveWorkspace.ActiveIdx = 0; OnPropertyChanged(nameof(Pages)); + + _ignoreIndexChange = false; GC.Collect(); } public void CloseRightTabs() { + _ignoreIndexChange = true; + var endIdx = Pages.IndexOf(ActivePage); for (var i = Pages.Count - 1; i > endIdx; i--) { @@ -234,6 +255,7 @@ namespace SourceGit.ViewModels Pages.Remove(Pages[i]); } + _ignoreIndexChange = false; GC.Collect(); } @@ -270,8 +292,6 @@ namespace SourceGit.ViewModels }; repo.Open(); - ActiveWorkspace.AddRepository(repo.FullPath); - Models.AutoFetchManager.Instance.AddRepository(repo.FullPath, repo.GitDir); if (page == null) { @@ -294,6 +314,16 @@ namespace SourceGit.ViewModels } ActivePage = page; + + ActiveWorkspace.Repositories.Clear(); + foreach (var p in Pages) + { + if (p.Data is Repository r) + ActiveWorkspace.Repositories.Add(r.FullPath); + } + + if (!_ignoreIndexChange) + ActiveWorkspace.ActiveIdx = ActiveWorkspace.Repositories.IndexOf(node.Id); } public void DispatchNotification(string pageId, string message, bool isError) @@ -490,7 +520,6 @@ namespace SourceGit.ViewModels if (removeFromWorkspace) ActiveWorkspace.Repositories.Remove(repo.FullPath); - Models.AutoFetchManager.Instance.RemoveRepository(repo.FullPath); repo.Close(); } diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index 71d4a634..92114f5e 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -44,6 +44,14 @@ namespace SourceGit.ViewModels return _node.Id; } + public override bool IsInProgress() + { + if (_data is Repository { IsAutoFetching: true }) + return true; + + return base.IsInProgress(); + } + public void CopyPath() { if (_node.IsRepository) diff --git a/src/ViewModels/PopupHost.cs b/src/ViewModels/PopupHost.cs index 1a4c8bb2..01d1158c 100644 --- a/src/ViewModels/PopupHost.cs +++ b/src/ViewModels/PopupHost.cs @@ -18,7 +18,7 @@ namespace SourceGit.ViewModels public static bool CanCreatePopup() { - return Active != null && (Active._popup == null || !Active._popup.InProgress); + return Active?.IsInProgress() != true; } public static void ShowPopup(Popup popup) @@ -40,6 +40,11 @@ namespace SourceGit.ViewModels return string.Empty; } + public virtual bool IsInProgress() + { + return _popup is { InProgress: true }; + } + public async void ProcessPopup() { if (_popup != null) diff --git a/src/ViewModels/Preference.cs b/src/ViewModels/Preference.cs index ef63d61e..68966f5d 100644 --- a/src/ViewModels/Preference.cs +++ b/src/ViewModels/Preference.cs @@ -216,35 +216,6 @@ namespace SourceGit.ViewModels set => SetProperty(ref _gitDefaultCloneDir, value); } - public bool GitAutoFetch - { - get => Models.AutoFetchManager.Instance.IsEnabled; - set - { - if (Models.AutoFetchManager.Instance.IsEnabled != value) - { - Models.AutoFetchManager.Instance.IsEnabled = value; - OnPropertyChanged(); - } - } - } - - public int? GitAutoFetchInterval - { - get => Models.AutoFetchManager.Instance.Interval; - set - { - if (value is null || value < 1) - return; - - if (Models.AutoFetchManager.Instance.Interval != value) - { - Models.AutoFetchManager.Instance.Interval = (int)value; - OnPropertyChanged(); - } - } - } - public int ShellOrTerminal { get => _shellOrTerminal; @@ -337,6 +308,12 @@ namespace SourceGit.ViewModels } } + public uint StatisticsSampleColor + { + get => _statisticsSampleColor; + set => SetProperty(ref _statisticsSampleColor, value); + } + public List RepositoryNodes { get; @@ -621,5 +598,7 @@ namespace SourceGit.ViewModels private int _shellOrTerminal = -1; private int _externalMergeToolType = 0; private string _externalMergeToolPath = string.Empty; + + private uint _statisticsSampleColor = 0xFF00FF00; } } diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index 75c0f47a..49c3cb19 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -140,6 +140,8 @@ namespace SourceGit.ViewModels if (!rs) return false; + _repo.MarkFetched(); + // Use merge/rebase instead of pull as fetch is done manually. if (UseRebase) { diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 42d26f94..0667c546 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; @@ -209,6 +210,12 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _isSearchLoadingVisible, value); } + public bool OnlySearchCommitsInCurrentBranch + { + get => _onlySearchCommitsInCurrentBranch; + set => SetProperty(ref _onlySearchCommitsInCurrentBranch, value); + } + public int SearchCommitFilterType { get => _searchCommitFilterType; @@ -323,6 +330,12 @@ namespace SourceGit.ViewModels } } + public bool IsAutoFetching + { + get; + private set; + } + public void Open() { var settingsFile = Path.Combine(_gitDir, "sourcegit.settings"); @@ -359,6 +372,7 @@ namespace SourceGit.ViewModels _inProgressContext = null; _hasUnsolvedConflicts = false; + _autoFetchTimer = new Timer(AutoFetchImpl, null, 5000, 5000); RefreshAll(); } @@ -377,6 +391,9 @@ namespace SourceGit.ViewModels } _settings = null; + _autoFetchTimer.Dispose(); + _autoFetchTimer = null; + _watcher?.Dispose(); _histories.Cleanup(); _workingCopy.Cleanup(); @@ -578,13 +595,13 @@ namespace SourceGit.ViewModels visible.Add(commit); break; case 1: - visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByUser).Result(); + visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByUser, _onlySearchCommitsInCurrentBranch).Result(); break; case 2: - visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByMessage).Result(); + visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByMessage, _onlySearchCommitsInCurrentBranch).Result(); break; case 3: - visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByFile).Result(); + visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, Models.CommitSearchMethod.ByFile, _onlySearchCommitsInCurrentBranch).Result(); break; } @@ -628,6 +645,11 @@ namespace SourceGit.ViewModels _watcher.MarkWorkingCopyDirtyManually(); } + public void MarkFetched() + { + _lastFetchTime = DateTime.Now; + } + public void NavigateToCommit(string sha) { if (_histories != null) @@ -643,20 +665,49 @@ namespace SourceGit.ViewModels NavigateToCommit(_currentBranch.Head); } - public void UpdateFilter(string filter, bool toggle) + public void AutoAddBranchFilterPostCheckout(Models.Branch local) + { + if (_settings.Filters.Count == 0 || _settings.Filters.Contains(local.FullName)) + return; + + var hasLeft = false; + foreach (var b in _branches) + { + if (!b.FullName.Equals(local.FullName, StringComparison.Ordinal) && + !b.FullName.Equals(local.Upstream, StringComparison.Ordinal) && + !_settings.Filters.Contains(b.FullName)) + { + hasLeft = true; + break; + } + } + + if (!hasLeft) + _settings.Filters.Clear(); + else if (string.IsNullOrEmpty(local.Upstream) || _settings.Filters.Contains(local.Upstream)) + _settings.Filters.Add(local.FullName); + else + _settings.Filters.AddRange([local.FullName, local.Upstream]); + } + + public void UpdateFilters(List filters, bool toggle) { var changed = false; if (toggle) { - if (!_settings.Filters.Contains(filter)) + foreach (var filter in filters) { - _settings.Filters.Add(filter); - changed = true; + if (!_settings.Filters.Contains(filter)) + { + _settings.Filters.Add(filter); + changed = true; + } } } else { - changed = _settings.Filters.Remove(filter); + foreach (var filter in filters) + changed |= _settings.Filters.Remove(filter); } if (changed) @@ -837,32 +888,14 @@ namespace SourceGit.ViewModels var hasUnsolvedConflict = _workingCopy.SetData(changes); var inProgress = null as InProgressContext; - var rebaseMergeFolder = Path.Combine(_gitDir, "rebase-merge"); - var rebaseApplyFolder = Path.Combine(_gitDir, "rebase-apply"); if (File.Exists(Path.Combine(_gitDir, "CHERRY_PICK_HEAD"))) - { inProgress = new CherryPickInProgress(_fullpath); - } - else if (File.Exists(Path.Combine(_gitDir, "REBASE_HEAD")) && Directory.Exists(rebaseMergeFolder)) - { + else if (File.Exists(Path.Combine(_gitDir, "REBASE_HEAD")) && Directory.Exists(Path.Combine(_gitDir, "rebase-merge"))) inProgress = new RebaseInProgress(this); - } else if (File.Exists(Path.Combine(_gitDir, "REVERT_HEAD"))) - { inProgress = new RevertInProgress(_fullpath); - } else if (File.Exists(Path.Combine(_gitDir, "MERGE_HEAD"))) - { inProgress = new MergeInProgress(_fullpath); - } - else - { - if (Directory.Exists(rebaseMergeFolder)) - Directory.Delete(rebaseMergeFolder, true); - - if (Directory.Exists(rebaseApplyFolder)) - Directory.Delete(rebaseApplyFolder, true); - } Dispatcher.UIThread.Invoke(() => { @@ -2005,6 +2038,28 @@ namespace SourceGit.ViewModels } } + private void AutoFetchImpl(object sender) + { + if (!_settings.EnableAutoFetch || IsAutoFetching) + return; + + var lockFile = Path.Combine(_gitDir, "index.lock"); + if (File.Exists(lockFile)) + return; + + var now = DateTime.Now; + var desire = _lastFetchTime.AddMinutes(_settings.AutoFetchInterval); + if (desire > now) + return; + + IsAutoFetching = true; + Dispatcher.UIThread.Invoke(() => OnPropertyChanged(nameof(IsAutoFetching))); + new Commands.Fetch(_fullpath, "--all", true, false, null) { RaiseError = false }.Exec(); + _lastFetchTime = DateTime.Now; + IsAutoFetching = false; + Dispatcher.UIThread.Invoke(() => OnPropertyChanged(nameof(IsAutoFetching))); + } + private string _fullpath = string.Empty; private string _gitDir = string.Empty; private Models.RepositorySettings _settings = null; @@ -2023,6 +2078,7 @@ namespace SourceGit.ViewModels private bool _isSearchLoadingVisible = false; private bool _isSearchCommitSuggestionOpen = false; private int _searchCommitFilterType = 2; + private bool _onlySearchCommitsInCurrentBranch = false; private bool _enableFirstParentInHistories = false; private string _searchCommitFilter = string.Empty; private List _searchedCommits = new List(); @@ -2050,5 +2106,8 @@ namespace SourceGit.ViewModels private InProgressContext _inProgressContext = null; private bool _hasUnsolvedConflicts = false; private Models.Commit _searchResultSelectedCommit = null; + + private Timer _autoFetchTimer = null; + private DateTime _lastFetchTime = DateTime.MinValue; } } diff --git a/src/ViewModels/RepositoryConfigure.cs b/src/ViewModels/RepositoryConfigure.cs index 8ddf84a5..fe5b1a2f 100644 --- a/src/ViewModels/RepositoryConfigure.cs +++ b/src/ViewModels/RepositoryConfigure.cs @@ -42,6 +42,26 @@ namespace SourceGit.ViewModels set => SetProperty(ref _httpProxy, value); } + public bool EnableAutoFetch + { + get => _repo.Settings.EnableAutoFetch; + set => _repo.Settings.EnableAutoFetch = value; + } + + public int? AutoFetchInterval + { + get => _repo.Settings.AutoFetchInterval; + set + { + if (value is null || value < 1) + return; + + var interval = (int)value; + if (_repo.Settings.AutoFetchInterval != interval) + _repo.Settings.AutoFetchInterval = interval; + } + } + public AvaloniaList CommitTemplates { get => _repo.Settings.CommitTemplates; diff --git a/src/ViewModels/Statistics.cs b/src/ViewModels/Statistics.cs index 587b1b71..7852a367 100644 --- a/src/ViewModels/Statistics.cs +++ b/src/ViewModels/Statistics.cs @@ -1,5 +1,8 @@ using System.Threading.Tasks; + +using Avalonia.Media; using Avalonia.Threading; + using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -28,6 +31,25 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _selectedReport, value); } + public uint SampleColor + { + get => Preference.Instance.StatisticsSampleColor; + set + { + if (value != Preference.Instance.StatisticsSampleColor) + { + Preference.Instance.StatisticsSampleColor = value; + OnPropertyChanged(nameof(SampleBrush)); + _selectedReport?.ChangeColor(value); + } + } + } + + public IBrush SampleBrush + { + get => new SolidColorBrush(SampleColor); + } + public Statistics(string repo) { Task.Run(() => @@ -47,18 +69,15 @@ namespace SourceGit.ViewModels if (_data == null) return; - switch (_selectedIndex) + var report = _selectedIndex switch { - case 0: - SelectedReport = _data.Year; - break; - case 1: - SelectedReport = _data.Month; - break; - default: - SelectedReport = _data.Week; - break; - } + 0 => _data.All, + 1 => _data.Month, + _ => _data.Week, + }; + + report.ChangeColor(SampleColor); + SelectedReport = report; } private bool _isLoading = true; diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 2dad3fc2..74ff3e0e 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -240,11 +240,6 @@ namespace SourceGit.ViewModels _cached = changes; _count = _cached.Count; - var unstaged = new List(); - var staged = new List(); - var selectedUnstaged = new List(); - var selectedStaged = new List(); - var lastSelectedUnstaged = new HashSet(); var lastSelectedStaged = new HashSet(); if (_selectedUnstaged != null && _selectedUnstaged.Count > 0) @@ -258,6 +253,8 @@ namespace SourceGit.ViewModels lastSelectedStaged.Add(c.Path); } + var unstaged = new List(); + var selectedUnstaged = new List(); var hasConflict = false; foreach (var c in changes) { @@ -271,7 +268,8 @@ namespace SourceGit.ViewModels } } - staged = GetStagedChanges(); + var staged = GetStagedChanges(); + var selectedStaged = new List(); foreach (var c in staged) { if (lastSelectedStaged.Contains(c.Path)) @@ -418,12 +416,17 @@ namespace SourceGit.ViewModels public void Commit() { - DoCommit(false); + DoCommit(AutoStageBeforeCommit, false); + } + + public void CommitWithAutoStage() + { + DoCommit(true, false); } public void CommitWithPush() { - DoCommit(true); + DoCommit(AutoStageBeforeCommit, true); } public ContextMenu CreateContextMenuForUnstagedChanges() @@ -1276,7 +1279,7 @@ namespace SourceGit.ViewModels _repo.SetWatcherEnabled(true); } - private void DoCommit(bool autoPush) + private void DoCommit(bool autoStage, bool autoPush) { if (!PopupHost.CanCreatePopup()) { @@ -1290,7 +1293,6 @@ namespace SourceGit.ViewModels return; } - var autoStage = AutoStageBeforeCommit; if (!_useAmend) { if (autoStage) diff --git a/src/ViewModels/Workspace.cs b/src/ViewModels/Workspace.cs index 949cca82..d527ff48 100644 --- a/src/ViewModels/Workspace.cs +++ b/src/ViewModels/Workspace.cs @@ -51,12 +51,6 @@ namespace SourceGit.ViewModels get => new SolidColorBrush(_color); } - public void AddRepository(string repo) - { - if (!Repositories.Contains(repo)) - Repositories.Add(repo); - } - private string _name = string.Empty; private uint _color = 4278221015; private bool _isActive = false; diff --git a/src/Views/Blame.axaml.cs b/src/Views/Blame.axaml.cs index 68e14de5..f96870e0 100644 --- a/src/Views/Blame.axaml.cs +++ b/src/Views/Blame.axaml.cs @@ -33,7 +33,7 @@ namespace SourceGit.Views return; var view = TextView; - if (view != null && view.VisualLinesValid) + if (view is { VisualLinesValid: true }) { var typeface = view.CreateTypeface(); var underlinePen = new Pen(Brushes.DarkOrange); @@ -142,12 +142,12 @@ namespace SourceGit.Views return new Size(maxWidth, 0); } - protected override void OnPointerPressed(PointerPressedEventArgs e) + protected override void OnPointerMoved(PointerEventArgs e) { - base.OnPointerPressed(e); + base.OnPointerMoved(e); var view = TextView; - if (!e.Handled && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && view != null && view.VisualLinesValid) + if (!e.Handled && view is { VisualLinesValid: true }) { var pos = e.GetPosition(this); var typeface = view.CreateTypeface(); @@ -158,7 +158,48 @@ namespace SourceGit.Views continue; var lineNumber = line.FirstDocumentLine.LineNumber; - if (lineNumber >= _editor.BlameData.LineInfos.Count) + if (lineNumber > _editor.BlameData.LineInfos.Count) + break; + + var info = _editor.BlameData.LineInfos[lineNumber - 1]; + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - view.VerticalOffset; + var shaLink = new FormattedText( + info.CommitSHA, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + _editor.FontSize, + Brushes.DarkOrange); + + var rect = new Rect(0, y, shaLink.Width, shaLink.Height); + if (rect.Contains(pos)) + { + Cursor = Cursor.Parse("Hand"); + return; + } + } + } + + Cursor = Cursor.Default; + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + var view = TextView; + if (!e.Handled && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && view is { VisualLinesValid: true }) + { + var pos = e.GetPosition(this); + var typeface = view.CreateTypeface(); + + foreach (var line in view.VisualLines) + { + if (line.IsDisposed || line.FirstDocumentLine == null || line.FirstDocumentLine.IsDeleted) + continue; + + var lineNumber = line.FirstDocumentLine.LineNumber; + if (lineNumber > _editor.BlameData.LineInfos.Count) break; var info = _editor.BlameData.LineInfos[lineNumber - 1]; @@ -262,7 +303,7 @@ namespace SourceGit.Views continue; var lineNumber = line.FirstDocumentLine.LineNumber; - if (lineNumber >= BlameData.LineInfos.Count) + if (lineNumber > BlameData.LineInfos.Count) break; var info = BlameData.LineInfos[lineNumber - 1]; @@ -321,7 +362,7 @@ namespace SourceGit.Views return; var caret = TextArea.Caret; - if (caret == null || caret.Line >= BlameData.LineInfos.Count) + if (caret == null || caret.Line > BlameData.LineInfos.Count) return; _highlight = BlameData.LineInfos[caret.Line - 1].CommitSHA; diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml index 9525631c..6df2dbb8 100644 --- a/src/Views/BranchTree.axaml +++ b/src/Views/BranchTree.axaml @@ -54,10 +54,10 @@ + Text="{Binding Name}" + Classes="primary" + FontWeight="{Binding NameFontWeight}" + TextTrimming="CharacterEllipsis"/> diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index bba91274..49d15ef0 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -428,12 +428,23 @@ namespace SourceGit.Views } } - private void OnToggleFilter(object sender, RoutedEventArgs e) + private void OnToggleFilterClicked(object sender, RoutedEventArgs e) { - if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo) + if (DataContext is ViewModels.Repository repo && + sender is ToggleButton toggle && + toggle.DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch } node) { - if (toggle.DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) - repo.UpdateFilter(branch.FullName, toggle.IsChecked == true); + bool filtered = toggle.IsChecked == true; + List filters = [branch.FullName]; + if (branch.IsLocal && !string.IsNullOrEmpty(branch.Upstream)) + { + filters.Add(branch.Upstream); + + node.IsFiltered = filtered; + UpdateUpstreamFilterState(repo.RemoteBranchTrees, branch.Upstream, filtered); + } + + repo.UpdateFilters(filters, filtered); } e.Handled = true; @@ -466,6 +477,23 @@ namespace SourceGit.Views CollectBranchesInNode(outs, sub); } + private bool UpdateUpstreamFilterState(List collection, string upstream, bool isFiltered) + { + foreach (var node in collection) + { + if (node.Backend is Models.Branch b && b.FullName == upstream) + { + node.IsFiltered = isFiltered; + return true; + } + + if (node.Backend is Models.Remote r && upstream.StartsWith($"refs/remotes/{r.Name}/", StringComparison.Ordinal)) + return UpdateUpstreamFilterState(node.Children, upstream, isFiltered); + } + + return false; + } + private bool _disableSelectionChangingEvent = false; } } diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index 39d46d77..a47988f0 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -41,7 +41,7 @@ namespace SourceGit.Views e.Handled = true; } } - + if (!e.Handled && e.Key != Key.Space) base.OnKeyDown(e); } @@ -166,10 +166,10 @@ namespace SourceGit.Views { if (lastUnselected == -1) continue; - + break; } - + lastUnselected = i; } } @@ -186,10 +186,10 @@ namespace SourceGit.Views { if (lastUnselected == -1) continue; - + break; } - + lastUnselected = i; } @@ -244,7 +244,7 @@ namespace SourceGit.Views _disableSelectionChangingEvent = true; var selected = new List(); - if (sender is ListBox { SelectedItems: {} selectedItems }) + if (sender is ListBox { SelectedItems: { } selectedItems }) { foreach (var item in selectedItems) { diff --git a/src/Views/CommitChanges.axaml b/src/Views/CommitChanges.axaml index bc36b023..41e0e29d 100644 --- a/src/Views/CommitChanges.axaml +++ b/src/Views/CommitChanges.axaml @@ -9,9 +9,9 @@ x:DataType="vm:CommitDetail"> - + - + diff --git a/src/Views/ContextMenuExtension.cs b/src/Views/ContextMenuExtension.cs index 6e6dbf20..2abcf2b9 100644 --- a/src/Views/ContextMenuExtension.cs +++ b/src/Views/ContextMenuExtension.cs @@ -1,5 +1,4 @@ using System.ComponentModel; - using Avalonia.Controls; namespace SourceGit.Views @@ -15,7 +14,7 @@ namespace SourceGit.Views menu.Closing += OnContextMenuClosing; // Clear context menu because it is dynamic. control.ContextMenu = menu; - control.ContextMenu.Open(); + control.ContextMenu?.Open(); } private static void OnContextMenuClosing(object sender, CancelEventArgs e) diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml index a5e229dc..bc048706 100644 --- a/src/Views/FileHistories.axaml +++ b/src/Views/FileHistories.axaml @@ -87,6 +87,7 @@ - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/Views/Hotkeys.axaml b/src/Views/Hotkeys.axaml index cd7f9c4a..5d98238d 100644 --- a/src/Views/Hotkeys.axaml +++ b/src/Views/Hotkeys.axaml @@ -78,7 +78,7 @@ FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Increase}}" Margin="0,8"/> - + @@ -102,12 +102,15 @@ - + - + - - + + + + + - + + + + + + + + + + + - - + - + + + + + + + + diff --git a/src/Views/Launcher.axaml b/src/Views/Launcher.axaml index 3d1b1bfd..285ea72c 100644 --- a/src/Views/Launcher.axaml +++ b/src/Views/Launcher.axaml @@ -70,7 +70,7 @@ -