diff --git a/.editorconfig b/.editorconfig
index 3ad9d05b..22c741b9 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -206,6 +206,9 @@ dotnet_diagnostic.CA1854.severity = warning
#CA2211:Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = error
+# IDE0005: remove used namespace using
+dotnet_diagnostic.IDE0005.severity = error
+
# Wrapping preferences
csharp_wrap_before_ternary_opsigns = false
@@ -293,6 +296,10 @@ end_of_line = lf
[*.{cmd,bat}]
end_of_line = crlf
+# Package manifests
+[{*.spec,control}]
+end_of_line = lf
+
# YAML files
[*.{yml,yaml}]
indent_size = 2
diff --git a/.gitattributes b/.gitattributes
index 7410eb08..bd1dfea9 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -3,10 +3,12 @@
*.png binary
*.ico binary
*.sh text eol=lf
+*.spec text eol=lf
+control text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.json text
.gitattributes export-ignore
-.gitignore export-ignore
\ No newline at end of file
+.gitignore export-ignore
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ad62efb6..d4117364 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -32,7 +32,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 8.0.x
+ dotnet-version: 9.0.x
- name: Configure arm64 packages
if: ${{ matrix.runtime == 'linux-arm64' }}
run: |
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index 53affe3d..e973c1ab 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -3,7 +3,7 @@ on:
workflow_call:
inputs:
version:
- description: Source Git package version
+ description: SourceGit package version
required: true
type: string
jobs:
diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml
new file mode 100644
index 00000000..9e465fe7
--- /dev/null
+++ b/.github/workflows/publish-packages.yml
@@ -0,0 +1,39 @@
+name: Publish to Buildkite
+on:
+ workflow_call:
+ secrets:
+ BUILDKITE_TOKEN:
+ required: true
+jobs:
+ publish:
+ name: Publish to Buildkite
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ runtime: [linux-x64, linux-arm64]
+ steps:
+ - name: Download package artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: package.${{ matrix.runtime }}
+ path: packages
+
+ - name: Publish DEB package
+ env:
+ BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
+ run: |
+ FILE=$(echo packages/*.deb)
+ curl -X POST https://api.buildkite.com/v2/packages/organizations/sourcegit/registries/sourcegit-deb/packages \
+ -H "Authorization: Bearer $BUILDKITE_TOKEN" \
+ -F "file=@$FILE" \
+ --fail
+
+ - name: Publish RPM package
+ env:
+ BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
+ run: |
+ FILE=$(echo packages/*.rpm)
+ curl -X POST https://api.buildkite.com/v2/packages/organizations/sourcegit/registries/sourcegit-rpm/packages \
+ -H "Authorization: Bearer $BUILDKITE_TOKEN" \
+ -F "file=@$FILE" \
+ --fail
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c19103e3..223fe75f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,6 +24,12 @@ jobs:
uses: ./.github/workflows/package.yml
with:
version: ${{ needs.version.outputs.version }}
+ publish-packages:
+ needs: [package, version]
+ name: Publish Packages
+ uses: ./.github/workflows/publish-packages.yml
+ secrets:
+ BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
release:
needs: [package, version]
name: Release
diff --git a/README.md b/README.md
index a0fe4147..6916edbc 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
* Supports Windows/macOS/Linux
* Opensource/Free
* Fast
-* Deutsch/English/Español/Français/Português/Русский/简体中文/繁體中文
+* Deutsch/English/Español/Français/Italiano/Português/Русский/简体中文/繁體中文
* Built-in light/dark themes
* Customize theme
* Visual commit graph
@@ -47,7 +47,7 @@
## Translation Status
-[](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md)
+[](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md) [](TRANSLATION.md)
## How to Use
@@ -98,9 +98,37 @@ For **macOS** users:
For **Linux** users:
+* For Debian/Ubuntu based distributions, you can add the `sourcegit` repository by following:
+ You may need to install curl and/or gpg first, if you're on a very minimal host:
+ ```shell
+ apt update && apt install curl gpg -y
+ ```
+ Install the registry signing key:
+ ```shell
+ curl -fsSL "https://packages.buildkite.com/sourcegit/sourcegit-deb/gpgkey" | gpg --dearmor -o /etc/apt/keyrings/sourcegit_sourcegit-deb-archive-keyring.gpg
+ ```
+ Configure the source:
+ ```shell
+ echo -e "deb [signed-by=/etc/apt/keyrings/sourcegit_sourcegit-deb-archive-keyring.gpg] https://packages.buildkite.com/sourcegit/sourcegit-deb/any/ any main\ndeb-src [signed-by=/etc/apt/keyrings/sourcegit_sourcegit-deb-archive-keyring.gpg] https://packages.buildkite.com/sourcegit/sourcegit-deb/any/ any main" > /etc/apt/sources.list.d/buildkite-sourcegit-sourcegit-deb.list
+ ```
+ Update your local repository and install the package:
+ ```shell
+ apt update && apt install sourcegit
+ ```
+* For RHEL/Fedora based distributions, you can add the `sourcegit` repository by following:
+ Configure the source:
+ ```shell
+ sudo sh -c 'echo -e "[sourcegit-rpm]\nname=sourcegit-rpm\nbaseurl=https://packages.buildkite.com/sourcegit/sourcegit-rpm/rpm_any/rpm_any/\$basearch\nenabled=1\nrepo_gpgcheck=1\ngpgcheck=0\ngpgkey=https://packages.buildkite.com/sourcegit/sourcegit-rpm/gpgkey\npriority=1"' > /etc/yum.repos.d/sourcegit-rpm.repo
+ ```
+ Install the package with this command:
+ ```shell
+ sudo dnf install -y sourcegit
+ ```
+* `Appimage` files can be found on [AppimageHub](https://appimage.github.io/SourceGit/)
* `xdg-open` must be installed to support open native file manager.
* Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your linux.
* Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI.
+* If you can NOT type accented characters, such as `ê`, `ó`, try to set the environment variable `AVALONIA_IM_MODULE` to `none`.
## OpenAI
diff --git a/SourceGit.sln b/SourceGit.sln
index 9799a09e..3eeb8a54 100644
--- a/SourceGit.sln
+++ b/SourceGit.sln
@@ -13,10 +13,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F45A
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{67B6D05F-A000-40BA-ADB4-C9065F880D7B}"
ProjectSection(SolutionItems) = preProject
+ .github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\ci.yml = .github\workflows\ci.yml
.github\workflows\package.yml = .github\workflows\package.yml
.github\workflows\release.yml = .github\workflows\release.yml
.github\workflows\localization-check.yml = .github\workflows\localization-check.yml
+ .github\workflows\publish-packages.yml = .github\workflows\publish-packages.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49A7C2D6-558C-4FAA-8F5D-EEE81497AED7}"
diff --git a/TRANSLATION.md b/TRANSLATION.md
index b85511df..6e743f14 100644
--- a/TRANSLATION.md
+++ b/TRANSLATION.md
@@ -1,158 +1,194 @@
-### de_DE.axaml: 98.41%
+### de_DE.axaml: 97.50%
Missing Keys
-- Text.Diff.SaveAsPatch
-- Text.Diff.VisualLines.All
-- Text.Hotkeys.Repo.CreateBranchOnCommit
-- Text.Hotkeys.Repo.Fetch
-- Text.Hotkeys.Repo.Pull
-- Text.Hotkeys.Repo.Push
-- Text.IssueLinkCM.OpenInBrowser
-- Text.IssueLinkCM.CopyLink
-- Text.Preference.Appearance.FontSize
-- Text.Preference.Appearance.FontSize.Default
-- Text.Preference.Appearance.FontSize.Editor
+- Text.BranchCM.MergeMultiBranches
+- Text.CommitCM.Merge
+- Text.CommitCM.MergeMultiple
+- Text.CommitDetail.Files.Search
+- Text.Diff.UseBlockNavigation
+- Text.FileCM.ResolveUsing
+- Text.Hotkeys.Global.Clone
+- Text.InProgress.CherryPick.Head
+- Text.InProgress.Merge.Operating
+- Text.InProgress.Rebase.StoppedAt
+- Text.InProgress.Revert.Head
+- Text.Merge.Source
+- Text.MergeMultiple
+- Text.MergeMultiple.CommitChanges
+- Text.MergeMultiple.Strategy
+- Text.MergeMultiple.Targets
+- Text.Repository.Skip
+- Text.WorkingCopy.CommitToEdit
-### es_ES.axaml: 99.57%
+### es_ES.axaml: 97.78%
Missing Keys
-- Text.Preference.Appearance.FontSize
-- Text.Preference.Appearance.FontSize.Default
-- Text.Preference.Appearance.FontSize.Editor
+- Text.BranchCM.MergeMultiBranches
+- Text.CommitCM.Merge
+- Text.CommitCM.MergeMultiple
+- Text.Diff.UseBlockNavigation
+- Text.FileCM.ResolveUsing
+- Text.Hotkeys.Global.Clone
+- Text.InProgress.CherryPick.Head
+- Text.InProgress.Merge.Operating
+- Text.InProgress.Rebase.StoppedAt
+- Text.InProgress.Revert.Head
+- Text.Merge.Source
+- Text.MergeMultiple
+- Text.MergeMultiple.CommitChanges
+- Text.MergeMultiple.Strategy
+- Text.MergeMultiple.Targets
+- Text.Repository.Skip
-### fr_FR.axaml: 86.31%
+### fr_FR.axaml: 95.00%
Missing Keys
-- Text.About.Chart
-- Text.AIAssistant
-- Text.AIAssistant.Tip
-- Text.BranchCM.FetchInto
-- Text.ChangeCM.GenerateCommitMessage
+- Text.BranchCM.MergeMultiBranches
- Text.CherryPick.AppendSourceToMessage
-- Text.CherryPick.Mainline
- Text.CherryPick.Mainline.Tips
- Text.CommitCM.CherryPickMultiple
-- Text.CommitCM.CustomAction
-- Text.CommitCM.SquashCommitsSinceThis
-- Text.CommitDetail.Info.WebLinks
-- Text.Configure.CustomAction
-- Text.Configure.CustomAction.Arguments
-- Text.Configure.CustomAction.Arguments.Tip
-- Text.Configure.CustomAction.Executable
-- Text.Configure.CustomAction.Name
-- Text.Configure.CustomAction.Scope
-- Text.Configure.CustomAction.Scope.Commit
-- Text.Configure.CustomAction.Scope.Repository
-- Text.Configure.Git.DefaultRemote
-- Text.Configure.Git.EnablePruneOnFetch
-- Text.Configure.Git.EnableSignOff
-- Text.Configure.IssueTracker.AddSampleGitLabIssue
-- Text.Configure.IssueTracker.AddSampleGitLabMergeRequest
-- Text.Configure.OpenAI
-- Text.Configure.OpenAI.Prefered
-- Text.Configure.OpenAI.Prefered.Tip
-- Text.ConfigureWorkspace
-- Text.ConfigureWorkspace.Color
-- Text.ConfigureWorkspace.Restore
-- Text.ConventionalCommit
-- Text.ConventionalCommit.BreakingChanges
-- Text.ConventionalCommit.ClosedIssue
-- Text.ConventionalCommit.Detail
-- Text.ConventionalCommit.Scope
-- Text.ConventionalCommit.ShortDescription
-- Text.ConventionalCommit.Type
-- Text.Diff.IgnoreWhitespace
-- Text.Diff.SaveAsPatch
-- Text.Diff.VisualLines.All
-- Text.Discard.IncludeIgnored
-- Text.ExecuteCustomAction
-- Text.ExecuteCustomAction.Name
-- Text.FileHistory.FileChange
-- Text.GitLFS.Locks.OnlyMine
-- Text.Histories.Header.AuthorTime
-- Text.Histories.Tips
-- Text.Histories.Tips.MacOS
-- Text.Histories.Tips.Prefix
-- Text.Hotkeys.Repo.CommitWithAutoStage
-- Text.Hotkeys.Repo.CreateBranchOnCommit
-- Text.Hotkeys.Repo.DiscardSelected
-- Text.Hotkeys.Repo.Fetch
-- Text.Hotkeys.Repo.Pull
-- Text.Hotkeys.Repo.Push
-- Text.IssueLinkCM.OpenInBrowser
-- Text.IssueLinkCM.CopyLink
-- Text.MoveRepositoryNode
-- Text.MoveRepositoryNode.Target
-- Text.Preference.AI
-- Text.Preference.AI.AnalyzeDiffPrompt
-- Text.Preference.AI.ApiKey
-- Text.Preference.AI.GenerateSubjectPrompt
-- Text.Preference.AI.Model
-- Text.Preference.AI.Name
-- Text.Preference.AI.Server
+- Text.CommitCM.Merge
+- Text.CommitCM.MergeMultiple
+- Text.CommitDetail.Files.Search
+- Text.Diff.UseBlockNavigation
+- Text.Fetch.Force
+- Text.FileCM.ResolveUsing
+- Text.Hotkeys.Global.Clone
+- Text.InProgress.CherryPick.Head
+- Text.InProgress.Merge.Operating
+- Text.InProgress.Rebase.StoppedAt
+- Text.InProgress.Revert.Head
+- Text.Merge.Source
+- Text.MergeMultiple
+- Text.MergeMultiple.CommitChanges
+- Text.MergeMultiple.Strategy
+- Text.MergeMultiple.Targets
- Text.Preference.Appearance.FontSize
- Text.Preference.Appearance.FontSize.Default
- Text.Preference.Appearance.FontSize.Editor
-- Text.Preference.General.ShowAuthorTime
-- Text.Preference.Integration
-- Text.Preference.Shell
-- Text.Preference.Shell.Type
-- Text.Preference.Shell.Path
-- Text.Repository.AutoFetching
+- Text.Preference.General.ShowChildren
- Text.Repository.CustomActions
-- Text.Repository.CustomActions.Empty
-- Text.Repository.EnableReflog
-- Text.Repository.Search.InCurrentBranch
+- Text.Repository.FilterCommits
+- Text.Repository.FilterCommits.Default
+- Text.Repository.FilterCommits.Exclude
+- Text.Repository.FilterCommits.Include
+- Text.Repository.HistoriesOrder
+- Text.Repository.HistoriesOrder.ByDate
+- Text.Repository.HistoriesOrder.Topo
+- Text.Repository.Skip
- Text.ScanRepositories
-- Text.ScanRepositories.RootDir
-- Text.Squash.Into
-- Text.Stash.KeepIndex
-- Text.Stash.OnlyStagedChanges
-- Text.Stash.TipForSelectedFiles
-- Text.Statistics.Overview
-- Text.TagCM.CopyMessage
-- Text.Welcome.Move
-- Text.Welcome.ScanDefaultCloneDir
-- Text.WorkingCopy.CommitTip
-- Text.WorkingCopy.CommitWithAutoStage
-- Text.WorkingCopy.ConfirmCommitWithoutFiles
-- Text.Workspace
-- Text.Workspace.Configure
+- Text.SHALinkCM.NavigateTo
+- Text.WorkingCopy.CommitToEdit
-### pt_BR.axaml: 99.57%
+### it_IT.axaml: 95.56%
Missing Keys
-- Text.Preference.Appearance.FontSize
-- Text.Preference.Appearance.FontSize.Default
-- Text.Preference.Appearance.FontSize.Editor
+- Text.BranchCM.MergeMultiBranches
+- Text.CommitCM.Merge
+- Text.CommitCM.MergeMultiple
+- Text.CommitDetail.Files.Search
+- Text.CommitDetail.Info.Children
+- Text.Configure.IssueTracker.AddSampleGitLabMergeRequest
+- Text.Configure.OpenAI.Preferred
+- Text.Configure.OpenAI.Preferred.Tip
+- Text.Diff.UseBlockNavigation
+- Text.Fetch.Force
+- Text.FileCM.ResolveUsing
+- Text.InProgress.CherryPick.Head
+- Text.InProgress.Merge.Operating
+- Text.InProgress.Rebase.StoppedAt
+- Text.InProgress.Revert.Head
+- Text.Merge.Source
+- Text.MergeMultiple
+- Text.MergeMultiple.CommitChanges
+- Text.MergeMultiple.Strategy
+- Text.MergeMultiple.Targets
+- Text.Preference.General.ShowChildren
+- Text.Repository.FilterCommits
+- Text.Repository.FilterCommits.Default
+- Text.Repository.FilterCommits.Exclude
+- Text.Repository.FilterCommits.Include
+- Text.Repository.HistoriesOrder
+- Text.Repository.HistoriesOrder.ByDate
+- Text.Repository.HistoriesOrder.Topo
+- Text.Repository.Skip
+- Text.SHALinkCM.CopySHA
+- Text.SHALinkCM.NavigateTo
+- Text.WorkingCopy.CommitToEdit
-### ru_RU.axaml: 100.00%
+### pt_BR.axaml: 96.81%
Missing Keys
+- Text.BranchCM.MergeMultiBranches
+- Text.CommitCM.Merge
+- Text.CommitCM.MergeMultiple
+- Text.CommitDetail.Files.Search
+- Text.CommitDetail.Info.Children
+- Text.Diff.UseBlockNavigation
+- Text.Fetch.Force
+- Text.FileCM.ResolveUsing
+- Text.Hotkeys.Global.Clone
+- Text.InProgress.CherryPick.Head
+- Text.InProgress.Merge.Operating
+- Text.InProgress.Rebase.StoppedAt
+- Text.InProgress.Revert.Head
+- Text.Merge.Source
+- Text.MergeMultiple
+- Text.MergeMultiple.CommitChanges
+- Text.MergeMultiple.Strategy
+- Text.MergeMultiple.Targets
+- Text.Preference.General.ShowChildren
+- Text.Repository.FilterCommits
+- Text.Repository.Skip
+- Text.SHALinkCM.NavigateTo
+- Text.WorkingCopy.CommitToEdit
+
+
+### ru_RU.axaml: 97.92%
+
+
+
+Missing Keys
+
+- Text.BranchCM.MergeMultiBranches
+- Text.CommitCM.Merge
+- Text.CommitCM.MergeMultiple
+- Text.FileCM.ResolveUsing
+- Text.Hotkeys.Global.Clone
+- Text.InProgress.CherryPick.Head
+- Text.InProgress.Merge.Operating
+- Text.InProgress.Rebase.StoppedAt
+- Text.InProgress.Revert.Head
+- Text.Merge.Source
+- Text.MergeMultiple
+- Text.MergeMultiple.CommitChanges
+- Text.MergeMultiple.Strategy
+- Text.MergeMultiple.Targets
+- Text.Repository.Skip
diff --git a/VERSION b/VERSION
index 833d11c8..d72e21bd 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.38
\ No newline at end of file
+8.42
\ No newline at end of file
diff --git a/build/README.md b/build/README.md
index a75f4d73..b4358a55 100644
--- a/build/README.md
+++ b/build/README.md
@@ -5,7 +5,7 @@
## How to build this project manually
-1. Make sure [.NET SDK 8](https://dotnet.microsoft.com/en-us/download) is installed on your machine.
+1. Make sure [.NET SDK 9](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
diff --git a/build/resources/_common/applications/sourcegit.desktop b/build/resources/_common/applications/sourcegit.desktop
index ff7ef135..bcf9c813 100644
--- a/build/resources/_common/applications/sourcegit.desktop
+++ b/build/resources/_common/applications/sourcegit.desktop
@@ -1,5 +1,5 @@
[Desktop Entry]
-Name=Source Git
+Name=SourceGit
Comment=Open-source & Free Git GUI Client
Exec=/opt/sourcegit/sourcegit
Icon=/usr/share/icons/sourcegit.png
diff --git a/build/resources/rpm/SPECS/build.spec b/build/resources/rpm/SPECS/build.spec
index 9dda5f96..bc10ca48 100644
--- a/build/resources/rpm/SPECS/build.spec
+++ b/build/resources/rpm/SPECS/build.spec
@@ -5,8 +5,9 @@ Summary: Open-source & Free Git Gui Client
License: MIT
URL: https://sourcegit-scm.github.io/
Source: https://github.com/sourcegit-scm/sourcegit/archive/refs/tags/v%_version.tar.gz
-Requires: (libX11 or libX11-6)
-Requires: (libSM or libSM6)
+Requires: libX11.so.6()(%{__isa_bits}bit)
+Requires: libSM.so.6()(%{__isa_bits}bit)
+Requires: libicu
%define _build_id_links none
diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh
index 5ffd5a27..5abb058b 100755
--- a/build/scripts/package.linux.sh
+++ b/build/scripts/package.linux.sh
@@ -5,16 +5,6 @@ set -o
set -u
set pipefail
-if [[ -z "$VERSION" ]]; then
- echo "Provide the version as environment variable VERSION"
- exit 1
-fi
-
-if [[ -z "$RUNTIME" ]]; then
- echo "Provide the runtime as environment variable RUNTIME"
- exit 1
-fi
-
arch=
appimage_arch=
target=
diff --git a/build/scripts/package.osx-app.sh b/build/scripts/package.osx-app.sh
index 4a50a860..2d43e24a 100755
--- a/build/scripts/package.osx-app.sh
+++ b/build/scripts/package.osx-app.sh
@@ -5,16 +5,6 @@ set -o
set -u
set pipefail
-if [[ -z "$VERSION" ]]; then
- echo "Provide the version as environment variable VERSION"
- exit 1
-fi
-
-if [[ -z "$RUNTIME" ]]; then
- echo "Provide the runtime as environment variable RUNTIME"
- exit 1
-fi
-
cd build
mkdir -p SourceGit.app/Contents/Resources
diff --git a/build/scripts/package.windows-portable.sh b/build/scripts/package.windows-portable.sh
index 9ba29216..6bd3879b 100755
--- a/build/scripts/package.windows-portable.sh
+++ b/build/scripts/package.windows-portable.sh
@@ -5,16 +5,6 @@ set -o
set -u
set pipefail
-if [[ -z "$VERSION" ]]; then
- echo "Provide the version as environment variable VERSION"
- exit 1
-fi
-
-if [[ -z "$RUNTIME" ]]; then
- echo "Provide the runtime as environment variable RUNTIME"
- exit 1
-fi
-
cd build
rm -rf SourceGit/*.pdb
diff --git a/global.json b/global.json
index b5b37b60..a27a2b82 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.0",
+ "version": "9.0.0",
"rollForward": "latestMajor",
"allowPrerelease": false
}
diff --git a/src/App.axaml b/src/App.axaml
index 0b809a84..59418965 100644
--- a/src/App.axaml
+++ b/src/App.axaml
@@ -14,6 +14,7 @@
+
diff --git a/src/App.axaml.cs b/src/App.axaml.cs
index dfec763b..0615724a 100644
--- a/src/App.axaml.cs
+++ b/src/App.axaml.cs
@@ -478,17 +478,20 @@ namespace SourceGit
if (args.Length <= 1 || !args[0].Equals("--rebase-message-editor", StringComparison.Ordinal))
return false;
+ exitCode = 0;
+
var file = args[1];
var filename = Path.GetFileName(file);
if (!filename.Equals("COMMIT_EDITMSG", StringComparison.OrdinalIgnoreCase))
return true;
- var jobsFile = Path.Combine(Path.GetDirectoryName(file)!, "sourcegit_rebase_jobs.json");
+ var gitDir = Path.GetDirectoryName(file)!;
+ var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json");
if (!File.Exists(jobsFile))
return true;
var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection);
- var doneFile = Path.Combine(Path.GetDirectoryName(file)!, "rebase-merge", "done");
+ var doneFile = Path.Combine(gitDir, "rebase-merge", "done");
if (!File.Exists(doneFile))
return true;
@@ -499,7 +502,6 @@ namespace SourceGit
var job = collection.Jobs[done.Length - 1];
File.WriteAllText(file, job.Message);
- exitCode = 0;
return true;
}
diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs
index 890b54ee..391aeeb2 100644
--- a/src/Commands/Branch.cs
+++ b/src/Commands/Branch.cs
@@ -2,6 +2,15 @@
{
public static class Branch
{
+ public static string ShowCurrent(string repo)
+ {
+ var cmd = new Command();
+ cmd.WorkingDirectory = repo;
+ cmd.Context = repo;
+ cmd.Args = $"branch --show-current";
+ return cmd.ReadToEnd().StdOut.Trim();
+ }
+
public static bool Create(string repo, string name, string basedOn)
{
var cmd = new Command();
@@ -48,8 +57,18 @@
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
- cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
- cmd.Args = $"push {remote} --delete {name}";
+
+ bool exists = new Remote(repo).HasBranch(remote, name);
+ if (exists)
+ {
+ cmd.SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
+ cmd.Args = $"push {remote} --delete {name}";
+ }
+ else
+ {
+ cmd.Args = $"branch -D -r {remote}/{name}";
+ }
+
return cmd.Exec();
}
}
diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs
index 8d304410..96a5b9c9 100644
--- a/src/Commands/Command.cs
+++ b/src/Commands/Command.cs
@@ -74,7 +74,11 @@ namespace SourceGit.Commands
}
if (string.IsNullOrEmpty(e.Data))
+ {
+ errs.Add(string.Empty);
return;
+ }
+
if (TraitErrorAsOutput)
OnReadline(e.Data);
@@ -89,6 +93,7 @@ namespace SourceGit.Commands
return;
if (REG_PROGRESS().IsMatch(e.Data))
return;
+
errs.Add(e.Data);
};
@@ -99,12 +104,8 @@ namespace SourceGit.Commands
catch (Exception e)
{
if (RaiseError)
- {
- Dispatcher.UIThread.Invoke(() =>
- {
- App.RaiseException(Context, e.Message);
- });
- }
+ Dispatcher.UIThread.Post(() => App.RaiseException(Context, e.Message));
+
return false;
}
@@ -115,15 +116,15 @@ namespace SourceGit.Commands
int exitCode = proc.ExitCode;
proc.Close();
- if (!isCancelled && exitCode != 0 && errs.Count > 0)
+ if (!isCancelled && exitCode != 0)
{
if (RaiseError)
{
- Dispatcher.UIThread.Invoke(() =>
- {
- App.RaiseException(Context, string.Join("\n", errs));
- });
+ var errMsg = string.Join("\n", errs);
+ if (!string.IsNullOrWhiteSpace(errMsg))
+ Dispatcher.UIThread.Post(() => App.RaiseException(Context, errMsg));
}
+
return false;
}
diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs
index cb9e6a18..da971e58 100644
--- a/src/Commands/Diff.cs
+++ b/src/Commands/Diff.cs
@@ -28,9 +28,9 @@ namespace SourceGit.Commands
Context = repo;
if (ignoreWhitespace)
- Args = $"diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}";
+ Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}";
else
- Args = $"diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
+ Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
}
public Models.DiffResult Result()
diff --git a/src/Commands/Fetch.cs b/src/Commands/Fetch.cs
index 08d2d1c6..1c3e78cb 100644
--- a/src/Commands/Fetch.cs
+++ b/src/Commands/Fetch.cs
@@ -4,7 +4,7 @@ namespace SourceGit.Commands
{
public class Fetch : Command
{
- public Fetch(string repo, string remote, bool noTags, bool prune, Action outputHandler)
+ public Fetch(string repo, string remote, bool noTags, bool prune, bool force, Action outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
@@ -16,6 +16,9 @@ namespace SourceGit.Commands
if (noTags)
Args += "--no-tags ";
else
+ Args += "--tags ";
+
+ if (force)
Args += "--force ";
if (prune)
diff --git a/src/Commands/FormatPatch.cs b/src/Commands/FormatPatch.cs
index 2c7359c0..b3ec2e4a 100644
--- a/src/Commands/FormatPatch.cs
+++ b/src/Commands/FormatPatch.cs
@@ -6,7 +6,7 @@
{
WorkingDirectory = repo;
Context = repo;
- Args = $"format-patch {commit} -1 -o \"{saveTo}\"";
+ Args = $"format-patch {commit} -1 --output=\"{saveTo}\"";
}
}
}
diff --git a/src/Commands/Merge.cs b/src/Commands/Merge.cs
index cf2e285f..bd1f3653 100644
--- a/src/Commands/Merge.cs
+++ b/src/Commands/Merge.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Text;
namespace SourceGit.Commands
{
@@ -13,6 +15,29 @@ namespace SourceGit.Commands
Args = $"merge --progress {source} {mode}";
}
+ public Merge(string repo, List targets, bool autoCommit, string strategy, Action outputHandler)
+ {
+ _outputHandler = outputHandler;
+ WorkingDirectory = repo;
+ Context = repo;
+ TraitErrorAsOutput = true;
+
+ var builder = new StringBuilder();
+ builder.Append("merge --progress ");
+ if (!string.IsNullOrEmpty(strategy))
+ builder.Append($"--strategy={strategy} ");
+ if (!autoCommit)
+ builder.Append("--no-commit ");
+
+ foreach (var t in targets)
+ {
+ builder.Append(t);
+ builder.Append(' ');
+ }
+
+ Args = builder.ToString();
+ }
+
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
diff --git a/src/Commands/QueryCommitChildren.cs b/src/Commands/QueryCommitChildren.cs
new file mode 100644
index 00000000..bef09abb
--- /dev/null
+++ b/src/Commands/QueryCommitChildren.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+
+namespace SourceGit.Commands
+{
+ public class QueryCommitChildren : Command
+ {
+ public QueryCommitChildren(string repo, string commit, int max)
+ {
+ WorkingDirectory = repo;
+ Context = repo;
+ _commit = commit;
+ Args = $"rev-list -{max} --parents --branches --remotes ^{commit}";
+ }
+
+ public IEnumerable Result()
+ {
+ Exec();
+ return _lines;
+ }
+
+ protected override void OnReadline(string line)
+ {
+ if (line.Contains(_commit))
+ _lines.Add(line.Substring(0, 40));
+ }
+
+ private string _commit;
+ private List _lines = new List();
+ }
+}
diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs
index 5875301e..80497a90 100644
--- a/src/Commands/QueryCommits.cs
+++ b/src/Commands/QueryCommits.cs
@@ -6,11 +6,13 @@ namespace SourceGit.Commands
{
public class QueryCommits : Command
{
- public QueryCommits(string repo, string limits, bool needFindHead = true)
+ public QueryCommits(string repo, bool useTopoOrder, string limits, bool needFindHead = true)
{
+ var order = useTopoOrder ? "--topo-order" : "--date-order";
+
WorkingDirectory = repo;
Context = repo;
- Args = "log --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 " + limits;
+ Args = $"log {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 {limits}";
_findFirstMerged = needFindHead;
}
diff --git a/src/Commands/QueryCommitsWithFullMessage.cs b/src/Commands/QueryCommitsWithFullMessage.cs
index 116cb3cd..c15cdbe1 100644
--- a/src/Commands/QueryCommitsWithFullMessage.cs
+++ b/src/Commands/QueryCommitsWithFullMessage.cs
@@ -52,16 +52,28 @@ namespace SourceGit.Commands
_current.Commit.CommitterTime = ulong.Parse(line);
break;
default:
- if (line.Equals(_boundary, StringComparison.Ordinal))
- nextPartIdx = -1;
+ var boundary = rs.StdOut.IndexOf(_boundary, end + 1);
+ if (boundary > end)
+ {
+ _current.Message = rs.StdOut.Substring(start, boundary - start - 1);
+ end = boundary + _boundary.Length;
+ }
else
- _current.Message += line;
+ {
+ _current.Message = rs.StdOut.Substring(start);
+ end = rs.StdOut.Length - 2;
+ }
+
+ nextPartIdx = -1;
break;
}
nextPartIdx++;
start = end + 1;
+ if (start >= rs.StdOut.Length - 1)
+ break;
+
end = rs.StdOut.IndexOf('\n', start);
}
diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs
index bdef9bf8..ea422215 100644
--- a/src/Commands/QueryLocalChanges.cs
+++ b/src/Commands/QueryLocalChanges.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
diff --git a/src/Commands/QueryCurrentRevisionFiles.cs b/src/Commands/QueryRevisionFileNames.cs
similarity index 55%
rename from src/Commands/QueryCurrentRevisionFiles.cs
rename to src/Commands/QueryRevisionFileNames.cs
index 217ea20e..d2d69614 100644
--- a/src/Commands/QueryCurrentRevisionFiles.cs
+++ b/src/Commands/QueryRevisionFileNames.cs
@@ -1,19 +1,19 @@
namespace SourceGit.Commands
{
- public class QueryCurrentRevisionFiles : Command
+ public class QueryRevisionFileNames : Command
{
- public QueryCurrentRevisionFiles(string repo)
+ public QueryRevisionFileNames(string repo, string revision)
{
WorkingDirectory = repo;
Context = repo;
- Args = "ls-tree -r --name-only HEAD";
+ Args = $"ls-tree -r -z --name-only {revision}";
}
public string[] Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
- return rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
+ return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
return [];
}
diff --git a/src/Commands/QueryRevisionObjects.cs b/src/Commands/QueryRevisionObjects.cs
index bcad9129..de3406e8 100644
--- a/src/Commands/QueryRevisionObjects.cs
+++ b/src/Commands/QueryRevisionObjects.cs
@@ -12,7 +12,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
- Args = $"ls-tree {sha}";
+ Args = $"ls-tree -z {sha}";
if (!string.IsNullOrEmpty(parentFolder))
Args += $" -- \"{parentFolder}\"";
@@ -20,11 +20,27 @@ namespace SourceGit.Commands
public List Result()
{
- Exec();
+ var rs = ReadToEnd();
+ if (rs.IsSuccess)
+ {
+ var start = 0;
+ var end = rs.StdOut.IndexOf('\0', start);
+ while (end > 0)
+ {
+ var line = rs.StdOut.Substring(start, end - start);
+ Parse(line);
+ start = end + 1;
+ end = rs.StdOut.IndexOf('\0', start);
+ }
+
+ if (start < rs.StdOut.Length)
+ Parse(rs.StdOut.Substring(start));
+ }
+
return _objects;
}
- protected override void OnReadline(string line)
+ private void Parse(string line)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
diff --git a/src/Commands/Remote.cs b/src/Commands/Remote.cs
index f2e8d09e..beaf412b 100644
--- a/src/Commands/Remote.cs
+++ b/src/Commands/Remote.cs
@@ -45,5 +45,14 @@
Args = "remote set-url" + (isPush ? " --push " : " ") + $"{name} {url}";
return Exec();
}
+
+ public bool HasBranch(string remote, string branch)
+ {
+ SSHKey = new Config(WorkingDirectory).Get($"remote.{remote}.sshkey");
+ Args = $"ls-remote {remote} {branch}";
+
+ var rs = ReadToEnd();
+ return rs.IsSuccess && rs.StdOut.Trim().Length > 0;
+ }
}
}
diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs
index adfcc574..511c43e8 100644
--- a/src/Commands/Statistics.cs
+++ b/src/Commands/Statistics.cs
@@ -4,11 +4,11 @@ namespace SourceGit.Commands
{
public class Statistics : Command
{
- public Statistics(string repo)
+ public Statistics(string repo, int max)
{
WorkingDirectory = repo;
Context = repo;
- Args = $"log --date-order --branches --remotes -40000 --pretty=format:\"%ct$%aN\"";
+ Args = $"log --date-order --branches --remotes -{max} --pretty=format:\"%ct$%aN\"";
}
public Models.Statistics Result()
diff --git a/src/Converters/FilterModeConverters.cs b/src/Converters/FilterModeConverters.cs
new file mode 100644
index 00000000..c486af5e
--- /dev/null
+++ b/src/Converters/FilterModeConverters.cs
@@ -0,0 +1,22 @@
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace SourceGit.Converters
+{
+ public static class FilterModeConverters
+ {
+ public static readonly FuncValueConverter ToBorderBrush =
+ new FuncValueConverter(v =>
+ {
+ switch (v)
+ {
+ case Models.FilterMode.Included:
+ return Brushes.Green;
+ case Models.FilterMode.Excluded:
+ return Brushes.Red;
+ default:
+ return Brushes.Transparent;
+ }
+ });
+ }
+}
diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs
index ac6b8c67..0ba320c1 100644
--- a/src/Models/Branch.cs
+++ b/src/Models/Branch.cs
@@ -7,6 +7,8 @@ namespace SourceGit.Models
public List Ahead { get; set; } = new List();
public List Behind { get; set; } = new List();
+ public bool IsVisible => Ahead.Count > 0 || Behind.Count > 0;
+
public override string ToString()
{
if (Ahead.Count == 0 && Behind.Count == 0)
diff --git a/src/Models/CommitTemplate.cs b/src/Models/CommitTemplate.cs
index b34fa5a5..56e1992c 100644
--- a/src/Models/CommitTemplate.cs
+++ b/src/Models/CommitTemplate.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Text.RegularExpressions;
+using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -9,9 +6,6 @@ namespace SourceGit.Models
{
public partial class CommitTemplate : ObservableObject
{
- [GeneratedRegex(@"\$\{files(\:\d+)?\}")]
- private static partial Regex REG_COMMIT_TEMPLATE_FILES();
-
public string Name
{
get => _name;
@@ -26,55 +20,8 @@ namespace SourceGit.Models
public string Apply(Branch branch, List changes)
{
- var content = _content
- .Replace("${files_num}", $"{changes.Count}")
- .Replace("${branch_name}", branch.Name);
-
- var matches = REG_COMMIT_TEMPLATE_FILES().Matches(content);
- if (matches.Count == 0)
- return content;
-
- var builder = new StringBuilder();
- var last = 0;
- for (int i = 0; i < matches.Count; i++)
- {
- var match = matches[i];
- if (!match.Success)
- continue;
-
- var start = match.Index;
- if (start != last)
- builder.Append(content.Substring(last, start - last));
-
- var countStr = match.Groups[1].Value;
- var paths = new List();
- var more = string.Empty;
- if (countStr is { Length: <= 1 })
- {
- foreach (var c in changes)
- paths.Add(c.Path);
- }
- else
- {
- var count = Math.Min(int.Parse(countStr.Substring(1)), changes.Count);
- for (int j = 0; j < count; j++)
- paths.Add(changes[j].Path);
-
- if (count < changes.Count)
- more = $" and {changes.Count - count} other files";
- }
-
- builder.Append(string.Join(", ", paths));
- if (!string.IsNullOrEmpty(more))
- builder.Append(more);
-
- last = start + match.Length;
- }
-
- if (last != content.Length - 1)
- builder.Append(content.Substring(last));
-
- return builder.ToString();
+ var te = new TemplateEngine();
+ return te.Eval(_content, branch, changes);
}
private string _name = string.Empty;
diff --git a/src/Models/ExternalMerger.cs b/src/Models/ExternalMerger.cs
index b5398835..a09f808c 100644
--- a/src/Models/ExternalMerger.cs
+++ b/src/Models/ExternalMerger.cs
@@ -39,7 +39,7 @@ namespace SourceGit.Models
new ExternalMerger(4, "tortoise_merge", "Tortoise Merge", "TortoiseMerge.exe;TortoiseGitMerge.exe", "-base:\"$BASE\" -theirs:\"$REMOTE\" -mine:\"$LOCAL\" -merged:\"$MERGED\"", "-base:\"$LOCAL\" -theirs:\"$REMOTE\""),
new ExternalMerger(5, "kdiff3", "KDiff3", "kdiff3.exe", "\"$REMOTE\" -b \"$BASE\" \"$LOCAL\" -o \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(6, "beyond_compare", "Beyond Compare", "BComp.exe", "\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
- new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "-u -e \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""),
+ new ExternalMerger(7, "win_merge", "WinMerge", "WinMergeU.exe", "\"$MERGED\"", "-u -e \"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(8, "codium", "VSCodium", "VSCodium.exe", "-n --wait \"$MERGED\"", "-n --wait --diff \"$LOCAL\" \"$REMOTE\""),
new ExternalMerger(9, "p4merge", "P4Merge", "p4merge.exe", "-tw 4 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", "-tw 4 \"$LOCAL\" \"$REMOTE\""),
};
diff --git a/src/Models/Filter.cs b/src/Models/Filter.cs
new file mode 100644
index 00000000..af4569fa
--- /dev/null
+++ b/src/Models/Filter.cs
@@ -0,0 +1,60 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace SourceGit.Models
+{
+ public enum FilterType
+ {
+ LocalBranch = 0,
+ LocalBranchFolder,
+ RemoteBranch,
+ RemoteBranchFolder,
+ Tag,
+ }
+
+ public enum FilterMode
+ {
+ None = 0,
+ Included,
+ Excluded,
+ }
+
+ public class Filter : ObservableObject
+ {
+ public string Pattern
+ {
+ get => _pattern;
+ set => SetProperty(ref _pattern, value);
+ }
+
+ public FilterType Type
+ {
+ get;
+ set;
+ } = FilterType.LocalBranch;
+
+ public FilterMode Mode
+ {
+ get => _mode;
+ set => SetProperty(ref _mode, value);
+ }
+
+ public bool IsBranch
+ {
+ get => Type != FilterType.Tag;
+ }
+
+ public Filter()
+ {
+ }
+
+ public Filter(string pattern, FilterType type, FilterMode mode)
+ {
+ _pattern = pattern;
+ _mode = mode;
+ Type = type;
+ }
+
+ private string _pattern = string.Empty;
+ private FilterMode _mode = FilterMode.None;
+ }
+}
diff --git a/src/Models/Locales.cs b/src/Models/Locales.cs
index f68bfd7e..802e88ef 100644
--- a/src/Models/Locales.cs
+++ b/src/Models/Locales.cs
@@ -12,6 +12,7 @@ namespace SourceGit.Models
new Locale("English", "en_US"),
new Locale("Español", "es_ES"),
new Locale("Français", "fr_FR"),
+ new Locale("Italiano", "it_IT"),
new Locale("Português (Brasil)", "pt_BR"),
new Locale("Русский", "ru_RU"),
new Locale("简体中文", "zh_CN"),
diff --git a/src/Models/MergeStrategy.cs b/src/Models/MergeStrategy.cs
new file mode 100644
index 00000000..ab1d446b
--- /dev/null
+++ b/src/Models/MergeStrategy.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace SourceGit.Models
+{
+ public class MergeStrategy
+ {
+ public string Name { get; internal set; }
+ public string Desc { get; internal set; }
+ public string Arg { get; internal set; }
+
+ public static List ForMultiple { get; private set; } = [
+ new MergeStrategy("Default", "Let Git automatically select a strategy", string.Empty),
+ new MergeStrategy("Octopus", "Attempt merging multiple heads", "octopus"),
+ new MergeStrategy("Ours", "Record the merge without modifying the tree", "ours"),
+ ];
+
+ public MergeStrategy(string n, string d, string a)
+ {
+ Name = n;
+ Desc = d;
+ Arg = a;
+ }
+ }
+}
diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs
index e9c7b5ed..df67ff66 100644
--- a/src/Models/OpenAI.cs
+++ b/src/Models/OpenAI.cs
@@ -150,12 +150,17 @@ namespace SourceGit.Models
public OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation)
{
var chat = new OpenAIChatRequest() { Model = Model };
- chat.AddMessage("system", prompt);
+ chat.AddMessage("user", prompt);
chat.AddMessage("user", question);
var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) };
if (!string.IsNullOrEmpty(ApiKey))
- client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
+ {
+ if (Server.Contains("openai.azure.com/", StringComparison.Ordinal))
+ client.DefaultRequestHeaders.Add("api-key", ApiKey);
+ else
+ client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
+ }
var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest), Encoding.UTF8, "application/json");
try
@@ -164,12 +169,15 @@ namespace SourceGit.Models
task.Wait(cancellation);
var rsp = task.Result;
- if (!rsp.IsSuccessStatusCode)
- throw new Exception($"AI service returns error code {rsp.StatusCode}");
-
var reader = rsp.Content.ReadAsStringAsync(cancellation);
reader.Wait(cancellation);
+ var body = reader.Result;
+ if (!rsp.IsSuccessStatusCode)
+ {
+ throw new Exception($"AI service returns error code {rsp.StatusCode}. Body: {body ?? string.Empty}");
+ }
+
return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
}
catch
diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs
index dcf30ddc..3c452460 100644
--- a/src/Models/Remote.cs
+++ b/src/Models/Remote.cs
@@ -1,11 +1,12 @@
using System;
+using System.IO;
using System.Text.RegularExpressions;
namespace SourceGit.Models
{
public partial class Remote
{
- [GeneratedRegex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/~%]+/[\w\-\.%]+(\.git)?$")]
+ [GeneratedRegex(@"^https?://([-a-zA-Z0-9:%._\+~#=]+@)?[-a-zA-Z0-9:%._\+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6})?(:[0-9]{1,5})?\b(/[-a-zA-Z0-9()@:%_\+.~#?&=]+)+(\.git)?$")]
private static partial Regex REG_HTTPS();
[GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/~%]+/[\w\-\.%]+(\.git)?$")]
private static partial Regex REG_SSH1();
@@ -49,7 +50,7 @@ namespace SourceGit.Models
return true;
}
- return false;
+ return url.EndsWith(".git", StringComparison.Ordinal) && Directory.Exists(url);
}
public bool TryGetVisitURL(out string url)
diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs
index 77c58ee7..5b3aa331 100644
--- a/src/Models/RepositorySettings.cs
+++ b/src/Models/RepositorySettings.cs
@@ -1,4 +1,8 @@
-using Avalonia.Collections;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using Avalonia.Collections;
namespace SourceGit.Models
{
@@ -76,11 +80,11 @@ namespace SourceGit.Models
set;
} = true;
- public AvaloniaList Filters
+ public AvaloniaList HistoriesFilters
{
get;
set;
- } = new AvaloniaList();
+ } = new AvaloniaList();
public AvaloniaList CommitTemplates
{
@@ -148,6 +152,214 @@ namespace SourceGit.Models
set;
} = "---";
+ public Dictionary CollectHistoriesFilters()
+ {
+ var map = new Dictionary();
+ foreach (var filter in HistoriesFilters)
+ map.Add(filter.Pattern, filter.Mode);
+ return map;
+ }
+
+ public bool UpdateHistoriesFilter(string pattern, FilterType type, FilterMode mode)
+ {
+ // Clear all filters when there's a filter that has different mode.
+ if (mode != FilterMode.None)
+ {
+ var clear = false;
+ foreach (var filter in HistoriesFilters)
+ {
+ if (filter.Mode != mode)
+ {
+ clear = true;
+ break;
+ }
+ }
+
+ if (clear)
+ {
+ HistoriesFilters.Clear();
+ HistoriesFilters.Add(new Filter(pattern, type, mode));
+ return true;
+ }
+ }
+ else
+ {
+ for (int i = 0; i < HistoriesFilters.Count; i++)
+ {
+ var filter = HistoriesFilters[i];
+ if (filter.Type == type && filter.Pattern.Equals(pattern, StringComparison.Ordinal))
+ {
+ HistoriesFilters.RemoveAt(i);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ for (int i = 0; i < HistoriesFilters.Count; i++)
+ {
+ var filter = HistoriesFilters[i];
+ if (filter.Type != type)
+ continue;
+
+ if (filter.Pattern.Equals(pattern, StringComparison.Ordinal))
+ return false;
+ }
+
+ HistoriesFilters.Add(new Filter(pattern, type, mode));
+ return true;
+ }
+
+ public void RemoveChildrenBranchFilters(string pattern)
+ {
+ var dirty = new List();
+ var prefix = $"{pattern}/";
+
+ foreach (var filter in HistoriesFilters)
+ {
+ if (filter.Type == FilterType.Tag)
+ continue;
+
+ if (filter.Pattern.StartsWith(prefix, StringComparison.Ordinal))
+ dirty.Add(filter);
+ }
+
+ foreach (var filter in dirty)
+ HistoriesFilters.Remove(filter);
+ }
+
+ public string BuildHistoriesFilter()
+ {
+ var excludedBranches = new List();
+ var excludedRemotes = new List();
+ var excludedTags = new List();
+ var includedBranches = new List();
+ var includedRemotes = new List();
+ var includedTags = new List();
+ foreach (var filter in HistoriesFilters)
+ {
+ if (filter.Type == FilterType.LocalBranch)
+ {
+ var name = filter.Pattern.Substring(11);
+ var b = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]";
+
+ if (filter.Mode == FilterMode.Included)
+ includedBranches.Add(b);
+ else if (filter.Mode == FilterMode.Excluded)
+ excludedBranches.Add(b);
+ }
+ else if (filter.Type == FilterType.LocalBranchFolder)
+ {
+ if (filter.Mode == FilterMode.Included)
+ includedBranches.Add($"{filter.Pattern.Substring(11)}/*");
+ else if (filter.Mode == FilterMode.Excluded)
+ excludedBranches.Add($"{filter.Pattern.Substring(11)}/*");
+ }
+ else if (filter.Type == FilterType.RemoteBranch)
+ {
+ var name = filter.Pattern.Substring(13);
+ var r = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]";
+
+ if (filter.Mode == FilterMode.Included)
+ includedRemotes.Add(r);
+ else if (filter.Mode == FilterMode.Excluded)
+ excludedRemotes.Add(r);
+ }
+ else if (filter.Type == FilterType.RemoteBranchFolder)
+ {
+ if (filter.Mode == FilterMode.Included)
+ includedRemotes.Add($"{filter.Pattern.Substring(13)}/*");
+ else if (filter.Mode == FilterMode.Excluded)
+ excludedRemotes.Add($"{filter.Pattern.Substring(13)}/*");
+ }
+ else if (filter.Type == FilterType.Tag)
+ {
+ var name = filter.Pattern;
+ var t = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]";
+
+ if (filter.Mode == FilterMode.Included)
+ includedTags.Add(t);
+ else if (filter.Mode == FilterMode.Excluded)
+ excludedTags.Add(t);
+ }
+ }
+
+ bool hasIncluded = includedBranches.Count > 0 || includedRemotes.Count > 0 || includedTags.Count > 0;
+ bool hasExcluded = excludedBranches.Count > 0 || excludedRemotes.Count > 0 || excludedTags.Count > 0;
+
+ var builder = new StringBuilder();
+ if (hasIncluded)
+ {
+ foreach (var b in includedBranches)
+ {
+ builder.Append("--branches=");
+ builder.Append(b);
+ builder.Append(' ');
+ }
+
+ foreach (var r in includedRemotes)
+ {
+ builder.Append("--remotes=");
+ builder.Append(r);
+ builder.Append(' ');
+ }
+
+ foreach (var t in includedTags)
+ {
+ builder.Append("--tags=");
+ builder.Append(t);
+ builder.Append(' ');
+ }
+ }
+ else if (hasExcluded)
+ {
+ if (excludedBranches.Count > 0)
+ {
+ foreach (var b in excludedBranches)
+ {
+ builder.Append("--exclude=");
+ builder.Append(b);
+ builder.Append(" --decorate-refs-exclude=refs/heads/");
+ builder.Append(b);
+ builder.Append(' ');
+ }
+ }
+
+ builder.Append("--exclude=HEA[D] --branches ");
+
+ if (excludedRemotes.Count > 0)
+ {
+ foreach (var r in excludedRemotes)
+ {
+ builder.Append("--exclude=");
+ builder.Append(r);
+ builder.Append(" --decorate-refs-exclude=refs/remotes/");
+ builder.Append(r);
+ builder.Append(' ');
+ }
+ }
+
+ builder.Append("--exclude=origin/HEA[D] --remotes ");
+
+ if (excludedTags.Count > 0)
+ {
+ foreach (var t in excludedTags)
+ {
+ builder.Append("--exclude=");
+ builder.Append(t);
+ builder.Append(" --decorate-refs-exclude=refs/tags/");
+ builder.Append(t);
+ builder.Append(' ');
+ }
+ }
+
+ builder.Append("--tags ");
+ }
+
+ return builder.ToString();
+ }
+
public void PushCommitMessage(string message)
{
var existIdx = CommitMessages.IndexOf(message);
diff --git a/src/Models/ResetMode.cs b/src/Models/ResetMode.cs
index 735533c2..827ccaa9 100644
--- a/src/Models/ResetMode.cs
+++ b/src/Models/ResetMode.cs
@@ -6,23 +6,25 @@ namespace SourceGit.Models
{
public static readonly ResetMode[] Supported =
[
- 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),
+ new ResetMode("Soft", "Keep all changes. Stage differences", "--soft", "S", Brushes.Green),
+ new ResetMode("Mixed", "Keep all changes. Unstage differences", "--mixed", "M", Brushes.Orange),
+ new ResetMode("Merge", "Reset while keeping unmerged changes", "--merge", "G", Brushes.Purple),
+ new ResetMode("Keep", "Reset while keeping local modifications", "--keep", "K", Brushes.Purple),
+ new ResetMode("Hard", "Discard all changes", "--hard", "H", Brushes.Red),
];
public string Name { get; set; }
public string Desc { get; set; }
public string Arg { get; set; }
+ public string Key { get; set; }
public IBrush Color { get; set; }
- public ResetMode(string n, string d, string a, IBrush b)
+ public ResetMode(string n, string d, string a, string k, IBrush b)
{
Name = n;
Desc = d;
Arg = a;
+ Key = k;
Color = b;
}
}
diff --git a/src/Models/Statistics.cs b/src/Models/Statistics.cs
index b669eb55..969d3945 100644
--- a/src/Models/Statistics.cs
+++ b/src/Models/Statistics.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using LiveChartsCore;
using LiveChartsCore.Defaults;
@@ -138,7 +139,8 @@ namespace SourceGit.Models
public Statistics()
{
_today = DateTime.Now.ToLocalTime().Date;
- _thisWeekStart = _today.AddSeconds(-(int)_today.DayOfWeek * 3600 * 24);
+ var weekOffset = (7 + (int)_today.DayOfWeek - (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % 7;
+ _thisWeekStart = _today.AddDays(-weekOffset);
_thisMonthStart = _today.AddDays(1 - _today.Day);
All = new StatisticsReport(StaticsticsMode.All, DateTime.MinValue);
diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs
index 2ec9e093..2e8f2c8e 100644
--- a/src/Models/Tag.cs
+++ b/src/Models/Tag.cs
@@ -1,10 +1,19 @@
-namespace SourceGit.Models
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace SourceGit.Models
{
- public class Tag
+ public class Tag : ObservableObject
{
- public string Name { get; set; }
- public string SHA { get; set; }
- public string Message { get; set; }
- public bool IsFiltered { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string SHA { get; set; } = string.Empty;
+ public string Message { get; set; } = string.Empty;
+
+ public FilterMode FilterMode
+ {
+ get => _filterMode;
+ set => SetProperty(ref _filterMode, value);
+ }
+
+ private FilterMode _filterMode = FilterMode.None;
}
}
diff --git a/src/Models/TemplateEngine.cs b/src/Models/TemplateEngine.cs
new file mode 100644
index 00000000..6b5f525d
--- /dev/null
+++ b/src/Models/TemplateEngine.cs
@@ -0,0 +1,410 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace SourceGit.Models
+{
+ public class TemplateEngine
+ {
+ private class Context(Branch branch, IReadOnlyList changes)
+ {
+ public Branch branch = branch;
+ public IReadOnlyList changes = changes;
+ }
+
+ private class Text(string text)
+ {
+ public string text = text;
+ }
+
+ private class Variable(string name)
+ {
+ public string name = name;
+ }
+
+ private class SlicedVariable(string name, int count)
+ {
+ public string name = name;
+ public int count = count;
+ }
+
+ private class RegexVariable(string name, Regex regex, string replacement)
+ {
+ public string name = name;
+ public Regex regex = regex;
+ public string replacement = replacement;
+ }
+
+ private const char ESCAPE = '\\';
+ private const char VARIABLE_ANCHOR = '$';
+ private const char VARIABLE_START = '{';
+ private const char VARIABLE_END = '}';
+ private const char VARIABLE_SLICE = ':';
+ private const char VARIABLE_REGEX = '/';
+ private const char NEWLINE = '\n';
+ private const RegexOptions REGEX_OPTIONS = RegexOptions.Singleline | RegexOptions.IgnoreCase;
+
+ public string Eval(string text, Branch branch, IReadOnlyList changes)
+ {
+ Reset();
+
+ _chars = text.ToCharArray();
+ Parse();
+
+ var context = new Context(branch, changes);
+ var sb = new StringBuilder();
+ sb.EnsureCapacity(text.Length);
+ foreach (var token in _tokens)
+ {
+ switch (token)
+ {
+ case Text text_token:
+ sb.Append(text_token.text);
+ break;
+ case Variable var_token:
+ sb.Append(EvalVariable(context, var_token));
+ break;
+ case SlicedVariable sliced_var:
+ sb.Append(EvalVariable(context, sliced_var));
+ break;
+ case RegexVariable regex_var:
+ sb.Append(EvalVariable(context, regex_var));
+ break;
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ private void Reset()
+ {
+ _pos = 0;
+ _chars = [];
+ _tokens.Clear();
+ }
+
+ private char? Next()
+ {
+ var c = Peek();
+ if (c is not null)
+ {
+ _pos++;
+ }
+ return c;
+ }
+
+ private char? Peek()
+ {
+ return (_pos >= _chars.Length) ? null : _chars[_pos];
+ }
+
+ private int? Integer()
+ {
+ var start = _pos;
+ while (Peek() is char c && c >= '0' && c <= '9')
+ {
+ _pos++;
+ }
+ if (start >= _pos)
+ return null;
+
+ var chars = new ReadOnlySpan(_chars, start, _pos - start);
+ return int.Parse(chars);
+ }
+
+ private void Parse()
+ {
+ // text token start
+ var tok = _pos;
+ bool esc = false;
+ while (Next() is char c)
+ {
+ if (esc)
+ {
+ esc = false;
+ continue;
+ }
+ switch (c)
+ {
+ case ESCAPE:
+ // allow to escape only \ and $
+ if (Peek() is char nc && (nc == ESCAPE || nc == VARIABLE_ANCHOR))
+ {
+ esc = true;
+ FlushText(tok, _pos - 1);
+ tok = _pos;
+ }
+ break;
+ case VARIABLE_ANCHOR:
+ // backup the position
+ var bak = _pos;
+ var variable = TryParseVariable();
+ if (variable is null)
+ {
+ // no variable found, rollback
+ _pos = bak;
+ }
+ else
+ {
+ // variable found, flush a text token
+ FlushText(tok, bak - 1);
+ _tokens.Add(variable);
+ tok = _pos;
+ }
+ break;
+ }
+ }
+ // flush text token
+ FlushText(tok, _pos);
+ }
+
+ private void FlushText(int start, int end)
+ {
+ int len = end - start;
+ if (len <= 0)
+ return;
+ var text = new string(_chars, start, len);
+ _tokens.Add(new Text(text));
+ }
+
+ private object TryParseVariable()
+ {
+ if (Next() != VARIABLE_START)
+ return null;
+ int name_start = _pos;
+ while (Next() is char c)
+ {
+ // name character, continue advancing
+ if (IsNameChar(c))
+ continue;
+
+ var name_end = _pos - 1;
+ // not a name character but name is empty, cancel
+ if (name_start >= name_end)
+ return null;
+ var name = new string(_chars, name_start, name_end - name_start);
+
+ return c switch
+ {
+ // variable
+ VARIABLE_END => new Variable(name),
+ // sliced variable
+ VARIABLE_SLICE => TryParseSlicedVariable(name),
+ // regex variable
+ VARIABLE_REGEX => TryParseRegexVariable(name),
+ _ => null,
+ };
+ }
+
+ return null;
+ }
+
+ private object TryParseSlicedVariable(string name)
+ {
+ int? n = Integer();
+ if (n is null)
+ return null;
+ if (Next() != VARIABLE_END)
+ return null;
+
+ return new SlicedVariable(name, (int)n);
+ }
+
+ private object TryParseRegexVariable(string name)
+ {
+ var regex = ParseRegex();
+ if (regex == null)
+ return null;
+ var replacement = ParseReplacement();
+ if (replacement == null)
+ return null;
+
+ return new RegexVariable(name, regex, replacement);
+ }
+
+ private Regex ParseRegex()
+ {
+ var sb = new StringBuilder();
+ var tok = _pos;
+ var esc = false;
+ while (Next() is char c)
+ {
+ if (esc)
+ {
+ esc = false;
+ continue;
+ }
+ switch (c)
+ {
+ case ESCAPE:
+ // allow to escape only / as \ and { used frequently in regexes
+ if (Peek() == VARIABLE_REGEX)
+ {
+ esc = true;
+ sb.Append(_chars, tok, _pos - 1 - tok);
+ tok = _pos;
+ }
+ break;
+ case VARIABLE_REGEX:
+ // goto is fine
+ goto Loop_exit;
+ case NEWLINE:
+ // no newlines allowed
+ return null;
+ }
+ }
+ Loop_exit:
+ sb.Append(_chars, tok, _pos - 1 - tok);
+
+ try
+ {
+ var pattern = sb.ToString();
+ if (pattern.Length == 0)
+ return null;
+ var regex = new Regex(pattern, REGEX_OPTIONS);
+
+ return regex;
+ }
+ catch (RegexParseException)
+ {
+ return null;
+ }
+ }
+
+ private string ParseReplacement()
+ {
+ var sb = new StringBuilder();
+ var tok = _pos;
+ var esc = false;
+ while (Next() is char c)
+ {
+ if (esc)
+ {
+ esc = false;
+ continue;
+ }
+ switch (c)
+ {
+ case ESCAPE:
+ // allow to escape only }
+ if (Peek() == VARIABLE_END)
+ {
+ esc = true;
+ sb.Append(_chars, tok, _pos - 1 - tok);
+ tok = _pos;
+ }
+ break;
+ case VARIABLE_END:
+ // goto is fine
+ goto Loop_exit;
+ case NEWLINE:
+ // no newlines allowed
+ return null;
+ }
+ }
+ Loop_exit:
+ sb.Append(_chars, tok, _pos - 1 - tok);
+
+ var replacement = sb.ToString();
+
+ return replacement;
+ }
+
+ private static bool IsNameChar(char c)
+ {
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
+ }
+
+ // (?) notice or log if variable is not found
+ private static string EvalVariable(Context context, string name)
+ {
+ if (!s_variables.TryGetValue(name, out var getter))
+ {
+ return string.Empty;
+ }
+ return getter(context);
+ }
+
+ private static string EvalVariable(Context context, Variable variable)
+ {
+ return EvalVariable(context, variable.name);
+ }
+
+ private static string EvalVariable(Context context, SlicedVariable variable)
+ {
+ if (!s_slicedVariables.TryGetValue(variable.name, out var getter))
+ {
+ return string.Empty;
+ }
+ return getter(context, variable.count);
+ }
+
+ private static string EvalVariable(Context context, RegexVariable variable)
+ {
+ var str = EvalVariable(context, variable.name);
+ if (string.IsNullOrEmpty(str))
+ return str;
+ return variable.regex.Replace(str, variable.replacement);
+ }
+
+ private int _pos = 0;
+ private char[] _chars = [];
+ private readonly List