Compare commits

..

479 commits

Author SHA1 Message Date
leo
190d2eec73
Merge branch 'release/v2025.23' 2025-06-23 09:59:54 +08:00
leo
bfb9d6b6bc
version: Release 2025.23
Signed-off-by: leo <longshuang@msn.cn>
2025-06-23 09:59:45 +08:00
leo
9d2f8b1555
fix: Conventional Commit Helper not working in Interactive Rebase (#1446)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-23 09:56:18 +08:00
github-actions[bot]
f59b34fe25 doc: Update translation status and sort locale files
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
2025-06-22 01:30:06 +00:00
Sina Hinderks
6e4f35c4e1
localization: update German translations (#1445) 2025-06-22 09:29:54 +08:00
github-actions[bot]
6ecdabc212 doc: Update translation status and sort locale files 2025-06-22 01:29:17 +00:00
AquariusStar
8b902bd5c9
localization: update russian translate (#1444) 2025-06-22 09:28:58 +08:00
github-actions[bot]
73eccdb495 doc: Update translation status and sort locale files
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
2025-06-21 04:03:36 +00:00
leo
9bfc315ace
feature: allow to push revision where local branch is ahead of its upstream (#1394) (#1441)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-21 11:58:56 +08:00
leo
64ffbb113f
fix: wrong localization key to compare remote branch with current
Signed-off-by: leo <longshuang@msn.cn>
2025-06-21 10:45:40 +08:00
leo
221e964df0
code_review: PR #1442
Trim both `\` (on Windows) and `/` (on Linux/macOS) characters at the end of path

Signed-off-by: leo <longshuang@msn.cn>
2025-06-21 10:42:19 +08:00
Ihor
20daa584e3
fix: fix working tree folder path detection error (#1442)
Fix the error of adding an extra slash when selecting a working tree directory.
2025-06-21 10:25:55 +08:00
leo
c5ad4b837d
feature: auto-follow HEAD when bisecting (#1438)
Some checks failed
Continuous Integration / Build (push) Has been cancelled
Continuous Integration / Prepare version string (push) Has been cancelled
Continuous Integration / Package (push) Has been cancelled
Signed-off-by: leo <longshuang@msn.cn>
2025-06-20 09:19:21 +08:00
leo
957c52aac4
fix: crash after remove worktree while it is opened in sourcegit (#1436)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-19 17:58:02 +08:00
leo
8d74586970
ux: show only subject in Apply Stash and Drop Stash popup
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-19 14:26:31 +08:00
leo
dcd8effc32
fix: saving revision file may crash this app if target dir is not exists (#1434)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-19 11:39:53 +08:00
leo
af2b644792
code_review: PR #1416
- Split `DoubleTapped` into two methods: `CheckoutBranchByDecorator` and `CheckoutBranchByCommit`
- Move `DoubleTappedEvent` from whole ListBox to the row tapped actually
- Do nothing if the decorator double-clicked is HEAD
- Code-style

Signed-off-by: leo <longshuang@msn.cn>
2025-06-19 11:31:04 +08:00
Nathan Baulch
88fd8f32f1
feature: double tap specific branch (#1416)
* feature: double tap specific branch
* exactly match behavior of left sidebar
2025-06-19 10:27:31 +08:00
leo
cadcf40d74
feature: support to open selected folder in file manager
Signed-off-by: leo <longshuang@msn.cn>
2025-06-19 09:32:27 +08:00
github-actions[bot]
fcf1107304 doc: Update translation status and sort locale files
Some checks failed
Localization Check / localization-check (push) Has been cancelled
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
2025-06-19 01:13:09 +00:00
leo
e81674912c
refactor: remove duplicated context menu to ignore untracked files under folder
Signed-off-by: leo <longshuang@msn.cn>
2025-06-19 09:12:27 +08:00
leo
6729d4e896
feature: supports to ignore new files in folder from context menu of selected folder node in change tree (#1432)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 22:10:23 +08:00
leo
f9f44ae9cb
ux: show Name of stash instead of SHA in Apply Stash and Drop Stash popup
Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 20:33:49 +08:00
leo
c67e8e3c64
enhance: create only one filesystem watcher when repo's $GIT_DIR is the same as $REPO_ROOT/.git
Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 17:12:50 +08:00
leo
5ec8ae1296
ux: use CheckCircled instead of Check icon for stash apply context menu
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 14:37:14 +08:00
leo
2d91fed05e
code_style: remove unnecessary namespace using
Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 14:30:46 +08:00
github-actions[bot]
3ac803d88c doc: Update translation status and sort locale files 2025-06-18 05:55:24 +00:00
leo
94d25ee6c9
code_review: PR #1430
- add missing Chinese translations
- add missing icons for stash context menu

Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 13:55:03 +08:00
github-actions[bot]
3711399c59 doc: Update translation status and sort locale files 2025-06-18 05:48:50 +00:00
Göran W
004022648c
Add "Copy Message" to context-menu for Stash item (#1430)
* Refactor: Simplify parsing in QueryStashes, by passing the `-z` argument to `git stash list` for item separation.
* Add "Copy Message" command in stash-item context-menu.
2025-06-18 13:48:39 +08:00
leo
240db2ea2f
ux: use small font size for tips
Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 12:51:39 +08:00
github-actions[bot]
5ca1fcfd8f doc: Update translation status and sort locale files 2025-06-18 04:37:53 +00:00
leo
bad8904edc
code_style: make sure translations are ordered by key
Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 12:37:39 +08:00
github-actions[bot]
86828b9711 doc: Update translation status and sort locale files 2025-06-18 04:33:05 +00:00
leo
6d682ac409
refactor: stash (#1420) (#1426)
- supports to use multi-line as stash message
- new style to display stashes

Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 12:32:50 +08:00
leo
b06a4cbb8a
code_style: remove whitespaces
Signed-off-by: leo <longshuang@msn.cn>
2025-06-18 09:32:39 +08:00
Nathan Baulch
d404f6dbe2
code_style: general cleanup (#1428) 2025-06-18 09:29:18 +08:00
github-actions[bot]
ae46728bbc doc: Update translation status and sort locale files
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
2025-06-17 12:56:18 +00:00
leo
957fbbf54f
refactor: rewrite stash local changes
Signed-off-by: leo <longshuang@msn.cn>
2025-06-17 20:56:02 +08:00
github-actions[bot]
10569022d7 doc: Update translation status and sort locale files
Some checks are pending
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Continuous Integration / Build (push) Waiting to run
Localization Check / localization-check (push) Waiting to run
2025-06-17 07:08:25 +00:00
leo
90310a704d
feature: supports to customize merge message (--edit) (#1421)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-17 15:07:55 +08:00
leo
a8da8c09ac
feature: add a button to open current revision file with default editor in FileHistories
Signed-off-by: leo <longshuang@msn.cn>
2025-06-17 14:44:12 +08:00
leo
df7375313e
enhance: clear last view revision file info after commit changed
Signed-off-by: leo <longshuang@msn.cn>
2025-06-17 14:12:00 +08:00
leo
efa6e46471
feature: add a button to open current revision file with default editor
Signed-off-by: leo <longshuang@msn.cn>
2025-06-17 12:20:02 +08:00
leo
e102e49f45
code_style: remove unnecessary properties
Signed-off-by: leo <longshuang@msn.cn>
2025-06-17 11:29:24 +08:00
leo
dcdc52592c
code_review: PR #1423
Some checks are pending
Continuous Integration / Package (push) Blocked by required conditions
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Localization Check / localization-check (push) Waiting to run
Since we have already used `OnPropertyChanged`, move `ActualThemeVariantProperty` changed handler into it

Signed-off-by: leo <longshuang@msn.cn>
2025-06-16 12:31:37 +08:00
Nathan Baulch
e28b537f89
enhance: darker ChangeStatusIcon when using dark theme (#1423) 2025-06-16 12:19:14 +08:00
leo
ed66d2337b
ux: show stash message as tooltip when hovering it (#1419)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-16 12:09:02 +08:00
leo
0cbf1215e0
Merge branch 'master' into develop 2025-06-16 09:19:19 +08:00
leo
74d77ab704
Merge branch 'release/v2025.22' 2025-06-16 09:18:41 +08:00
leo
b617181fc5
version: Release 2025.22
Signed-off-by: leo <longshuang@msn.cn>
2025-06-16 09:18:31 +08:00
github-actions[bot]
9dedcacb2f doc: Update translation status and sort locale files 2025-06-16 01:15:05 +00:00
Javier J. Martínez M.
b22733b565
localization: update spanish translations (#1422)
add missing translations
2025-06-16 09:14:52 +08:00
Göran W
28844c59cf
perf: optimize the WorkingCopy.IsChanged() method (#1418)
Some checks failed
Continuous Integration / Build (push) Has been cancelled
Continuous Integration / Prepare version string (push) Has been cancelled
Continuous Integration / Package (push) Has been cancelled
* There's no need to populate a Dictionary just to diff the the "old" and "cur" Lists of Changes.
* If the two lists are of equal length and no change has occurred, we can assume that they are also reported in equal sort-order (by git-status).
* Thus, we only need to compare the two items at each successive index.
2025-06-13 19:23:20 +08:00
leo
f88652ffdd
code_review: PR #1417
Remove unnecessary namespace using

Signed-off-by: leo <longshuang@msn.cn>
2025-06-13 17:12:14 +08:00
Göran W
8dffdef48d
refactor: reduce redundant code in NumericSort (#1417)
* Refactor the 2 do-loops into simpler while-loops and replace the use of "tmp1/2" char-arrays with calls to String.Substring().
* Rename the "loc1/2" variables to "subLen1/2", for clarity, since they represent the lengths of the two extracted Substrings.

These changes make the logic more compact and easier to follow.
2025-06-13 17:06:35 +08:00
leo
158d926189
ux: new style for submodule diff
Signed-off-by: leo <longshuang@msn.cn>
2025-06-13 16:19:04 +08:00
leo
99b7208a54
enhance: prevent to start bisect if it is already running
Signed-off-by: leo <longshuang@msn.cn>
2025-06-13 15:32:31 +08:00
leo
05757ebf40
feature: supports to view .tiff images
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-12 18:15:25 +08:00
leo
cb6d6a233f
feature: show change tooltip in INFORMATION page
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-12 11:42:58 +08:00
leo
79650d1851
feature: supports to view .gif file as static image (not animated)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-12 11:32:24 +08:00
leo
7e2f3bec8c
enhance: clear commit message before merging/cherry-picking/rebasing/reverting to allow SourceGit read it from git (#1414)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-12 10:39:52 +08:00
leo
7de5991ecb
code_review: PR #1415
- Column for hotkey in `Reset` popup should use `Auto` for width
- Add `SelectionBoxItemTemplate` for current selected mode in `Reset` popup

Signed-off-by: leo <longshuang@msn.cn>
2025-06-12 09:54:53 +08:00
Nathan Baulch
ffac71b15f
code_style: general cleanup (#1415)
* code_style:  general cleanup

* code_style: whitespace cleanup
2025-06-12 09:35:37 +08:00
leo
35eda489be
feature: show local branch's track status in CheckoutAndFastForward popup
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-11 21:14:06 +08:00
leo
f59851f454
refactor: case-insensitive sorting
- `ToUpper` is not necessary to compare digit char with non-digit char
- use numeric sorting for commit decorators with same type

Signed-off-by: leo <longshuang@msn.cn>
2025-06-11 20:25:41 +08:00
leo
a128b67bd4
code_review: PR #1412
- Use `ViewModels.StashesPage.SelectedStash` instead of `sender is not ListBox { SelectedValue: Models.Stash stash }`
- In tags view, `SelectedItem` can be `Models.Tag` or `ViewModels.TagTreeNode`
- In logs window, `vm.SelectedLog` may be null

Signed-off-by: leo <longshuang@msn.cn>
2025-06-11 16:13:47 +08:00
Nathan Baulch
196b454ae8
feature: support delete key everywhere (#1412) 2025-06-11 15:35:43 +08:00
leo
5494093261
refactor: now all filesystem related trees/lists are sorted in case-insensitive mode
Signed-off-by: leo <longshuang@msn.cn>
2025-06-11 15:20:50 +08:00
leo
c3c7d32167
refactor: do not change original image aspect ratio and do not up-scale image in BLEND image-diff mode
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-11 10:25:24 +08:00
leo
7c1a894525
refactor: do not change original image aspect ratio and do not upscale image in SWIPE mode
Signed-off-by: leo <longshuang@msn.cn>
2025-06-11 10:12:03 +08:00
leo
bcefb773c1
code_style: remove comment
Some checks failed
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
Signed-off-by: leo <longshuang@msn.cn>
2025-06-10 17:19:37 +08:00
leo
aa1c8b1cc1
code_style: use combined expr
Signed-off-by: leo <longshuang@msn.cn>
2025-06-10 17:09:57 +08:00
github-actions[bot]
5e303d43d4 doc: Update translation status and sort locale files 2025-06-10 09:04:35 +00:00
leo
7d0536d94b
feature: when trying to checkout a local branch from its tracking upstream and it is behind the upstream, show Checkout & Fast-Forward popup
Signed-off-by: leo <longshuang@msn.cn>
2025-06-10 17:04:06 +08:00
Nathan Baulch
6c04f5390a
feature: double tap commit with tracked remote branch checks out local tracking branch (#1409) 2025-06-10 15:58:57 +08:00
leo
ee4d8a6a0e
code_review: PR #1408
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-10 11:18:20 +08:00
johanw1232
0ea4021a64
feature: update blame data when clicking/navigating commits (#1408)
* initial work on allowing navigation by clicking on commits in the blame window
* cleanup
2025-06-10 09:30:12 +08:00
leo
11a46dbc93
update: built-in github and unreal icons
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-09 17:35:20 +08:00
leo
69792b3262
refactor: do not upscale images
Signed-off-by: leo <longshuang@msn.cn>
2025-06-09 17:22:31 +08:00
leo
1b1dc2f666
code_style: remove unnecessary code
Signed-off-by: leo <longshuang@msn.cn>
2025-06-09 15:42:02 +08:00
leo
4302d7adb5
Merge branch 'master' into develop
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
2025-06-09 09:31:46 +08:00
leo
932baeec53
Merge branch 'release/v2025.21' 2025-06-09 09:30:45 +08:00
leo
637e133f47
version: Release 2025.21
Signed-off-by: leo <longshuang@msn.cn>
2025-06-09 09:30:36 +08:00
github-actions[bot]
a1e76e9bea doc: Update translation status and sort locale files 2025-06-09 01:27:28 +00:00
AquariusStar
a8541a780e
localization: update translate Russian (#1404) 2025-06-09 09:27:10 +08:00
Sina Hinderks
d55f19586f
fix: issue tracker rule over keyword in subject (#1403)
Some teams use issue tracker numbers in front of the commit message
subject, followed by a colon.  It was not possible to use an issue
tracker rule in such cases, since the issue tracker number would be
interpreted as a keyword due to the colon and therefore displayed in
bold face instead of as a link into the issue tracker.
2025-06-09 09:26:27 +08:00
leo
a22c39519f
code_style: remove unnecessary code
Some checks failed
Continuous Integration / Build (push) Has been cancelled
Continuous Integration / Prepare version string (push) Has been cancelled
Continuous Integration / Package (push) Has been cancelled
Signed-off-by: leo <longshuang@msn.cn>
2025-06-08 11:54:54 +08:00
leo
84fb39f97a
code_review: PR #1402
- it's unnecessary to implement `IEnumerable` interface
- we should check `IsIntersecting` before creating `InlineElement` to avoid unnecessary works suck as running `git cat-file -t <hash>`
- sort whold list after all elements have been added to avoid unnecessary memmove in `Insert`

Signed-off-by: leo <longshuang@msn.cn>
2025-06-08 11:09:20 +08:00
Sina Hinderks
fe54d30b70
refactor: collecting inlines for subjects (#1402)
Instead of checking intersections of inline elements yourself before adding an inline element, the new class `InlineElementCollector` prevents intersections internally.

Additionally the inline elements are sorted by the new class, so it's no longer necessary to do this after adding the inline elements.
2025-06-08 08:47:03 +08:00
leo
ba4c0f0cd2
enhance: scroll to home when active revision file changed
Some checks are pending
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Continuous Integration / Build (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-06-07 20:53:34 +08:00
leo
2478d2953b
code_style: remove unnecessary code in DiffContext
Signed-off-by: leo <longshuang@msn.cn>
2025-06-07 20:42:45 +08:00
leo
74f52fb266
enhance: only show syntax-highlighting toggle if current revision content is a text file
Signed-off-by: leo <longshuang@msn.cn>
2025-06-07 20:27:52 +08:00
leo
f830b68f6a
ux: change foreground for some labels
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-07 12:20:09 +08:00
leo
d323a2064e
feature: supports RGBA16 pixel format
Signed-off-by: leo <longshuang@msn.cn>
2025-06-07 12:00:16 +08:00
leo
203c50350e
fix: wrong pfim image format
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-06 20:50:37 +08:00
leo
47012e29dc
fix: file extensions are case-insensitive
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
Signed-off-by: leo <longshuang@msn.cn>
2025-06-06 18:47:36 +08:00
leo
8db033be99
code_review: PR #1392
- fix the issue that not all channel takes 8 bits
- if `PixelFormatTranscoder.Transcode` supports the same pixel formats, let it converts pixels automatically

Signed-off-by: leo <longshuang@msn.cn>
2025-06-06 18:23:10 +08:00
Henrik Andersson
a2ca071f08
feature: .dds image support (#1392)
* Added Pfim as 3rdparty lib

* Added support for parsing showing dds and tga images using Pfim

---------

Co-authored-by: Snimax <snimax@live.se>
2025-06-06 16:44:40 +08:00
Nathan Baulch
7bba40d03f
typos: (#1397) 2025-06-06 12:10:55 +08:00
leo
0c22409b7b
ux: new sort by time icon (#1393)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-06 11:37:56 +08:00
leo
f63fe8637b
feature: use different icon for sort mode (#1393)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-06-06 11:22:30 +08:00
github-actions[bot]
08665e45c1 doc: Update translation status and sort locale files 2025-06-06 02:45:35 +00:00
leo
3bb20868fc
refactor: remove unnecessary sort by name (descending) for tags (#1393)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-06 10:45:18 +08:00
leo
ac55bed812
enhance: revision file viewer
- show current file path
- add a toggle button to use global syntax highlighting setting (sometimes TextMateSharp will crash this app)

Signed-off-by: leo <longshuang@msn.cn>
2025-06-06 10:07:58 +08:00
leo
f003f67129
fix: should use file.SHA instead of _commit.SHA to query submodule's commit
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-05 21:54:09 +08:00
Göran W
f04b0c5d97
fix: prevent exception on repo with no commits/branches
Signed-off-by: leo <longshuang@msn.cn>
2025-06-05 21:28:32 +08:00
Göran W
406ace9e79
enhance: activate TabsDropdownItem on Tapped instead of DoubleTapped
Signed-off-by: leo <longshuang@msn.cn>
2025-06-05 21:28:10 +08:00
leo
464fe74580
code_review: commit b969ac161a
- The return code of `AutoRemoveInvalidNode`  is never used
- It's not necessary to sort all nodes after re-scan default clone dir. Because `FindOrAddNodeByRepositoryPath` makes sure added node is ordered
- Add a new parameter `save` to `FindOrAddNodeByRepositoryPath` method, and disable it while scanning. Instead, we will save it after scan finished.

Signed-off-by: leo <longshuang@msn.cn>
2025-06-05 21:27:19 +08:00
Göran W
b969ac161a
enhance: unify sorting of RepositoryNode tree, unconditional sort & save after rescan 2025-06-05 21:19:25 +08:00
Göran W
88c38b4139
enhance: unified all file-path normalization - use char-replace, trim trailing slash 2025-06-05 21:17:18 +08:00
Göran W
54c05ac35a
fix: remove trailing slash in paths, to avoid failing comparisons.
This is needed since DirectoryInfo.Fullname (and .FullPath) will not "normalize" trailing slashes, so direct equality tests are error-prone.
This fixes a bug in ScanRepositories.GetUnmanagedRepositories(), where not all Git repo folders were added.
(Also, corrected a variable name from 'founded' to 'found'.)
2025-06-05 21:15:28 +08:00
Göran W
75c32c1a01
typo: corrected spelling error in App.GetLauncher() method 2025-06-05 21:15:28 +08:00
leo
a023a9259b
refactor: rewrite lfs pointer detection and image loading
Signed-off-by: leo <longshuang@msn.cn>
2025-06-05 21:06:31 +08:00
leo
eebadd67a1
feature: remember the last active tab index in lfs-image diff view
Some checks are pending
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Continuous Integration / Build (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-06-05 09:18:19 +08:00
leo
f716c5ee1e
refactor: use existing QueryFileContent command
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-04 21:30:08 +08:00
leo
ed496a41fb
feature: supports to view image diff when lfs object points to a image
Signed-off-by: leo <longshuang@msn.cn>
2025-06-04 20:53:42 +08:00
leo
06a77502bc
fix: crash when clicking Previous Difference without visual lines (#1385)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Always scroll to top when the state of `Show All Lines` changed

Signed-off-by: leo <longshuang@msn.cn>
2025-06-04 13:13:28 +08:00
leo
c2187edbe9
fix: running git command in UIThread via context menu entry blocks whole app (#1384)
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
Signed-off-by: leo <longshuang@msn.cn>
2025-06-03 23:36:15 +08:00
github-actions[bot]
d85f82e171 doc: Update translation status and sort locale files 2025-06-03 13:38:04 +00:00
leo
f7c10d0b33
feature: supports to load avatar from local image and save it to disk
Signed-off-by: leo <longshuang@msn.cn>
2025-06-03 21:37:47 +08:00
leo
25e272fa55
ux: layout of filter mode toggle buttons
Signed-off-by: leo <longshuang@msn.cn>
2025-06-03 20:28:44 +08:00
leo
98041c803e
feature: supports re-order custom actions (#1346)
Signed-off-by: leo <longshuang@msn.cn>
2025-06-03 20:24:30 +08:00
leo
ee2e7d0127
enhance: ignores $ char when measuring contents in NamedHighlightedTextBlock
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-03 14:12:23 +08:00
leo
6517e78ab6
enhance: enable StaysOpenOnClick for filter mode in graph context menu item
Signed-off-by: leo <longshuang@msn.cn>
2025-06-03 14:06:53 +08:00
leo
bf43dd828a
ux: new style for ref's Visibility in Graph context menu item
Signed-off-by: leo <longshuang@msn.cn>
2025-06-03 12:34:49 +08:00
leo
cd009bda6b
ux: enable Use monospace font only in text editor by default
Signed-off-by: leo <longshuang@msn.cn>
2025-06-03 10:15:58 +08:00
leo
cd8ff2e9bf
Merge branch 'master' into develop
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
2025-06-03 09:27:51 +08:00
leo
0594196dee
Merge branch 'release/v2025.20' 2025-06-03 09:27:03 +08:00
leo
425395da29
version: Release 2025.20
Signed-off-by: leo <longshuang@msn.cn>
2025-06-03 09:26:57 +08:00
leo
6e501b1ee4
feature!: now SourceGit requires git >= 2.25.1
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-06-02 22:38:00 +08:00
leo
7b05b011aa
fix: USE THEIRS for AU conflict and USE MINE for UA conflict
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-02 13:03:38 +08:00
leo
f1052c3efc
refactor: use git reset --hard HEAD to discard all changes and use git restore --staged to unstage changes in text diff view
Signed-off-by: leo <longshuang@msn.cn>
2025-06-02 12:50:58 +08:00
leo
78f9ae2fa9
refactor: rewrite git restore integration
Signed-off-by: leo <longshuang@msn.cn>
2025-06-02 12:14:22 +08:00
leo
80df53cf04
ux: move hunk-based operation button away from scrollbar (#1382)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-01 20:04:15 +08:00
leo
57004c4baf
code_style: run dotnet format
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-06-01 11:23:04 +08:00
leo
fa004ce31b
enhance: unstaged renamed file should use git restore --staged <path> <org_path> instead of git restore --staged <path>
Signed-off-by: leo <longshuang@msn.cn>
2025-06-01 11:19:41 +08:00
leo
6620bd193e
ux: remove tooltip for USE THEIRS and USE MINE button
Signed-off-by: leo <longshuang@msn.cn>
2025-06-01 11:09:31 +08:00
leo
26307e2343
refactor: new tooltip for change
Signed-off-by: leo <longshuang@msn.cn>
2025-06-01 10:34:24 +08:00
leo
db5bb0aec9
fix: must convert the relative path to absolute before send it to first instance (#1376)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-05-31 21:26:01 +08:00
leo
dd432c63e8
enhance: when counting commits in Statistics, if the authors have the same e-mail address, the commits are considered to be from the same person (#1380)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-31 18:52:15 +08:00
github-actions[bot]
b94f26a937 doc: Update translation status and sort locale files
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
2025-05-31 04:15:23 +00:00
Javier J. Martínez M.
8e5d5b946e
localization: update spanish translations (#1379)
add missing translations
2025-05-31 12:15:06 +08:00
leo
a9734ea8e9
code_style: remove unused code
Signed-off-by: leo <longshuang@msn.cn>
2025-05-31 11:33:22 +08:00
github-actions[bot]
e22f0f8513 doc: Update translation status and sort locale files
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
2025-05-30 10:20:10 +00:00
leo
8b17f3b1f4
localization: change Compare with HEAD to Compare with <current_branch_name>
Signed-off-by: leo <longshuang@msn.cn>
2025-05-30 18:19:46 +08:00
leo
7934496cff
feature: reset non-active branch to selected commit should not depends on upstream
Signed-off-by: leo <longshuang@msn.cn>
2025-05-30 17:56:06 +08:00
leo
188408fdfc
project: remove duplicated attributes in csproj
Signed-off-by: leo <longshuang@msn.cn>
2025-05-30 17:21:49 +08:00
leo
bc5deac9fe
fix: OpenFolderPickerAsync raise exception when selected a drive root such as E:\
Signed-off-by: leo <longshuang@msn.cn>
2025-05-30 17:12:56 +08:00
leo
1bd2044589
ux: show conflict description in change status icon
Some checks are pending
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Continuous Integration / Build (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-05-30 11:20:43 +08:00
leo
f0c77ffeb8
code_style: run dotnet format
Signed-off-by: leo <longshuang@msn.cn>
2025-05-30 11:16:07 +08:00
leo
60cd210b80
fix: using theirs or mine does not work if it is deleted by ours or theirs
Signed-off-by: leo <longshuang@msn.cn>
2025-05-30 11:13:29 +08:00
leo
75015d550c
ux: show conflict short format in changes view
Signed-off-by: leo <longshuang@msn.cn>
2025-05-30 10:42:02 +08:00
leo
e40ca4bbe0
refactor: use git restore instead of git reset to unstage local changes (#1373)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-30 09:43:45 +08:00
leo
46231a759c
code_style: run dotnet format
Some checks failed
Continuous Integration / Build (push) Has been cancelled
Continuous Integration / Prepare version string (push) Has been cancelled
Localization Check / localization-check (push) Has been cancelled
Continuous Integration / Package (push) Has been cancelled
Signed-off-by: leo <longshuang@msn.cn>
2025-05-28 15:17:05 +08:00
leo
fbc8edcc13
feature: show conflict reason
Signed-off-by: leo <longshuang@msn.cn>
2025-05-28 14:20:56 +08:00
github-actions[bot]
3437f5f4a9 doc: Update translation status and sort locale files 2025-05-28 02:19:23 +00:00
leo
40bf69bff3
ux: move some buttons to page switcher (#1370)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-28 10:18:34 +08:00
leo
2aac6779a5
fix: squash should not change the original author
Signed-off-by: leo <longshuang@msn.cn>
2025-05-28 09:50:14 +08:00
cdammanintopix
9affca1fb2
fix: arguments order for checkout command (#1368)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
2025-05-27 17:43:33 +08:00
Gadfly
729e06d5c0
fix: SaveAsPatch for untracked changes in stash (#1366)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
2025-05-26 22:50:29 +08:00
leo
826619e7c9
fix: new added file in stash changes may not come from untracked file but from staged files (#1358)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-05-26 22:13:10 +08:00
github-actions[bot]
056b90a5ae doc: Update translation status and sort locale files
Some checks failed
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Continuous Integration / Build (push) Waiting to run
Localization Check / localization-check (push) Has been cancelled
2025-05-26 04:28:23 +00:00
leo
0641a22230
feature: allow to reset author when --amend is enabled for committing 2025-05-26 12:28:00 +08:00
leo
d3bc85418e
Merge branch 'master' into develop 2025-05-26 09:56:00 +08:00
leo
4887252870
Merge branch 'release/v2025.19' 2025-05-26 09:45:48 +08:00
leo
860f6f2369
version: Release 2025.19
Signed-off-by: leo <longshuang@msn.cn>
2025-05-26 09:45:43 +08:00
leo
cfc80d41a1
fix: sometimes track status is not correct because the local branch name is ambiguous to git
Signed-off-by: leo <longshuang@msn.cn>
2025-05-26 09:42:46 +08:00
leo
ac26d5bb06
fix: can not squash and fixup until first picked/edit/reword commit in interactive rebase exists list (#1362)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-05-25 13:46:27 +08:00
github-actions[bot]
d7c3bb7150 doc: Update translation status and sort locale files 2025-05-25 05:05:31 +00:00
AquariusStar
39d955b033
localization: update russian translate (#1363) 2025-05-25 13:05:14 +08:00
leo
0e35c56529
enhance: disable squash and fixup for the first commit in interactive rebase list (#1362)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-25 13:03:36 +08:00
leo
22339ab619
enhance: allow to use arrow keys to select changes up/down after stage/unstage previous selected changes by hotkey (#1361)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-05-24 21:29:48 +08:00
leo
ef53dd0025
refactor: use a new Models.ChangeState.Conflicted to represent all the unmerged file state (#1359)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-24 20:25:05 +08:00
Göran W
30d4c1008a
enhance: don't show unmerged files in STAGED area (#1359)
Unmerged files (i.e unresolved conflicts) should only appear in the Unstaged area, and not "duplicated" in the Staged area.

Motivation:
* The user-friendly git-status command does not show these as "Changes to be committed".
* If they appear in the Staged area, they are quite redundant since the Diff view will just show "No changes or only EOL changes".
* Some other Git UIs (like Fork) don't show these as Staged items either.

NOTE: According to docs for the git-status "Short Format" (and --porcelain=v1), the XY fields for Unmerged paths do NOT actually represent the Index & Working-tree states, instead they represent the states introduced by each HEAD in the merge (i.e Ours & Theirs, relative to Base).
2025-05-24 19:42:10 +08:00
leo
ca33107a45
code_review: PR #1360
Signed-off-by: leo <longshuang@msn.cn>
2025-05-24 19:26:17 +08:00
Göran W
4363b8b6aa
refactor: add new constant Models.Commit.EmptyTreeSHA1 (#1360) 2025-05-24 19:23:28 +08:00
Göran W
f3fe90b2e1
fix: IsConflictResolved check should not be done for submodules (#1356)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
A submodule conflict is not resolved until it's Staged.
2025-05-24 09:40:17 +08:00
leo
e28b75b860
enhance: include stdout in error popup when git process (in Exec mode, we do not need to parse its output) exit with non-zero code
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-05-23 16:12:40 +08:00
leo
3377886bab
enhance: filter hint: blocks
Signed-off-by: leo <longshuang@msn.cn>
2025-05-23 14:13:48 +08:00
leo
4807cd5eb2
fix: if font family name contains '#', make sure we have that built-in font
Signed-off-by: leo <longshuang@msn.cn>
2025-05-23 13:32:02 +08:00
github-actions[bot]
d21b790784 doc: Update translation status and sort locale files
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
2025-05-23 04:25:41 +00:00
Javier J. Martínez M.
764ae31239
localization: update spanish translations (#1355)
* localization: update spanish translations

add missing translations

* Update es_ES.axaml

Quickfix for `Text.DeinitSubmodule.Force` translation
2025-05-23 12:25:29 +08:00
github-actions[bot]
38d67d7f17 doc: Update translation status and sort locale files 2025-05-23 03:28:10 +00:00
leo
76a197aae7
feature: supports to overwrite existing branch while creating new branch (#1349)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-23 11:27:45 +08:00
leo
594ffc0d1a
localization: fix typo in en_US for Text.DeinitSubmodule.Force
Signed-off-by: leo <longshuang@msn.cn>
2025-05-23 10:38:44 +08:00
leo
fb1f5638ce
code_style: simpfy context menu creation for blame editor
Signed-off-by: leo <longshuang@msn.cn>
2025-05-23 10:31:11 +08:00
leo
492da8dd57
fix: blame highlight background did not update when using scrollbar (#1354)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-23 10:27:48 +08:00
leo
0ae39faad1
enhance: do not show hint: messages in error popup, but leave it in git command logs (#1348)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-23 10:05:09 +08:00
leo
c112549b69
refactor: query branch head after operation finished to avoid branch head mismatch
Signed-off-by: leo <longshuang@msn.cn>
2025-05-23 09:40:15 +08:00
Göran W
9fb8af51ff
code_style: move hardcoded brush/strings (for outlier Conflict-icon) into named constants (#1350)
This makes code more consistent and gives better overview of all the icons.
(Potentially, this special/outlier icon could be moved into the existing arrays as an extra ChangeState enum-value.)
2025-05-23 09:18:05 +08:00
leo
1ee7d1184e
enhance: only refresh submodules when it is needed (#1344)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-05-22 11:02:54 +08:00
Göran W
44c83ef342
enhance: added more FileSystemWatcher patterns, to improve handling (#1345)
Skip files frequently updated by Git fsmonitor--daemon and Visual Studio, to ease debugging and for early exit.
Check for HEAD and ORIG_HEAD under .git/modules/<submodule>/, to improve handling of submodules.
Check for MERGE_HEAD and AUTO_MERGE under .git/, to improve handling of submodules.
2025-05-22 09:11:28 +08:00
leo
c3ac59ee1a
enhance: refresh submodules after .gitmodules file changed
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 20:51:29 +08:00
leo
c73f775aa5
enhance: mark submodule having local changes even if there are only untracked files (#1344)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 20:47:10 +08:00
M-L-Ml
afbd0d7135
fix: typo in name SetupExternalTools (#1343) 2025-05-21 20:35:22 +08:00
github-actions[bot]
bf39673b21 doc: Update translation status and sort locale files 2025-05-21 12:34:51 +00:00
leo
b0c0c46441
feature: supports to de-initialize a submodule (#1272)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 20:34:33 +08:00
leo
1fef7a7baa
perf: only update uninited or outdated submodules
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 17:51:00 +08:00
github-actions[bot]
1872740265 doc: Update translation status and sort locale files 2025-05-21 09:18:49 +00:00
leo
09d0122e26
refactor: rewrite git pull command
If we do not provide pulling remote branch, it will auto fetch all branches first.

Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 17:18:25 +08:00
github-actions[bot]
7728f4ffbf doc: Update translation status and sort locale files 2025-05-21 08:54:46 +00:00
leo
936160ea5c
feature: supports --recurse-submodules on pull (#1342)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 16:54:23 +08:00
leo
d73ae83b01
feature: supports to use relative path in remote URL (#1339)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 16:29:33 +08:00
leo
5e05c008fc
refactor: simplfy the regex to check remote's URL with HTTP/HTTPS/GIT protocol
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 15:09:36 +08:00
leo
598ba6d9f6
refactor: rewrite Remote.IsValidURL (#1339)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 14:48:21 +08:00
leo
3232e6f313
fix: on Windows, the correct file protocol url format is file:///<driver>:/path/to/file_or_dir (#1339)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 14:18:06 +08:00
leo
0a6b1faa65
feature: support git:// protocol (#1339)
Some checks are pending
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Continuous Integration / Build (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 09:36:41 +08:00
leo
71b90a82b6
refactor: remove validation for relative path while adding submodule (#1339)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 09:27:02 +08:00
leo
d304c50e7f
enhance: show custom action output in popup
Some checks are pending
Localization Check / localization-check (push) Waiting to run
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 00:16:19 +08:00
leo
438aa76695
feature: support to use relative path as submodule's url when adding new submodule (#1339)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-21 00:10:10 +08:00
leo
ece51fbd32
fix: remove binding error in debug mode (#1338)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 23:02:45 +08:00
leo
224f7a949a
fix: since can not been used in HotKey property and ⌘+⌥+D has been used to show Dock, change the hotkey to open external merge tool to Ctrl+Shift+D/⌘+⇧+D
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 23:01:21 +08:00
leo
e6fdc778b7
fix: remove binding error in debug mode (#1338)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 21:48:58 +08:00
leo
53c6fc8999
fix: remove binding error in debug mode (#1338)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 21:44:28 +08:00
leo
f0d1d460a9
fix: remove binding error in debug mode (#1338)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 21:35:14 +08:00
leo
3386cb177b
refactor: rewrite git flow init
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 21:26:41 +08:00
leo
4d5be9f280
refactor: rewrite git-flow integration
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 21:08:00 +08:00
leo
6fa454ace8
fix!: same hotkey for opening external diff tool and discard block (#1337)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 17:32:06 +08:00
leo
75b7724d44
refactor: implement IDisposable instead of calling custom Cleanup
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 17:24:00 +08:00
leo
550493b572
enhance: prevent requesting worktree files more than once time
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 16:49:00 +08:00
leo
eb183589f5
enhance: prevent requesting revision files more than once time
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 16:32:57 +08:00
github-actions[bot]
d56c6a5030 doc: Update translation status and sort locale files 2025-05-20 04:24:40 +00:00
leo
f9b6116a76
feature: supports reset branch to selected commit without checkout (#1247) (#1318)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 12:24:07 +08:00
Gadfly
12d2b7721c
fix: Trim and normalize commit message history line endings (#1335)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
2025-05-20 10:56:02 +08:00
leo
119b0fe95c
feature: log output of custom action if Wait for action exit enabled (#1334)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-20 09:17:00 +08:00
github-actions[bot]
1dfb629cef doc: Update translation status and sort locale files
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
2025-05-19 04:22:07 +00:00
leo
0e2bb1b276
feature: show commit changes count (#1306)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-19 12:21:50 +08:00
leo
57ee1f7dbd
fix: wrong hotkey tip for open Preferences window
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-05-19 10:55:25 +08:00
leo
aaf53ac694
enhance: try to cancel switcher first then other popup
Signed-off-by: leo <longshuang@msn.cn>
2025-05-19 10:08:37 +08:00
leo
736991198f
Merge branch 'master' into develop 2025-05-19 09:45:11 +08:00
leo
7dd1389c25
Merge branch 'release/v2025.18' 2025-05-19 09:43:59 +08:00
leo
341ac26576
version: Release 2025.18
Signed-off-by: leo <longshuang@msn.cn>
2025-05-19 09:43:48 +08:00
leo
aff003fd6d
enhance: cleanup unused resources
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
Signed-off-by: leo <longshuang@msn.cn>
2025-05-18 22:00:35 +08:00
qiufengshe
b78f6b0ea8
perf: minimize temporary strings for better performance (#1332) 2025-05-18 20:52:05 +08:00
leo
5e85f6fefe
enhance: auto-select the first page by default
Signed-off-by: leo <longshuang@msn.cn>
2025-05-18 20:47:38 +08:00
github-actions[bot]
52991351af doc: Update translation status and sort locale files 2025-05-18 12:34:17 +00:00
leo
4b849d9d5c
ux: update workspace/page switcher popup layout
Signed-off-by: leo <longshuang@msn.cn>
2025-05-18 20:33:55 +08:00
github-actions[bot]
6b083dcd3e doc: Update translation status and sort locale files 2025-05-18 11:36:39 +00:00
leo
9614b995d8
refactor: workspace/page switcher (#1330)
- add `Switch Tab` popup
- change hotkey to open `Preferences` to `Ctrl+,/⌘+,`
- change hotkey to open `Switch Workspace` to `Ctrl+Shift+P/⌘+⇧+P`
- change hotkey to open `Switch Tab` to `Ctrl+P/⌘+P`

Signed-off-by: leo <longshuang@msn.cn>
2025-05-18 19:36:17 +08:00
github-actions[bot]
36c2e083cc doc: Update translation status and sort locale files
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
2025-05-18 07:02:46 +00:00
AquariusStar
fd35e0817d
localization: update russian translate (#1331) 2025-05-18 15:02:30 +08:00
github-actions[bot]
d429a6426a doc: Update translation status and sort locale files
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
2025-05-17 12:14:32 +00:00
leo
4c1ba717a7
refactor: rewrite workspace switcher (#1267)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-17 20:14:09 +08:00
github-actions[bot]
bd553405c2 doc: Update translation status and sort locale files 2025-05-17 10:37:25 +00:00
leo
f121975a28
code_review: PR #1328
* remove hotkey to open workspace dropdown menu
* call orignal `ViewModels.Launcher.SwitchWorkspace` directly in view
* add missing translation for Chinese

Signed-off-by: leo <longshuang@msn.cn>
2025-05-17 18:37:02 +08:00
github-actions[bot]
ea320d2cdf doc: Update translation status and sort locale files
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Waiting to run
2025-05-17 05:17:22 +00:00
popara
01945f231e
Added workspaces shortcuts (#1328)
- added Alt+Space for opening Workspaces context menu (which can then be navigated normally with arrows)
- added Alt+1 through Alt+9 for switching to corresponding workspace
2025-05-17 13:17:10 +08:00
github-actions[bot]
506dbc218c doc: Update translation status and sort locale files 2025-05-17 05:12:20 +00:00
Javier J. Martínez M.
d3a740fb95
localization: update spanish translations (#1329)
add missing translations
2025-05-17 13:12:01 +08:00
leo
d3d0e7b15c
ux: thinner border for default avatar
Some checks are pending
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Signed-off-by: leo <longshuang@msn.cn>
2025-05-17 08:13:19 +08:00
Gadfly
879b84ac20
enhance: Show the stderr content from QueryLocalChanges (#1327) 2025-05-17 07:58:47 +08:00
github-actions[bot]
7f86ad9f22 doc: Update translation status and sort locale files 2025-05-16 12:08:16 +00:00
Leonardo
0c9cb41e68
localization: new keys translated to italian (#1323) 2025-05-16 20:08:04 +08:00
leo
86f27c5e58
refactor: generate hash based default avatar instead of simple label (#1322)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-16 17:55:02 +08:00
leo
1f0ab2bfec
refactor: simpfy SourceGit.Views.BranchTreeNodeIcon
Signed-off-by: leo <longshuang@msn.cn>
2025-05-16 13:43:41 +08:00
leo
fd935259aa
refactor: build tags view data in viewmodels instead of views
Signed-off-by: leo <longshuang@msn.cn>
2025-05-16 12:22:52 +08:00
github-actions[bot]
f46bbd01cd doc: Update translation status and sort locale files 2025-05-16 03:32:09 +00:00
leo
ed1351b1f7
feature: supports to show submodules as tree or list (#1307)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-16 11:31:53 +08:00
leo
d299469613
ux: show tooltip at right of hovered item in repository's left side bar (#1317)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-16 10:47:38 +08:00
leo
e4490d87dc
code_review: PR #1314
Signed-off-by: leo <longshuang@msn.cn>
2025-05-16 09:45:26 +08:00
Martin Smith
85b223a3d0
Task/UI usability tweaks (#1314)
* Allow About to center in parent
* About closes on Escape
* ConfirmEmptyCommit dialog closes on Escape
* Ignore Dev file
* Missed condition

---------

Co-authored-by: Martin Smith <martin.smith@purplebricks.com>
2025-05-16 09:27:42 +08:00
github-actions[bot]
6c62789c4c doc: Update translation status and sort locale files 2025-05-16 01:20:43 +00:00
AquariusStar
85e08f5eea
localization: update russian localization (#1319) 2025-05-16 09:20:31 +08:00
leo
463d161ac7
refactor: show submodule as tree instead of list (#1307) 2025-05-14 17:55:28 +08:00
github-actions[bot]
5ec51eefb9 doc: Update translation status and sort locale files 2025-05-14 08:02:08 +00:00
leo
bc5c4670de
feature: supports to use Ctrl+D/⌘+D to open in external diff/merge tool (#1312) 2025-05-14 16:01:47 +08:00
leo
d3363429df
ux: new style for submodule tooltip (#1307) 2025-05-14 15:49:42 +08:00
github-actions[bot]
f83b6c24ae doc: Update translation status and sort locale files 2025-05-14 06:27:00 +00:00
leo
61bb0f7dc7
feature: show submodule's URL in tooltip (#1307) 2025-05-14 14:26:33 +08:00
leo
20a239621b
fix: can not open submodule that has not been initialized 2025-05-14 11:48:44 +08:00
github-actions[bot]
9e91494a20 doc: Update translation status and sort locale files 2025-05-14 03:36:15 +00:00
leo
d71189c705
feature: tooltip for submodule list item (#1307) 2025-05-14 11:35:34 +08:00
leo
55232aeddd
project: ignore custom script files
Signed-off-by: leo <longshuang@msn.cn>
2025-05-13 22:55:24 +08:00
leo
6bf930a9e0
feature: show tags count in tags tree (#1306)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-13 22:50:10 +08:00
Göran W
5b72b15cf2
feature: show remote's URL in tooltip for relevant BranchTreeNodes (#1310) 2025-05-13 22:36:10 +08:00
leo
0e61a0196b
fix: right caption buttons should not visible on macOS (#1311)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-13 22:34:53 +08:00
leo
7bb4e355bd
feature: show branches count in branch tree (#1306) 2025-05-13 19:28:52 +08:00
leo
57d15dc6d3
fix: git submodule status may return lines that start as U character (#1307) 2025-05-13 18:11:51 +08:00
leo
65808e5f58
fix: filter with local branch should not include invalid upstream (gone) (#1308) 2025-05-13 17:59:51 +08:00
leo
cf7b61dd71
refactor: new way to display item count 2025-05-13 17:50:47 +08:00
leo
ac1bd7ca85
ux: hide tag message if it's the same with its name (#1305) 2025-05-13 14:22:41 +08:00
leo
142ee5a327
ux: use localized text instead of hard-coded string annotated (#1305) 2025-05-13 13:01:15 +08:00
leo
afc8a772dd
ux: new style for tag's tooltip (#1305) 2025-05-13 12:26:33 +08:00
leo
8a45e25106
refactor: rewrite custom WM_NCHITTEST implementation on Windows 2025-05-13 10:19:51 +08:00
leo
4e41a6207a
enhance: display tag's name instead of nothing while showing tooltip of tag without message 2025-05-13 10:01:41 +08:00
Göran W
a5c25cf9fe
enhance: add border around tag name, makes tooltip work as for branches
(cherry picked from commit 6a5e6d12d70f52e5777cc4edc4022fed870151d4)
2025-05-13 09:38:09 +08:00
leo
11a9d7fdd8
enhance: force using --no-sign to ignore tag.gpgsign configuration while creating lightweight tag
Signed-off-by: leo <longshuang@msn.cn>
2025-05-13 09:24:00 +08:00
leo
ef4b639f8e
code_style: move platform dependent code to initialize window to namespace SourceGit.Native
Signed-off-by: leo <longshuang@msn.cn>
2025-05-12 21:52:50 +08:00
leo
c62b4a031f
perf: return HTCLIENT directly when window is fullscreen or maximized
Signed-off-by: leo <longshuang@msn.cn>
2025-05-12 18:09:25 +08:00
leo
af9cf6ba6a
ux: force using 4 * RenderScaling as resize border size on Windows
Signed-off-by: leo <longshuang@msn.cn>
2025-05-12 17:57:49 +08:00
leo
641098ffb2
ux: better content padding for maximized window on Windows
Signed-off-by: leo <longshuang@msn.cn>
2025-05-12 16:27:54 +08:00
leo
fcad8eeadc
Merge branch 'master' into develop 2025-05-12 09:24:48 +08:00
leo
01625ada1a
Merge branch 'release/v2025.17' 2025-05-12 09:23:46 +08:00
leo
88dc12275a
version: Release 2025.17
Signed-off-by: leo <longshuang@msn.cn>
2025-05-12 09:22:28 +08:00
Bailey Allen
bac21c5714
enhance: added support for kitty terminal on macOS and Linux. (#1300) 2025-05-12 09:17:20 +08:00
github-actions[bot]
19a51f227b doc: Update translation status and sort locale files 2025-05-12 01:13:27 +00:00
Javier J. Martínez M.
b6d618a6d7
localization: update spanish translations (#1302)
* localization: update spanish translations

add missing translations

* localization: update spanish translations

add missing translations
2025-05-12 09:13:13 +08:00
github-actions[bot]
2573553e01 doc: Update translation status and sort locale files 2025-05-12 01:12:59 +00:00
AquariusStar
9dd0beb61f
localization: update russian translate (#1301) 2025-05-12 09:12:49 +08:00
leo
029fd6933f
refactor: new way to discard selected or all local changes
This modification aims to solve the problem that the deleted submodule cannot be discarded.

Signed-off-by: leo <longshuang@msn.cn>
2025-05-09 22:57:46 +08:00
leo
0f6c8976af
refactor: rewrite checkout/create branch with submodules
Signed-off-by: leo <longshuang@msn.cn>
2025-05-09 18:12:30 +08:00
leo
e446e97f28
fix: remove testing code for git checkout command
Signed-off-by: leo <longshuang@msn.cn>
2025-05-09 17:22:17 +08:00
leo
3e530de9cc
enhance: update submodules individually (#1272)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-09 17:12:12 +08:00
leo
6cf1b20ea6
refactor: context menu for commit change and revision file
Signed-off-by: leo <longshuang@msn.cn>
2025-05-09 14:11:15 +08:00
github-actions[bot]
321ccf9622 doc: Update translation status and sort locale files 2025-05-09 02:47:52 +00:00
leo
ebe0e61367
feature: support to enable --squash and --push option while finishing git-flow branches (#1290)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-09 10:47:36 +08:00
leo
e8bf58f6c3
code_review: PR #1292
Use syntax `String.AsSpan(int start, int len)` instead of `String.AsSpan().Slice(int start, int len)`

Signed-off-by: leo <longshuang@msn.cn>
2025-05-09 09:30:00 +08:00
qiufengshe
15ee2dac91
perf: minimize temporary strings for better performance (#1292) 2025-05-09 09:19:33 +08:00
github-actions[bot]
5d1601086f doc: Update translation status and sort locale files 2025-05-09 01:16:10 +00:00
AquariusStar
3eaa24a993
localization: update and fix translation russian (#1291) 2025-05-09 09:15:59 +08:00
leo
6986e1ac24
code_style: calculate bounds only when it is needed
Signed-off-by: leo <longshuang@msn.cn>
2025-05-08 13:42:21 +08:00
leo
2c8370fa92
refactor: get graph clip width from grid column definition directly
Signed-off-by: leo <longshuang@msn.cn>
2025-05-08 13:39:27 +08:00
leo
008708f07c
ux: use larger font size for commit ref label
Signed-off-by: leo <longshuang@msn.cn>
2025-05-08 13:13:22 +08:00
leo
832fcd7487
fix: offset of commit graph does not look quite right (#1287)
This is because that when using `VirtualizingStackPanel`, the `Bounds.Height` of `ListBoxItem` may not be the same with its `Height` setted in axaml.

Signed-off-by: leo <longshuang@msn.cn>
2025-05-08 12:22:23 +08:00
leo
6df38ad970
ux: new style for inline code in commit subject
Signed-off-by: leo <longshuang@msn.cn>
2025-05-07 20:23:06 +08:00
github-actions[bot]
0a7b973388 doc: Update translation status and sort locale files 2025-05-07 11:08:51 +00:00
Christopher Göttfert
6b050fa557
localization: updated german translations (#1284) 2025-05-07 19:08:39 +08:00
leo
417ab3ecc2
ux: layout for revision compare targets
Signed-off-by: leo <longshuang@msn.cn>
2025-05-07 09:52:26 +08:00
leo
a413df6f89
code_style: run dotnet format
Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 20:56:45 +08:00
leo
ddf643c081
ux: new style for revision/branch compare targets
Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 20:52:43 +08:00
leo
bbc840a5cb
perf: set/update TimeToSort while creating branch nodes
Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 19:26:06 +08:00
github-actions[bot]
c8e21673e4 doc: Update translation status and sort locale files 2025-05-06 10:24:59 +00:00
leo
e45e37d305
feature: supports sort branches by committer date (#1192)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 18:24:34 +08:00
github-actions[bot]
b7fa04d141 doc: Update translation status and sort locale files 2025-05-06 07:52:23 +00:00
leo
93a5d7baea
feature: supports to visit remote repository in web browser (#1265)
- combine `Open in File Manager`, `Open in Terminal` and `Open with external editor` into one dropdown menu
- add `Visit $REMOTE in Browser`

Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 15:51:57 +08:00
leo
e4e2f7b3a7
ux: use smaller font size for inline code in commit subject
Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 14:49:54 +08:00
leo
eae6d10784
enhance: only log exception in popup task (#1281)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 12:17:48 +08:00
github-actions[bot]
4bc5b90e6b
doc: Update translation status and sort locale files
(cherry picked from commit 15445d02379020144239886bc87380ae38c2018a)
2025-05-06 12:02:19 +08:00
leo
df29edd8f0
feature: make --recurse-submdoules an option while trying to checkout branch with submodules (#1272)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 12:01:58 +08:00
leo
054bbf7e0c
enhance: do not override core.autocrlf configure while reading file diff (#1278)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 09:39:03 +08:00
leo
ce00fa6c88
Merge branch 'master' into develop 2025-05-06 09:23:20 +08:00
leo
a960e14368
Merge branch 'release/v2025.16' 2025-05-06 09:22:58 +08:00
leo
867edd9453
version: Release 2025.16
Signed-off-by: leo <longshuang@msn.cn>
2025-05-06 09:22:49 +08:00
github-actions[bot]
aee4ce6387 doc: Update translation status and sort locale files 2025-05-06 01:17:55 +00:00
Javier J. Martínez M.
d92d279fbe
localization: update spanish translations (#1279)
add missing translations
2025-05-06 09:17:45 +08:00
github-actions[bot]
5e080279ce doc: Update translation status and sort locale files 2025-05-04 09:36:42 +00:00
AquariusStar
704c6f589d
localization: update and fix translation russian (#1276) 2025-05-04 17:36:30 +08:00
broknecho
666275c747
feature: add Meld as an option for external merge tool on Windows (#1275) 2025-05-04 11:24:11 +08:00
leo
c0c52695a3
code_style: remove unused code
Signed-off-by: leo <longshuang@msn.cn>
2025-05-03 21:31:10 +08:00
Alen Šiljak
c529fab869
feature: close repository configuration dialog when user pressed Esc (#1269) 2025-05-03 21:18:24 +08:00
leo
4b2983b330
fix: commit detail panel is overlapping history when resizing (#1273)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-03 21:09:43 +08:00
leo
8c1d397480
fix: inline blocks is not sorted in order (#1274)
Signed-off-by: leo <longshuang@msn.cn>
2025-05-03 20:52:40 +08:00
leo
007acb3fa6
project: remove unused scripts
Signed-off-by: leo <longshuang@msn.cn>
2025-04-30 21:40:01 +08:00
leo
3b0c57be84
feature: supports to re-order workspaces (#1261)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-30 21:39:03 +08:00
github-actions[bot]
61bc42612e doc: Update translation status and sort locale files 2025-04-30 13:06:53 +00:00
leo
fe677d40c1
feature: supports search commits by change content (#1263)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-30 21:05:53 +08:00
leo
9bde797b24
fix: make sure the new pattern is appended as a new line (#1264)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-30 20:41:00 +08:00
leo
7501588c95
enhance: quit application after main window has been closed
Signed-off-by: leo <longshuang@msn.cn>
2025-04-30 15:03:14 +08:00
leo
80aead3a17
feature: add dirty state indicator icon to repository tab (#1227)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-30 11:01:39 +08:00
leo
847a1e727b
refactor: enable --ignore-cr-at-eol in diff by default
Signed-off-by: leo <longshuang@msn.cn>
2025-04-30 09:26:34 +08:00
leo
98dd37a9bc
localization: update tranlation for Text.Diff.IgnoreWhitespace
This is because that in `git diff` command the `--ignore-all-space` option will also ignore line-ending changes (`--ignore-cr-at-eol`)

Signed-off-by: leo <longshuang@msn.cn>
2025-04-29 22:49:20 +08:00
leo
95ea0a6ba6
ux: Ignore Whitespace and EOL Changes should always be visible (#1260)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-29 22:21:38 +08:00
leo
b9dc5a8164
feature: parse url in commit message (#1133)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-29 18:08:35 +08:00
leo
63803c9b88
feature: show command running time in logs window (#1253)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-29 16:36:05 +08:00
leo
825b74c2a3
refactor: use String.AsSpan(int, int) instead of String.AsSpan().Slice(int, int)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-29 09:44:06 +08:00
qiufengshe
48bb8e91de
perf: minimize temporary strings for better performance (#1255) 2025-04-29 09:33:14 +08:00
leo
53a55467f1
enhance: ignore submodule changes when deal with local changes before pull/checkout/create branch (#1256)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-29 09:27:09 +08:00
leo
5681bf489d
fix: empty dialog when generating commit message with AI (#1257)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-29 09:14:24 +08:00
leo
226bc434f5
ux: make log window resizable (#1253)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-28 16:53:51 +08:00
leo
30d42b11e2
enhance: wait a while after branch changed (#1254)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-28 16:19:40 +08:00
leo
2698427cd0
fix: popup running animation did not update after switch back from another page
Signed-off-by: leo <longshuang@msn.cn>
2025-04-28 11:57:36 +08:00
leo
b4f1f35e67
Merge branch 'master' into develop 2025-04-28 09:17:40 +08:00
leo
92f215d039
Merge branch 'release/v2025.15' 2025-04-28 09:16:44 +08:00
leo
2e1cf76c82
version: Release 2025.15
Signed-off-by: leo <longshuang@msn.cn>
2025-04-28 09:16:37 +08:00
leo
951ea8f088
fix: use subject as context menu item header to fix vertical alignment (#1251)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-27 11:20:44 +08:00
Chiahong
21cb87cec5
localization: update zh_TW.axaml (#1249) 2025-04-27 09:38:28 +08:00
github-actions[bot]
f39048df77 doc: Update translation status and sort locale files 2025-04-27 01:38:20 +00:00
AquariusStar
bbdeecdcc6
locallization: update russian translate (#1248) 2025-04-27 09:38:09 +08:00
leo
d2e688908c
ux: use different inline code background for different themes
Signed-off-by: leo <longshuang@msn.cn>
2025-04-25 21:26:21 +08:00
leo
91acf0a32a
enhance: fore invalidate measure after data context of BisectStateIndicator changed
Signed-off-by: leo <longshuang@msn.cn>
2025-04-25 20:55:11 +08:00
leo
d44d2b9770
ux: use a outer border to hold tooltip of commit message in FileHistories view (#1232)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-25 18:50:56 +08:00
cdammanintopix
f09367a614
fix: Append UserName to the SourceGitIPCChannel NamedPipeServerStream to allow multiple users usage on the same server (#1244) (#1246) 2025-04-25 16:56:28 +08:00
leo
00e56ce9d1
fix: System.NullReferenceException raised after popup stop (success or not) running
Signed-off-by: leo <longshuang@msn.cn>
2025-04-25 15:56:37 +08:00
leo
22d4f26bc3
code_style: run dotnet format
Signed-off-by: leo <longshuang@msn.cn>
2025-04-25 14:31:14 +08:00
leo
a94c7f55ce
ux: remove tips in commit list
Signed-off-by: leo <longshuang@msn.cn>
2025-04-25 13:37:11 +08:00
leo
1d16925e74
enhance: stop render next inline elements when it is out of bounds
Signed-off-by: leo <longshuang@msn.cn>
2025-04-25 13:30:00 +08:00
leo
8c4362a98d
feature: subject presenter supports inline codeblock
Signed-off-by: leo <longshuang@msn.cn>
2025-04-25 13:24:13 +08:00
leo
9efbc7dd7a
localization: update translations for Chinese
Signed-off-by: leo <longshuang@msn.cn>
2025-04-24 10:04:57 +08:00
github-actions[bot]
c519381645 doc: Update translation status and sort locale files 2025-04-24 02:00:26 +00:00
leo
6590812634
localization: update translations for Chinese
Signed-off-by: leo <longshuang@msn.cn>
2025-04-24 10:00:07 +08:00
github-actions[bot]
ad6ed1512b doc: Update translation status and sort locale files 2025-04-24 01:22:43 +00:00
Javier J. Martínez M.
f73e0687a1
localization: update spanish translations (#1241)
add missing translations. `Bisect`/`Bisecting` stays the same because they reference command names.
2025-04-24 09:22:32 +08:00
qiufengshe
ea680782fe
perf: minimize temporary strings for better performance (#1240)
(cherry picked from commit f4dad2bf551ead5640a500297a4a6f408aef1350)
2025-04-23 21:15:58 +08:00
leo
7e282b13fa
code_style: run dotnet format
Signed-off-by: leo <longshuang@msn.cn>
2025-04-23 20:59:39 +08:00
leo
1386ca30e3
fix: typo in conventional commit type (#1239)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-23 20:52:41 +08:00
github-actions[bot]
2107676058 doc: Update translation status and sort locale files 2025-04-23 07:34:37 +00:00
leo
f72f1894c3
feature: supports to enable --ignore-cr-at-eol in diff by default
Signed-off-by: leo <longshuang@msn.cn>
2025-04-23 15:34:21 +08:00
github-actions[bot]
586ff39da1 doc: Update translation status and sort locale files 2025-04-23 02:39:41 +00:00
AquariusStar
9bdbf47522
localization: update russian localization (#1233) 2025-04-23 10:39:30 +08:00
leo
17c08d42a0
enhance: ignore all sub-directories those names start with '.' (#1234)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-23 10:38:01 +08:00
leo
fafa2a53ae
enhance: show commit full message tooltip when hover commit subject in FileHistories view (#1232)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-23 10:33:49 +08:00
leo
7890f7abbf
refactor: use PointerPressed event instead of ListBox.SelectionChanged event to navigate to commit (#1230)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-23 10:17:14 +08:00
leo
345ad06aba
refactor: diff for staged file with --amend enabled (#1231)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 19:20:27 +08:00
leo
78f4809875
fix: no changes were displayed when try to amend a commit without parent (branch first commit) (#1231)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 19:04:40 +08:00
leo
87ebe3741d
fix: for init-commit, app will crash with COMMIT & PUSH due to local branch has not been updated (#1229)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 18:45:14 +08:00
leo
f2000b4a84
enhance: show git log without command itself for git bisect
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 17:51:55 +08:00
leo
9a6c671a96
refactor: --ignore-cr-at-eol is not necessary when --ignore-all-space is enabled
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 16:50:46 +08:00
leo
34e0ea3bcb
enhance: raise bisect error manually
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 16:07:23 +08:00
leo
7be37424e1
fix: modal dialog did not take focus after show (#1225)
Co-authored-by: Gadfly <gadfly@gadfly.vip>
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 16:02:27 +08:00
github-actions[bot]
a42df87b9c doc: Update translation status and sort locale files 2025-04-22 07:45:38 +00:00
leo
df5294bcb7
feature: git bisect support
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 15:45:15 +08:00
leo
9eae1eeb81
ux: add InputGesture for hotkeys dialog
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 11:05:39 +08:00
leo
4c3698b171
feature: use F1 to quick open the Keyboard Shortcuts Reference dialog (#1225)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 10:30:19 +08:00
qiufengshe
9f18cbca5b
minimize temporary strings for better performance (#1224)
* minimize temporary strings for better performance

* minimize temporary strings for better performance

(cherry picked from commit c9e6a8d4c2d7b5fe03ee13af0a79c5334c23b1c0)
2025-04-22 10:23:20 +08:00
leo
6882ae069f
enhance: do not show tooltip if the ower window is deactived (#1218)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-22 10:22:02 +08:00
leo
9c53894cb4
ux: add an empty icon if there are no git command logs available
Signed-off-by: leo <longshuang@msn.cn>
2025-04-21 18:48:01 +08:00
leo
06d033464d
code_style: move commit link parser to Models.CommitLink.Get
Signed-off-by: leo <longshuang@msn.cn>
2025-04-21 17:27:07 +08:00
leo
750ca8ec61
refactor: use custom view locator to create new window/dialog (#1216)
Signed-off-by: leo <longshuang@msn.cn>
(cherry picked from commit 3e6f2b25f15b263e2b84922abc5cf6d621d62a83)
2025-04-21 15:32:21 +08:00
leo
86113701f3
Merge branch 'master' into develop 2025-04-21 09:49:51 +08:00
leo
387b68cdfe
Merge branch 'release/v2025.14' 2025-04-21 09:49:02 +08:00
leo
550c108f84
version: Release 2025.14
Signed-off-by: leo <longshuang@msn.cn>
2025-04-21 09:48:52 +08:00
qiufengshe
232482ca92
minimize temporary strings for better performance (#1215)
(cherry picked from commit b4fa80c0939ca198bff8e858a4dc241efd31d558)
2025-04-21 09:44:26 +08:00
heartacker
b4db88a663
chore: update Avalonia package references to version 11.2.8 (#1220) 2025-04-21 09:43:44 +08:00
leo
41416a6bed
refactor: use DataTemplates instead of create NamedHighlightedTextBlock manually for menu item (#1216)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-20 11:05:24 +08:00
leo
5fd074a9b6
refactor: use view locator instead of creating views manually in viewmodels (#1213)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-19 11:14:19 +08:00
leo
413669741d
enhance: ensure sourcegit_rebase_jobs.json only being used when orig-head and onto are both matched
Signed-off-by: leo <longshuang@msn.cn>
2025-04-18 12:49:19 +08:00
leo
75b4a4b294
enhance: record more git command logs
Signed-off-by: leo <longshuang@msn.cn>
2025-04-18 11:29:59 +08:00
leo
d254b557a9
docs: update README.md
Signed-off-by: leo <longshuang@msn.cn>
2025-04-18 10:25:53 +08:00
leo
892f3b8032
code_style: move SourceGit.CommandExtensions to SourceGit.ViewModels.CommandExtensions
Signed-off-by: leo <longshuang@msn.cn>
2025-04-18 10:24:20 +08:00
leo
afe5d4b969
ux: show an icon to draw user's attention to LOCAL CHANGES when there is an in-progress operation (#1210)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-18 09:59:34 +08:00
lwray-renesas
4d31392085
Fixed tooltip
Tooltip for chosing mine was wrong.
Was --theirs when should be --ours.

(cherry picked from commit 26a471933c)
2025-04-18 09:47:31 +08:00
leo
de31d4bad4
ux: layout of git command log item
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 21:17:54 +08:00
github-actions[bot]
5bd7dd428d doc: Update translation status and sort locale files 2025-04-17 12:04:02 +00:00
leo
4c1a04477e
refactor: enhanced copy commit information context menu (#1209)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 20:03:46 +08:00
leo
090b64d68d
refactor: notification popup uses the same text presenter with git command log viewer (#1149)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 18:21:55 +08:00
leo
231010abc6
ux: custom style for command line in git command log
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 17:45:49 +08:00
leo
3358ff9aee
enhance: disable hyper link and email link in git command logs
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 17:24:56 +08:00
leo
104a3f0bbf
code_style: run dotnet format
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 16:35:18 +08:00
github-actions[bot]
a06d1183d7 doc: Update translation status and sort locale files 2025-04-17 08:32:08 +00:00
leo
9f493abd1a
enhance: add context menu for selected log
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 16:31:30 +08:00
github-actions[bot]
349844cf2a doc: Update translation status and sort locale files 2025-04-17 08:07:57 +00:00
leo
021aab8408
enhance: add a button to clear all git command logs
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 16:07:40 +08:00
leo
c1e31ac4e3
ci: try to remove zlib1g-dev:arm64
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 15:48:02 +08:00
github-actions[bot]
2a43efde07 doc: Update translation status and sort locale files 2025-04-17 05:34:40 +00:00
Javier J. Martínez M.
33ae6a9989
localization: update spanish translations (#1206)
add missing translations
modify instances of `stagear` with `hacer stage`
2025-04-17 13:34:27 +08:00
leo
0e967ffc8e
fix: pressing Alt+Enter to commit and push in a repository that has no remotes will crash (#1205)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 13:30:41 +08:00
github-actions[bot]
c231772298 doc: Update translation status and sort locale files 2025-04-17 05:24:26 +00:00
leo
8b39df32cc
feature: git command logs
Signed-off-by: leo <longshuang@msn.cn>
2025-04-17 13:23:56 +08:00
leo
928a0ad3c5
feature: add wip (work in progress) type (#1200)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-16 19:39:24 +08:00
leo
9606f128e4
enhance: remember commit message when exiting (#1166)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-16 16:36:23 +08:00
leo
67255a5529
ux: reduce combobox item height
Signed-off-by: leo <longshuang@msn.cn>
2025-04-16 15:42:44 +08:00
leo
cac4b7edf6
enhance: conventional commit message helper (#1200)
- add `build`, `ci`, `pref`
- re-design helper dialog

Signed-off-by: leo <longshuang@msn.cn>
2025-04-16 15:40:26 +08:00
leo
fa44fa773c
ux: re-design welcome (repositories manager) page (#1202)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-16 11:54:50 +08:00
leo
db46de0261
enhance: append character U+26D4 to line when \ No newline at end of file is found in git diff output (#1197)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-16 11:41:40 +08:00
leo
e9036b5fb9
ux: re-design commit message input box
Signed-off-by: leo <longshuang@msn.cn>
2025-04-16 10:22:54 +08:00
leo
9ba0b595d9
enhance: remember the last state of Ignore Whitespace Change and EOF in text diff view (#1198)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 17:58:26 +08:00
github-actions[bot]
cf763b47c6 doc: Update translation status and sort locale files 2025-04-15 09:47:30 +00:00
leo
539d3f6eca
ux: re-design commit message input box (#1169)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 17:47:12 +08:00
leo
03216fc31e
code_style: run dotnet format and re-order codes
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 16:30:50 +08:00
leo
3cc463d24b
enhance: use Environment.Exit(0) instead of IClassicDesktopStyleApplicationLifetime.Shutdown to stop for non-first instance of SourceGit
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 16:24:48 +08:00
leo
33a463ce59
feature: allow to view contribution chart based on selected author (#1196)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 16:08:38 +08:00
leo
90b37663ed
refactor: use lock file instead of named mutex since the second one may not work on other platforms
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 14:36:12 +08:00
leo
9ebde1943e
project: downgrade AvaloniaUI to 11.2.6 to fix duplicated characters when input to textbox (#1195)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 11:15:13 +08:00
leo
be3f418680
fix: no diff content shows with new files (#1193)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 11:03:34 +08:00
leo
a97f163860
readme: update translation tips
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 10:53:53 +08:00
github-actions[bot]
c1839199ee doc: Update translation status and sort locale files 2025-04-15 02:42:33 +00:00
leo
7d5ffaf867
code_style: keep all translations ordered by key
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 10:42:16 +08:00
github-actions[bot]
f0d4cfc9f9 doc: Update translation status and sort locale files 2025-04-15 02:30:43 +00:00
Oleksii Borovyk
70494485ab
Added ukrainian translation (#1191)
(cherry picked from commit b40bfeb98f35da080a1b3935e801e422858faf14)
2025-04-15 10:30:24 +08:00
leo
c4c04b8b01
enhance: bring window into view after receive IPC message
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 10:19:57 +08:00
leo
e2da44c8fd
enhance: use Mutex to force running SourceGit in singleton mode
Signed-off-by: leo <longshuang@msn.cn>
2025-04-15 09:35:16 +08:00
leo
0acbe3e487
enhance: use PipeOptions.FirstPipeInstance to create NamedPipeServerStream
Signed-off-by: leo <longshuang@msn.cn>
2025-04-14 23:55:42 +08:00
leo
05982e6dc0
style: re-design style for disabled primary button
Signed-off-by: leo <longshuang@msn.cn>
2025-04-14 23:23:40 +08:00
leo
e5dc211c35
refactor: simpfy IPC code
Signed-off-by: leo <longshuang@msn.cn>
2025-04-14 23:17:04 +08:00
Gadfly
1e0fd63543
localization: add translation sorting and formatting (#1186)
* doc: Update translation status and missing keys

* localization: add translation sorting and formatting

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-04-14 22:07:24 +08:00
Gadfly
3b1018e0e2
fix: update visible staged changes retrieval in WorkingCopy (#1187)
* doc: Update translation status and missing keys

* fix: update visible staged changes retrieval in WorkingCopy

* fix: prevent unintended amend behavior when changing current branch

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-04-14 22:05:05 +08:00
leo
7d20f97f4e
code_review: PR #1185
- make `SourceGit` running in singleton mode
- `TrySendArgsToExistingInstance` should not be called before `BuildAvaloniaApp().StartWithClassicDesktopLifetime(args)` since we may want to launch `SourceGit` as a core editor.
- avoid `preference.json` to be saved by multiple instances.
- move IPC code to models.

Signed-off-by: leo <longshuang@msn.cn>
2025-04-14 22:03:51 +08:00
Massimo
09c0edef8e
feat: implement IPC for opening repositories in new tabs (#1185)
* refactor: improve diff handling for EOL changes and enhance text diff display

- Updated `Diff.cs` to streamline whitespace handling in diff arguments.
- Enhanced `DiffContext.cs` to check for EOL changes when old and new hashes differ, creating a text diff if necessary.
- Added support for showing end-of-line symbols in `TextDiffView.axaml.cs` options.

* localization: update translations to include EOF handling in ignore whitespace messages

- Modified the ignore whitespace text in multiple language files to specify that EOF changes are also ignored.
- Ensured consistency across all localization files for the patch application feature.

* revert: Typo in DiffResult comment

* revert: update diff arguments to ignore CR at EOL in whitespace handling (like before changes)

* revert: update translations to remove EOF references in Text.Apply.IgnoreWS and fixed typo in Text.Diff.IgnoreWhitespace (EOF => EOL)

* feat: add workspace-specific default clone directory functionality

- Implemented logic in Clone.cs to set ParentFolder based on the active workspace's DefaultCloneDir if available, falling back to the global GitDefaultCloneDir.
- Added DefaultCloneDir property to Workspace.cs to store the default clone directory for each workspace.
- Updated ConfigureWorkspace.axaml to include a TextBox and Button for setting the DefaultCloneDir in the UI.
- Implemented folder selection functionality in ConfigureWorkspace.axaml.cs to allow users to choose a directory for cloning.
- This closes issue #1145

* feat: implement IPC for opening repositories in new tabs

- Added functionality to send repository paths to an existing instance of the application using named pipes.
- Introduced a new preference option to open repositories in a new tab instead of a new window.
- Updated UI to include a checkbox for the new preference.
- Enhanced the handling of IPC server lifecycle based on the new preference setting.
- This closes issue #1184

---------

Co-authored-by: mpagani <massimo.pagani@unitec-group.com>
2025-04-14 19:16:15 +08:00
github-actions[bot]
558eb7c9ac doc: Update translation status and missing keys 2025-04-14 09:03:23 +00:00
leo
b7aa49403b
code_review: PR #1183
- code style in `Clone` constructor
- re-design workspace configuration dialog

Signed-off-by: leo <longshuang@msn.cn>
2025-04-14 17:03:08 +08:00
Massimo
f14a666091
feat: add workspace-specific default clone directory functionality (#1183)
* refactor: improve diff handling for EOL changes and enhance text diff display

- Updated `Diff.cs` to streamline whitespace handling in diff arguments.
- Enhanced `DiffContext.cs` to check for EOL changes when old and new hashes differ, creating a text diff if necessary.
- Added support for showing end-of-line symbols in `TextDiffView.axaml.cs` options.

* localization: update translations to include EOF handling in ignore whitespace messages

- Modified the ignore whitespace text in multiple language files to specify that EOF changes are also ignored.
- Ensured consistency across all localization files for the patch application feature.

* revert: Typo in DiffResult comment

* revert: update diff arguments to ignore CR at EOL in whitespace handling (like before changes)

* revert: update translations to remove EOF references in Text.Apply.IgnoreWS and fixed typo in Text.Diff.IgnoreWhitespace (EOF => EOL)

* feat: add workspace-specific default clone directory functionality

- Implemented logic in Clone.cs to set ParentFolder based on the active workspace's DefaultCloneDir if available, falling back to the global GitDefaultCloneDir.
- Added DefaultCloneDir property to Workspace.cs to store the default clone directory for each workspace.
- Updated ConfigureWorkspace.axaml to include a TextBox and Button for setting the DefaultCloneDir in the UI.
- Implemented folder selection functionality in ConfigureWorkspace.axaml.cs to allow users to choose a directory for cloning.
- This closes issue #1145

---------

Co-authored-by: mpagani <massimo.pagani@unitec-group.com>
2025-04-14 16:41:34 +08:00
leo
e89dbd8f43
code_review: PR #1177
- use `Command.ReadToEnd` instead of `Command.Exec` to avoid git trims line endings.
- use `StringBuilder.Append('\n')` instead of `StringBuilder.AppendLine()` to restore original line endings (we split the original diff output by `\n` not `\r')
- there's no need to show file content (the `StreamReader.ReadLine()` will trim line endings)

Signed-off-by: leo <longshuang@msn.cn>
2025-04-14 16:06:52 +08:00
Massimo
81820e7034
refactor: improve diff handling for EOL changes and enhance text diff… (#1177)
* refactor: improve diff handling for EOL changes and enhance text diff display

- Updated `Diff.cs` to streamline whitespace handling in diff arguments.
- Enhanced `DiffContext.cs` to check for EOL changes when old and new hashes differ, creating a text diff if necessary.
- Added support for showing end-of-line symbols in `TextDiffView.axaml.cs` options.

* localization: update translations to include EOF handling in ignore whitespace messages

- Modified the ignore whitespace text in multiple language files to specify that EOF changes are also ignored.
- Ensured consistency across all localization files for the patch application feature.

* revert: Typo in DiffResult comment

* revert: update diff arguments to ignore CR at EOL in whitespace handling (like before changes)

* revert: update translations to remove EOF references in Text.Apply.IgnoreWS and fixed typo in Text.Diff.IgnoreWhitespace (EOF => EOL)

---------

Co-authored-by: mpagani <massimo.pagani@unitec-group.com>
2025-04-14 15:18:45 +08:00
leo
e7f0217a7b
refactor: move binding for ToolTip.IsOpen from code to axaml
Signed-off-by: leo <longshuang@msn.cn>
2025-04-14 11:51:40 +08:00
leo
0cb2ca78fe
code_review: PR #1180
- replace `SetNeedNavigateToUpstreamHead` with `NavigateToBranchDelayed`
- navigate to current HEAD instead of original source HEAD after merge

Signed-off-by: leo <longshuang@msn.cn>
2025-04-14 11:35:50 +08:00
Gadfly
17cf402c78
enhance: navigate to upstream head after fetch, pull, and merge (#1180) 2025-04-14 10:42:34 +08:00
leo
245de9b458
fix: tooltip did not hide after pointer move out (#1178)
Signed-off-by: leo <longshuang@msn.cn>
2025-04-14 10:40:24 +08:00
leo
e76328ff38
Merge branch 'master' into develop 2025-04-14 09:56:48 +08:00
367 changed files with 12902 additions and 5404 deletions

View file

@ -71,20 +71,20 @@ dotnet_style_predefined_type_for_member_access = true:suggestion
# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# private static fields should have s_ prefix
dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion
dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
@ -93,7 +93,7 @@ dotnet_naming_style.private_static_prefix_style.capitalization = camel_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field

View file

@ -58,7 +58,7 @@ jobs:
if: ${{ matrix.runtime == 'linux-arm64' }}
run: |
sudo apt-get update
sudo apt-get install -y llvm gcc-aarch64-linux-gnu zlib1g-dev:arm64
sudo apt-get install -y llvm gcc-aarch64-linux-gnu
- name: Build
run: dotnet build -c Release
- name: Publish

View file

@ -4,7 +4,6 @@ on:
branches: [ develop ]
paths:
- 'src/Resources/Locales/**'
- 'README.md'
workflow_dispatch:
workflow_call:
@ -32,8 +31,8 @@ jobs:
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
if [ -n "$(git status --porcelain)" ]; then
git add README.md TRANSLATION.md
git commit -m 'doc: Update translation status and missing keys'
git add TRANSLATION.md src/Resources/Locales/*.axaml
git commit -m 'doc: Update translation status and sort locale files'
git push
else
echo "No changes to commit"

2
.gitignore vendored
View file

@ -37,3 +37,5 @@ build/*.deb
build/*.rpm
build/*.AppImage
SourceGit.app/
build.command
src/Properties/launchSettings.json

View file

@ -11,7 +11,7 @@
* Supports Windows/macOS/Linux
* Opensource/Free
* Fast
* Deutsch/English/Español/Français/Italiano/Português/Русский/简体中文/繁體中文/日本語/தமிழ் (Tamil)
* Deutsch/English/Español/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil)
* Built-in light/dark themes
* Customize theme
* Visual commit graph
@ -35,9 +35,11 @@
* Revision Diffs
* Branch Diff
* Image Diff - Side-By-Side/Swipe/Blend
* Git command logs
* Search commits
* GitFlow
* Git LFS
* Bisect
* Issue Link
* Workspace
* Custom Action
@ -52,9 +54,9 @@ You can find the current translation status in [TRANSLATION.md](https://github.c
## How to Use
**To use this tool, you need to install Git(>=2.23.0) first.**
**To use this tool, you need to install Git(>=2.25.1) first.**
You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [Github Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits.
You can download the latest stable from [Releases](https://github.com/sourcegit-scm/sourcegit/releases/latest) or download workflow artifacts from [GitHub Actions](https://github.com/sourcegit-scm/sourcegit/actions) to try this app based on latest commits.
This software creates a folder `$"{System.Environment.SpecialFolder.ApplicationData}/SourceGit"`, which is platform-dependent, to store user settings, downloaded avatars and crash logs.
@ -91,7 +93,7 @@ For **macOS** users:
brew tap ybeapps/homebrew-sourcegit
brew install --cask --no-quarantine sourcegit
```
* If you want to install `SourceGit.app` from Github Release manually, you need run following command to make sure it works:
* If you want to install `SourceGit.app` from GitHub Release manually, you need run following command to make sure it works:
```shell
sudo xattr -cr /Applications/SourceGit.app
```

View file

@ -6,111 +6,220 @@ This document shows the translation status of each locale file in the repository
### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen)
### ![de__DE](https://img.shields.io/badge/de__DE-97.34%25-yellow)
### ![de__DE](https://img.shields.io/badge/de__DE-%E2%88%9A-brightgreen)
<details>
<summary>Missing keys in de_DE.axaml</summary>
- Text.BranchUpstreamInvalid
- Text.Configure.CustomAction.WaitForExit
- Text.Configure.Git.PreferredMergeMode
- Text.Configure.IssueTracker.AddSampleAzure
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CopyFullPath
- Text.Diff.First
- Text.Diff.Last
- Text.Preferences.AI.Streaming
- Text.Preferences.Appearance.EditorTabWidth
- Text.Preferences.General.ShowTagsInGraph
- Text.StashCM.SaveAsPatch
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
</details>
### ![es__ES](https://img.shields.io/badge/es__ES-98.67%25-yellow)
### ![es__ES](https://img.shields.io/badge/es__ES-99.13%25-yellow)
<details>
<summary>Missing keys in es_ES.axaml</summary>
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.CommitCM.PushRevision
- Text.Merge.Edit
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.WorkingCopy.AddToGitIgnore.InFolder
</details>
### ![fr__FR](https://img.shields.io/badge/fr__FR-98.67%25-yellow)
### ![fr__FR](https://img.shields.io/badge/fr__FR-91.19%25-yellow)
<details>
<summary>Missing keys in fr_FR.axaml</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details>
### ![it__IT](https://img.shields.io/badge/it__IT-98.40%25-yellow)
### ![it__IT](https://img.shields.io/badge/it__IT-96.53%25-yellow)
<details>
<summary>Missing keys in it_IT.axaml</summary>
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CopyFullPath
- Text.Preferences.General.ShowTagsInGraph
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.Avatar.Load
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.ClearStashes
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ResetAuthor
</details>
### ![ja__JP](https://img.shields.io/badge/ja__JP-98.40%25-yellow)
### ![ja__JP](https://img.shields.io/badge/ja__JP-90.94%25-yellow)
<details>
<summary>Missing keys in ja_JP.axaml</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.CompareWithCurrent
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.FilterCommits
- Text.Repository.Tags.OrderByNameDes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details>
### ![pt__BR](https://img.shields.io/badge/pt__BR-89.76%25-yellow)
### ![pt__BR](https://img.shields.io/badge/pt__BR-83.25%25-yellow)
<details>
<summary>Missing keys in pt_BR.axaml</summary>
@ -121,14 +230,32 @@ This document shows the translation status of each locale file in the repository
- Text.ApplyStash.DropAfterApply
- Text.ApplyStash.RestoreIndex
- Text.ApplyStash.Stash
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.CustomAction
- Text.BranchCM.MergeMultiBranches
- Text.BranchCM.ResetToSelectedCommit
- Text.BranchUpstreamInvalid
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.Clone.RecurseSubmodules
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.Merge
- Text.CommitCM.MergeMultiple
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.CustomAction.Scope.Branch
- Text.Configure.CustomAction.WaitForExit
- Text.Configure.Git.PreferredMergeMode
@ -140,19 +267,32 @@ This document shows the translation status of each locale file in the repository
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CopyFullPath
- Text.CreateBranch.Name.WarnSpace
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.DeleteRepositoryNode.Path
- Text.DeleteRepositoryNode.TipForGroup
- Text.DeleteRepositoryNode.TipForRepository
- Text.Diff.First
- Text.Diff.Last
- Text.Diff.Submodule.Deleted
- Text.Diff.UseBlockNavigation
- Text.Fetch.Force
- Text.FileCM.ResolveUsing
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.Clone
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.InProgress.CherryPick.Head
- Text.InProgress.Merge.Operating
- Text.InProgress.Rebase.StoppedAt
- Text.InProgress.Revert.Head
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Merge.Source
- Text.MergeMultiple
- Text.MergeMultiple.CommitChanges
@ -163,7 +303,15 @@ This document shows the translation status of each locale file in the repository
- Text.Preferences.General.DateFormat
- Text.Preferences.General.ShowChildren
- Text.Preferences.General.ShowTagsInGraph
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Preferences.Git.SSLVerify
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.FilterCommits
- Text.Repository.HistoriesLayout
- Text.Repository.HistoriesLayout.Horizontal
@ -171,47 +319,198 @@ This document shows the translation status of each locale file in the repository
- Text.Repository.HistoriesOrder
- Text.Repository.Notifications.Clear
- Text.Repository.OnlyHighlightCurrentBranchInHistories
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.Skip
- Text.Repository.Tags.OrderByCreatorDate
- Text.Repository.Tags.OrderByNameAsc
- Text.Repository.Tags.OrderByNameDes
- Text.Repository.Tags.OrderByName
- Text.Repository.Tags.Sort
- Text.Repository.UseRelativeTimeInHistories
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.SetUpstream
- Text.SetUpstream.Local
- Text.SetUpstream.Unset
- Text.SetUpstream.Upstream
- Text.SHALinkCM.NavigateTo
- Text.Stash.AutoRestore
- Text.Stash.AutoRestore.Tip
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.StashCM.SaveAsPatch
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.CommitToEdit
- Text.WorkingCopy.ConfirmCommitWithFilter
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
- Text.WorkingCopy.SignOff
</details>
### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen)
### ![ta__IN](https://img.shields.io/badge/ta__IN-98.67%25-yellow)
### ![ta__IN](https://img.shields.io/badge/ta__IN-91.07%25-yellow)
<details>
<summary>Missing keys in ta_IN.axaml</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.CompareWithCurrent
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.Configure.Git.PreferredMergeMode
- Text.ConfirmEmptyCommit.Continue
- Text.ConfirmEmptyCommit.NoLocalChanges
- Text.ConfirmEmptyCommit.StageAllThenCommit
- Text.ConfirmEmptyCommit.WithLocalChanges
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.UpdateSubmodules.Target
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.Conflicts.OpenExternalMergeTool
- Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts
- Text.WorkingCopy.Conflicts.UseMine
- Text.WorkingCopy.Conflicts.UseTheirs
- Text.WorkingCopy.ResetAuthor
</details>
### ![uk__UA](https://img.shields.io/badge/uk__UA-92.31%25-yellow)
<details>
<summary>Missing keys in uk_UA.axaml</summary>
- Text.Avatar.Load
- Text.Bisect
- Text.Bisect.Abort
- Text.Bisect.Bad
- Text.Bisect.Detecting
- Text.Bisect.Good
- Text.Bisect.Skip
- Text.Bisect.WaitingForRange
- Text.BranchCM.ResetToSelectedCommit
- Text.Checkout.RecurseSubmodules
- Text.Checkout.WithFastForward
- Text.Checkout.WithFastForward.Upstream
- Text.CommitCM.CopyAuthor
- Text.CommitCM.CopyCommitter
- Text.CommitCM.CopySubject
- Text.CommitCM.PushRevision
- Text.CommitDetail.Changes.Count
- Text.CommitMessageTextBox.SubjectCount
- Text.ConfigureWorkspace.Name
- Text.CreateBranch.OverwriteExisting
- Text.DeinitSubmodule
- Text.DeinitSubmodule.Force
- Text.DeinitSubmodule.Path
- Text.Diff.Submodule.Deleted
- Text.GitFlow.FinishWithPush
- Text.GitFlow.FinishWithSquash
- Text.Hotkeys.Global.SwitchWorkspace
- Text.Hotkeys.Global.SwitchTab
- Text.Hotkeys.TextEditor.OpenExternalMergeTool
- Text.Launcher.Workspaces
- Text.Launcher.Pages
- Text.Merge.Edit
- Text.Preferences.Git.IgnoreCRAtEOLInDiff
- Text.Pull.RecurseSubmodules
- Text.Push.Revision
- Text.Push.Revision.Title
- Text.Repository.BranchSort
- Text.Repository.BranchSort.ByCommitterDate
- Text.Repository.BranchSort.ByName
- Text.Repository.ClearStashes
- Text.Repository.Search.ByContent
- Text.Repository.ShowSubmodulesAsTree
- Text.Repository.ViewLogs
- Text.Repository.Visit
- Text.ResetWithoutCheckout
- Text.ResetWithoutCheckout.MoveTo
- Text.ResetWithoutCheckout.Target
- Text.Stash.Mode
- Text.StashCM.CopyMessage
- Text.Submodule.Deinit
- Text.Submodule.Status
- Text.Submodule.Status.Modified
- Text.Submodule.Status.NotInited
- Text.Submodule.Status.RevisionChanged
- Text.Submodule.Status.Unmerged
- Text.Submodule.URL
- Text.ViewLogs
- Text.ViewLogs.Clear
- Text.ViewLogs.CopyLog
- Text.ViewLogs.Delete
- Text.WorkingCopy.AddToGitIgnore.InFolder
- Text.WorkingCopy.ResetAuthor
</details>

View file

@ -1 +1 @@
2025.13
2025.23

View file

@ -12,4 +12,4 @@
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.
> Please replace the `$RUNTIME_IDENTIFIER` with one of `win-x64`,`win-arm64`,`linux-x64`,`linux-arm64`,`osx-x64`,`osx-arm64`, and replace the `$DESTINATION_FOLDER` with the real path that will store the output executable files.

View file

@ -14,6 +14,22 @@ async function parseXml(filePath) {
return parser.parseStringPromise(data);
}
async function filterAndSortTranslations(localeData, enUSKeys, enUSData) {
const strings = localeData.ResourceDictionary['x:String'];
// Remove keys that don't exist in English file
const filtered = strings.filter(item => enUSKeys.has(item.$['x:Key']));
// Sort based on the key order in English file
const enUSKeysArray = enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']);
filtered.sort((a, b) => {
const aIndex = enUSKeysArray.indexOf(a.$['x:Key']);
const bIndex = enUSKeysArray.indexOf(b.$['x:Key']);
return aIndex - bIndex;
});
return filtered;
}
async function calculateTranslationRate() {
const enUSData = await parseXml(enUSFile);
const enUSKeys = new Set(enUSData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
@ -33,6 +49,21 @@ async function calculateTranslationRate() {
const localeKeys = new Set(localeData.ResourceDictionary['x:String'].map(item => item.$['x:Key']));
const missingKeys = [...enUSKeys].filter(key => !localeKeys.has(key));
// Sort and clean up extra translations
const sortedAndCleaned = await filterAndSortTranslations(localeData, enUSKeys, enUSData);
localeData.ResourceDictionary['x:String'] = sortedAndCleaned;
// Save the updated file
const builder = new xml2js.Builder({
headless: true,
renderOpts: { pretty: true, indent: ' ' }
});
let xmlStr = builder.buildObject(localeData);
// Add an empty line before the first x:String
xmlStr = xmlStr.replace(' <x:String', '\n <x:String');
await fs.writeFile(filePath, xmlStr + '\n', 'utf8');
if (missingKeys.length > 0) {
const progress = ((enUSKeys.size - missingKeys.length) / enUSKeys.size) * 100;
const badgeColor = progress >= 75 ? 'yellow' : 'red';

View file

@ -37,10 +37,10 @@ namespace SourceGit
}
}
public static readonly Command OpenPreferencesCommand = new Command(_ => OpenDialog(new Views.Preferences()));
public static readonly Command OpenHotkeysCommand = new Command(_ => OpenDialog(new Views.Hotkeys()));
public static readonly Command OpenPreferencesCommand = new Command(_ => ShowWindow(new Views.Preferences(), false));
public static readonly Command OpenHotkeysCommand = new Command(_ => ShowWindow(new Views.Hotkeys(), false));
public static readonly Command OpenAppDataDirCommand = new Command(_ => Native.OS.OpenInFileManager(Native.OS.DataDir));
public static readonly Command OpenAboutCommand = new Command(_ => OpenDialog(new Views.About()));
public static readonly Command OpenAboutCommand = new Command(_ => ShowWindow(new Views.About(), false));
public static readonly Command CheckForUpdateCommand = new Command(_ => (Current as App)?.Check4Update(true));
public static readonly Command QuitCommand = new Command(_ => Quit(0));
public static readonly Command CopyTextBlockCommand = new Command(p =>

View file

@ -16,6 +16,7 @@
<ResourceInclude x:Key="fr_FR" Source="/Resources/Locales/fr_FR.axaml"/>
<ResourceInclude x:Key="it_IT" Source="/Resources/Locales/it_IT.axaml"/>
<ResourceInclude x:Key="pt_BR" Source="/Resources/Locales/pt_BR.axaml"/>
<ResourceInclude x:Key="uk_UA" Source="/Resources/Locales/uk_UA.axaml"/>
<ResourceInclude x:Key="ru_RU" Source="/Resources/Locales/ru_RU.axaml"/>
<ResourceInclude x:Key="zh_CN" Source="/Resources/Locales/zh_CN.axaml"/>
<ResourceInclude x:Key="zh_TW" Source="/Resources/Locales/zh_TW.axaml"/>
@ -34,7 +35,7 @@
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="{DynamicResource Text.About.Menu}" Command="{x:Static s:App.OpenAboutCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}"/>
<NativeMenuItem Header="{DynamicResource Text.Hotkeys}" Command="{x:Static s:App.OpenHotkeysCommand}" Gesture="F1"/>
<NativeMenuItem Header="{DynamicResource Text.SelfUpdate}" Command="{x:Static s:App.CheckForUpdateCommand}" IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{DynamicResource Text.Preferences}" Command="{x:Static s:App.OpenPreferencesCommand}" Gesture="⌘+,"/>

View file

@ -6,6 +6,7 @@ using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@ -77,7 +78,7 @@ namespace SourceGit
return builder;
}
private static void LogException(Exception ex)
public static void LogException(Exception ex)
{
if (ex == null)
return;
@ -104,10 +105,44 @@ namespace SourceGit
#endregion
#region Utility Functions
public static void OpenDialog(Window window)
public static void ShowWindow(object data, bool showAsDialog)
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
window.ShowDialog(owner);
var impl = (Views.ChromelessWindow target, bool isDialog) =>
{
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
{
if (isDialog)
target.ShowDialog(owner);
else
target.Show(owner);
}
else
{
target.Show();
}
};
if (data is Views.ChromelessWindow window)
{
impl(window, showAsDialog);
return;
}
var dataTypeName = data.GetType().FullName;
if (string.IsNullOrEmpty(dataTypeName) || !dataTypeName.Contains(".ViewModels.", StringComparison.Ordinal))
return;
var viewTypeName = dataTypeName.Replace(".ViewModels.", ".Views.");
var viewType = Type.GetType(viewTypeName);
if (viewType == null || !viewType.IsSubclassOf(typeof(Views.ChromelessWindow)))
return;
window = Activator.CreateInstance(viewType) as Views.ChromelessWindow;
if (window != null)
{
window.DataContext = data;
impl(window, showAsDialog);
}
}
public static void RaiseException(string context, string message)
@ -266,7 +301,7 @@ namespace SourceGit
return await clipboard.GetTextAsync();
}
}
return default;
return null;
}
public static string Text(string key, params object[] args)
@ -288,8 +323,7 @@ namespace SourceGit
icon.Height = 12;
icon.Stretch = Stretch.Uniform;
var geo = Current?.FindResource(key) as StreamGeometry;
if (geo != null)
if (Current?.FindResource(key) is StreamGeometry geo)
icon.Data = geo;
return icon;
@ -303,7 +337,7 @@ namespace SourceGit
return null;
}
public static ViewModels.Launcher GetLauncer()
public static ViewModels.Launcher GetLauncher()
{
return Current is App app ? app._launcher : null;
}
@ -341,13 +375,42 @@ namespace SourceGit
{
BindingPlugins.DataValidators.RemoveAt(0);
// Disable tooltip if window is not active.
ToolTip.ToolTipOpeningEvent.AddClassHandler<Control>((c, e) =>
{
var topLevel = TopLevel.GetTopLevel(c);
if (topLevel is not Window { IsActive: true })
e.Cancel = true;
});
if (TryLaunchAsCoreEditor(desktop))
return;
if (TryLaunchAsAskpass(desktop))
return;
TryLaunchAsNormal(desktop);
_ipcChannel = new Models.IpcChannel();
if (!_ipcChannel.IsFirstInstance)
{
var arg = desktop.Args is { Length: > 0 } ? desktop.Args[0].Trim() : string.Empty;
if (!string.IsNullOrEmpty(arg))
{
if (arg.StartsWith('"') && arg.EndsWith('"'))
arg = arg.Substring(1, arg.Length - 2).Trim();
if (arg.Length > 0 && !Path.IsPathFullyQualified(arg))
arg = Path.GetFullPath(arg);
}
_ipcChannel.SendToFirstInstance(arg);
Environment.Exit(0);
}
else
{
_ipcChannel.MessageReceived += TryOpenRepository;
desktop.Exit += (_, _) => _ipcChannel.Dispose();
TryLaunchAsNormal(desktop);
}
}
}
#endregion
@ -420,21 +483,37 @@ namespace SourceGit
return true;
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 origHeadFile = Path.Combine(gitDir, "rebase-merge", "orig-head");
var ontoFile = Path.Combine(gitDir, "rebase-merge", "onto");
var doneFile = Path.Combine(gitDir, "rebase-merge", "done");
if (!File.Exists(doneFile))
var jobsFile = Path.Combine(gitDir, "sourcegit_rebase_jobs.json");
if (!File.Exists(ontoFile) || !File.Exists(origHeadFile) || !File.Exists(doneFile) || !File.Exists(jobsFile))
return true;
var done = File.ReadAllText(doneFile).Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
if (done.Length > collection.Jobs.Count)
var origHead = File.ReadAllText(origHeadFile).Trim();
var onto = File.ReadAllText(ontoFile).Trim();
var collection = JsonSerializer.Deserialize(File.ReadAllText(jobsFile), JsonCodeGen.Default.InteractiveRebaseJobCollection);
if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead))
return true;
var job = collection.Jobs[done.Length - 1];
File.WriteAllText(file, job.Message);
var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
if (done.Length == 0)
return true;
var current = done[^1].Trim();
var match = REG_REBASE_TODO().Match(current);
if (!match.Success)
return true;
var sha = match.Groups[1].Value;
foreach (var job in collection.Jobs)
{
if (job.SHA.StartsWith(sha))
{
File.WriteAllText(file, job.Message);
break;
}
}
return true;
}
@ -442,7 +521,7 @@ namespace SourceGit
private bool TryLaunchAsCoreEditor(IClassicDesktopStyleApplicationLifetime desktop)
{
var args = desktop.Args;
if (args == null || args.Length <= 1 || !args[0].Equals("--core-editor", StringComparison.Ordinal))
if (args is not { Length: > 1 } || !args[0].Equals("--core-editor", StringComparison.Ordinal))
return false;
var file = args[1];
@ -452,8 +531,8 @@ namespace SourceGit
return true;
}
var editor = new Views.StandaloneCommitMessageEditor();
editor.SetFile(file);
var editor = new Views.CommitMessageEditor();
editor.AsStandalone(file);
desktop.MainWindow = editor;
return true;
}
@ -478,23 +557,54 @@ namespace SourceGit
private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
{
Native.OS.SetupEnternalTools();
Native.OS.SetupExternalTools();
Models.AvatarManager.Instance.Start();
string startupRepo = null;
if (desktop.Args != null && desktop.Args.Length == 1 && Directory.Exists(desktop.Args[0]))
startupRepo = desktop.Args[0];
var pref = ViewModels.Preferences.Instance;
pref.SetCanModify();
_launcher = new ViewModels.Launcher(startupRepo);
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
#if !DISABLE_UPDATE_DETECTION
var pref = ViewModels.Preferences.Instance;
if (pref.ShouldCheck4UpdateOnStartup())
Check4Update();
#endif
}
private void TryOpenRepository(string repo)
{
if (!string.IsNullOrEmpty(repo) && Directory.Exists(repo))
{
var test = new Commands.QueryRepositoryRootPath(repo).ReadToEnd();
if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut))
{
Dispatcher.UIThread.Invoke(() =>
{
var node = ViewModels.Preferences.Instance.FindOrAddNodeByRepositoryPath(test.StdOut.Trim(), null, false);
ViewModels.Welcome.Instance.Refresh();
_launcher?.OpenRepositoryInTab(node, null);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher wnd })
wnd.BringToTop();
});
return;
}
}
Dispatcher.UIThread.Invoke(() =>
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: Views.Launcher launcher })
launcher.BringToTop();
});
}
private void Check4Update(bool manually = false)
{
Task.Run(async () =>
@ -540,11 +650,7 @@ namespace SourceGit
{
Dispatcher.UIThread.Post(() =>
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
{
var dialog = new Views.SelfUpdate() { DataContext = new ViewModels.SelfUpdate() { Data = data } };
dialog.ShowDialog(owner);
}
ShowWindow(new ViewModels.SelfUpdate() { Data = data }, true);
});
}
@ -574,12 +680,24 @@ namespace SourceGit
prevChar = c;
}
trimmed.Add(sb.ToString());
var name = sb.ToString();
if (name.Contains('#', StringComparison.Ordinal))
{
if (!name.Equals("fonts:Inter#Inter", StringComparison.Ordinal) &&
!name.Equals("fonts:SourceGit#JetBrains Mono", StringComparison.Ordinal))
continue;
}
trimmed.Add(name);
}
return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty;
}
[GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,40})(\s+.*)?$")]
private static partial Regex REG_REBASE_TODO();
private Models.IpcChannel _ipcChannel = null;
private ViewModels.Launcher _launcher = null;
private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null;

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="SourceGit.Desktop"/>

View file

@ -1,7 +1,4 @@
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public class Add : Command
{
@ -12,20 +9,11 @@ namespace SourceGit.Commands
Args = includeUntracked ? "add ." : "add -u .";
}
public Add(string repo, List<string> changes)
public Add(string repo, Models.Change change)
{
WorkingDirectory = repo;
Context = repo;
var builder = new StringBuilder();
builder.Append("add --");
foreach (var c in changes)
{
builder.Append(" \"");
builder.Append(c);
builder.Append("\"");
}
Args = builder.ToString();
Args = $"add -- \"{change.Path}\"";
}
public Add(string repo, string pathspecFromFile)

View file

@ -1,23 +1,12 @@
using System;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public class Archive : Command
{
public Archive(string repo, string revision, string saveTo, Action<string> outputHandler)
public Archive(string repo, string revision, string saveTo)
{
WorkingDirectory = repo;
Context = repo;
Args = $"archive --format=zip --verbose --output=\"{saveTo}\" {revision}";
TraitErrorAsOutput = true;
_outputHandler = outputHandler;
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
}

View file

@ -1,75 +1,14 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public partial class AssumeUnchanged
public class AssumeUnchanged : Command
{
[GeneratedRegex(@"^(\w)\s+(.+)$")]
private static partial Regex REG_PARSE();
class ViewCommand : Command
public AssumeUnchanged(string repo, string file, bool bAdd)
{
public ViewCommand(string repo)
{
WorkingDirectory = repo;
Args = "ls-files -v";
RaiseError = false;
}
var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged";
public List<string> Result()
{
Exec();
return _outs;
}
protected override void OnReadline(string line)
{
var match = REG_PARSE().Match(line);
if (!match.Success)
return;
if (match.Groups[1].Value == "h")
{
_outs.Add(match.Groups[2].Value);
}
}
private readonly List<string> _outs = new List<string>();
WorkingDirectory = repo;
Context = repo;
Args = $"update-index {mode} -- \"{file}\"";
}
class ModCommand : Command
{
public ModCommand(string repo, string file, bool bAdd)
{
var mode = bAdd ? "--assume-unchanged" : "--no-assume-unchanged";
WorkingDirectory = repo;
Context = repo;
Args = $"update-index {mode} -- \"{file}\"";
}
}
public AssumeUnchanged(string repo)
{
_repo = repo;
}
public List<string> View()
{
return new ViewCommand(_repo).Result();
}
public void Add(string file)
{
new ModCommand(_repo, file, true).Exec();
}
public void Remove(string file)
{
new ModCommand(_repo, file, false).Exec();
}
private readonly string _repo;
}
}

13
src/Commands/Bisect.cs Normal file
View file

@ -0,0 +1,13 @@
namespace SourceGit.Commands
{
public class Bisect : Command
{
public Bisect(string repo, string subcmd)
{
WorkingDirectory = repo;
Context = repo;
RaiseError = false;
Args = $"bisect {subcmd}";
}
}
}

View file

@ -21,10 +21,17 @@ namespace SourceGit.Commands
public Models.BlameData Result()
{
var succ = Exec();
if (!succ)
var rs = ReadToEnd();
if (!rs.IsSuccess)
return _result;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
return new Models.BlameData();
ParseLine(line);
if (_result.IsBinary)
break;
}
if (_needUnifyCommitSHA)
@ -42,14 +49,9 @@ namespace SourceGit.Commands
return _result;
}
protected override void OnReadline(string line)
private void ParseLine(string line)
{
if (_result.IsBinary)
return;
if (string.IsNullOrEmpty(line))
return;
if (line.IndexOf('\0', StringComparison.Ordinal) >= 0)
if (line.Contains('\0', StringComparison.Ordinal))
{
_result.IsBinary = true;
_result.LineInfos.Clear();
@ -87,7 +89,7 @@ namespace SourceGit.Commands
private readonly Models.BlameData _result = new Models.BlameData();
private readonly StringBuilder _content = new StringBuilder();
private readonly string _dateFormat = Models.DateTimeFormat.Actived.DateOnly;
private readonly string _dateFormat = Models.DateTimeFormat.Active.DateOnly;
private string _lastSHA = string.Empty;
private bool _needUnifyCommitSHA = false;
private int _minSHALen = 64;

View file

@ -1,4 +1,6 @@
namespace SourceGit.Commands
using System.Text;
namespace SourceGit.Commands
{
public static class Branch
{
@ -11,29 +13,40 @@
return cmd.ReadToEnd().StdOut.Trim();
}
public static bool Create(string repo, string name, string basedOn)
public static bool Create(string repo, string name, string basedOn, bool force, Models.ICommandLog log)
{
var builder = new StringBuilder();
builder.Append("branch ");
if (force)
builder.Append("-f ");
builder.Append(name);
builder.Append(" ");
builder.Append(basedOn);
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch {name} {basedOn}";
cmd.Args = builder.ToString();
cmd.Log = log;
return cmd.Exec();
}
public static bool Rename(string repo, string name, string to)
public static bool Rename(string repo, string name, string to, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -M {name} {to}";
cmd.Log = log;
return cmd.Exec();
}
public static bool SetUpstream(string repo, string name, string upstream)
public static bool SetUpstream(string repo, string name, string upstream, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Log = log;
if (string.IsNullOrEmpty(upstream))
cmd.Args = $"branch {name} --unset-upstream";
@ -43,25 +56,27 @@
return cmd.Exec();
}
public static bool DeleteLocal(string repo, string name)
public static bool DeleteLocal(string repo, string name, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -D {name}";
cmd.Log = log;
return cmd.Exec();
}
public static bool DeleteRemote(string repo, string remote, string name)
public static bool DeleteRemote(string repo, string remote, string name, Models.ICommandLog log)
{
bool exists = new Remote(repo).HasBranch(remote, name);
if (exists)
return new Push(repo, remote, $"refs/heads/{name}", true).Exec();
return new Push(repo, remote, $"refs/heads/{name}", true) { Log = log }.Exec();
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"branch -D -r {remote}/{name}";
cmd.Log = log;
return cmd.Exec();
}
}

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
@ -12,19 +11,37 @@ namespace SourceGit.Commands
Context = repo;
}
public bool Branch(string branch, Action<string> onProgress)
public bool Branch(string branch, bool force)
{
Args = $"checkout --recurse-submodules --progress {branch}";
TraitErrorAsOutput = true;
_outputHandler = onProgress;
var builder = new StringBuilder();
builder.Append("checkout --progress ");
if (force)
builder.Append("--force ");
builder.Append(branch);
Args = builder.ToString();
return Exec();
}
public bool Branch(string branch, string basedOn, Action<string> onProgress)
public bool Branch(string branch, string basedOn, bool force, bool allowOverwrite)
{
Args = $"checkout --recurse-submodules --progress -b {branch} {basedOn}";
TraitErrorAsOutput = true;
_outputHandler = onProgress;
var builder = new StringBuilder();
builder.Append("checkout --progress ");
if (force)
builder.Append("--force ");
builder.Append(allowOverwrite ? "-B " : "-b ");
builder.Append(branch);
builder.Append(" ");
builder.Append(basedOn);
Args = builder.ToString();
return Exec();
}
public bool Commit(string commitId, bool force)
{
var option = force ? "--force" : string.Empty;
Args = $"checkout {option} --detach --progress {commitId}";
return Exec();
}
@ -61,20 +78,5 @@ namespace SourceGit.Commands
Args = $"checkout --no-overlay {revision} -- \"{file}\"";
return Exec();
}
public bool Commit(string commitId, Action<string> onProgress)
{
Args = $"checkout --detach --progress {commitId}";
TraitErrorAsOutput = true;
_outputHandler = onProgress;
return Exec();
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -1,31 +1,12 @@
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public class Clean : Command
{
public Clean(string repo, bool includeIgnored)
public Clean(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = includeIgnored ? "clean -qfdx" : "clean -qfd";
}
public Clean(string repo, List<string> files)
{
var builder = new StringBuilder();
builder.Append("clean -qfd --");
foreach (var f in files)
{
builder.Append(" \"");
builder.Append(f);
builder.Append("\"");
}
WorkingDirectory = repo;
Context = repo;
Args = builder.ToString();
Args = "clean -qfdx";
}
}
}

View file

@ -1,16 +1,11 @@
using System;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public class Clone : Command
{
private readonly Action<string> _notifyProgress;
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs, Action<string> ouputHandler)
public Clone(string ctx, string path, string url, string localName, string sshKey, string extraArgs)
{
Context = ctx;
WorkingDirectory = path;
TraitErrorAsOutput = true;
SSHKey = sshKey;
Args = "clone --progress --verbose ";
@ -21,13 +16,6 @@ namespace SourceGit.Commands
if (!string.IsNullOrEmpty(localName))
Args += localName;
_notifyProgress = ouputHandler;
}
protected override void OnReadline(string line)
{
_notifyProgress?.Invoke(line);
}
}
}

View file

@ -32,45 +32,18 @@ namespace SourceGit.Commands
public string SSHKey { get; set; } = string.Empty;
public string Args { get; set; } = string.Empty;
public bool RaiseError { get; set; } = true;
public bool TraitErrorAsOutput { get; set; } = false;
public Models.ICommandLog Log { get; set; } = null;
public bool Exec()
{
Log?.AppendLine($"$ git {Args}\n");
var start = CreateGitStartInfo();
var errs = new List<string>();
var proc = new Process() { StartInfo = start };
proc.OutputDataReceived += (_, e) =>
{
if (e.Data != null)
OnReadline(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (string.IsNullOrEmpty(e.Data))
{
errs.Add(string.Empty);
return;
}
if (TraitErrorAsOutput)
OnReadline(e.Data);
// Ignore progress messages
if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal))
return;
if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal))
return;
if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal))
return;
if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal))
return;
if (REG_PROGRESS().IsMatch(e.Data))
return;
errs.Add(e.Data);
};
proc.OutputDataReceived += (_, e) => HandleOutput(e.Data, errs);
proc.ErrorDataReceived += (_, e) => HandleOutput(e.Data, errs);
var dummy = null as Process;
var dummyProcLock = new object();
@ -97,6 +70,7 @@ namespace SourceGit.Commands
if (RaiseError)
Dispatcher.UIThread.Post(() => App.RaiseException(Context, e.Message));
Log?.AppendLine(string.Empty);
return false;
}
@ -114,6 +88,7 @@ namespace SourceGit.Commands
int exitCode = proc.ExitCode;
proc.Close();
Log?.AppendLine(string.Empty);
if (!CancellationToken.IsCancellationRequested && exitCode != 0)
{
@ -162,11 +137,6 @@ namespace SourceGit.Commands
return rs;
}
protected virtual void OnReadline(string line)
{
// Implemented by derived class
}
private ProcessStartInfo CreateGitStartInfo()
{
var start = new ProcessStartInfo();
@ -222,6 +192,28 @@ namespace SourceGit.Commands
return start;
}
private void HandleOutput(string line, List<string> errs)
{
line ??= string.Empty;
Log?.AppendLine(line);
// Lines to hide in error message.
if (line.Length > 0)
{
if (line.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal) ||
line.StartsWith("remote: Counting objects:", StringComparison.Ordinal) ||
line.StartsWith("remote: Compressing objects:", StringComparison.Ordinal) ||
line.StartsWith("Filtering content:", StringComparison.Ordinal) ||
line.StartsWith("hint:", StringComparison.Ordinal))
return;
if (REG_PROGRESS().IsMatch(line))
return;
}
errs.Add(line);
}
[GeneratedRegex(@"\d+%")]
private static partial Regex REG_PROGRESS();
}

View file

@ -4,19 +4,18 @@ namespace SourceGit.Commands
{
public class Commit : Command
{
public Commit(string repo, string message, bool amend, bool signOff)
public Commit(string repo, string message, bool signOff, bool amend, bool resetAuthor)
{
_tmpFile = Path.GetTempFileName();
File.WriteAllText(_tmpFile, message);
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
Args = $"commit --allow-empty --file=\"{_tmpFile}\"";
if (amend)
Args += " --amend --no-edit";
if (signOff)
Args += " --signoff";
if (amend)
Args += resetAuthor ? " --amend --reset-author --no-edit" : " --amend --no-edit";
}
public bool Run()
@ -35,6 +34,6 @@ namespace SourceGit.Commands
return succ;
}
private string _tmpFile = string.Empty;
private readonly string _tmpFile;
}
}

View file

@ -31,12 +31,19 @@ namespace SourceGit.Commands
public List<Models.Change> Result()
{
Exec();
_changes.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal));
var rs = ReadToEnd();
if (!rs.IsSuccess)
return _changes;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
ParseLine(line);
_changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path));
return _changes;
}
protected override void OnReadline(string line)
private void ParseLine(string line)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)

View file

@ -8,7 +8,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain";
Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain";
}
public int Result()

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@ -28,34 +28,48 @@ namespace SourceGit.Commands
Context = repo;
if (ignoreWhitespace)
Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --ignore-all-space --unified={unified} {opt}";
Args = $"diff --no-ext-diff --patch --ignore-all-space --unified={unified} {opt}";
else if (Models.DiffOption.IgnoreCRAtEOL)
Args = $"diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
else
Args = $"-c core.autocrlf=false diff --no-ext-diff --patch --ignore-cr-at-eol --unified={unified} {opt}";
Args = $"diff --no-ext-diff --patch --unified={unified} {opt}";
}
public Models.DiffResult Result()
{
Exec();
var rs = ReadToEnd();
var start = 0;
var end = rs.StdOut.IndexOf('\n', start);
while (end > 0)
{
var line = rs.StdOut.Substring(start, end - start);
ParseLine(line);
if (_result.IsBinary || _result.IsLFS)
start = end + 1;
end = rs.StdOut.IndexOf('\n', start);
}
if (start < rs.StdOut.Length)
ParseLine(rs.StdOut.Substring(start));
if (_result.IsBinary || _result.IsLFS || _result.TextDiff.Lines.Count == 0)
{
_result.TextDiff = null;
}
else
{
ProcessInlineHighlights();
if (_result.TextDiff.Lines.Count == 0)
_result.TextDiff = null;
else
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
}
return _result;
}
protected override void OnReadline(string line)
private void ParseLine(string line)
{
if (_result.IsBinary)
return;
if (line.StartsWith("old mode ", StringComparison.Ordinal))
{
_result.OldMode = line.Substring(9);
@ -80,9 +94,6 @@ namespace SourceGit.Commands
return;
}
if (_result.IsBinary)
return;
if (_result.IsLFS)
{
var ch = line[0];
@ -94,7 +105,7 @@ namespace SourceGit.Commands
}
else if (line.StartsWith("-size ", StringComparison.Ordinal))
{
_result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
_result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6));
}
}
else if (ch == '+')
@ -105,12 +116,12 @@ namespace SourceGit.Commands
}
else if (line.StartsWith("+size ", StringComparison.Ordinal))
{
_result.LFSDiff.New.Size = long.Parse(line.Substring(6));
_result.LFSDiff.New.Size = long.Parse(line.AsSpan(6));
}
}
else if (line.StartsWith(" size ", StringComparison.Ordinal))
{
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.Substring(6));
_result.LFSDiff.New.Size = _result.LFSDiff.Old.Size = long.Parse(line.AsSpan(6));
}
return;
}
@ -140,7 +151,8 @@ namespace SourceGit.Commands
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
_last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0);
_result.TextDiff.Lines.Add(_last);
}
}
else
@ -148,7 +160,8 @@ namespace SourceGit.Commands
if (line.Length == 0)
{
ProcessInlineHighlights();
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine));
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine);
_result.TextDiff.Lines.Add(_last);
_oldLine++;
_newLine++;
return;
@ -164,7 +177,8 @@ namespace SourceGit.Commands
return;
}
_deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0));
_last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0);
_deleted.Add(_last);
_oldLine++;
}
else if (ch == '+')
@ -176,7 +190,8 @@ namespace SourceGit.Commands
return;
}
_added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine));
_last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine);
_added.Add(_last);
_newLine++;
}
else if (ch != '\\')
@ -187,7 +202,8 @@ namespace SourceGit.Commands
{
_oldLine = int.Parse(match.Groups[1].Value);
_newLine = int.Parse(match.Groups[2].Value);
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
_last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0);
_result.TextDiff.Lines.Add(_last);
}
else
{
@ -198,11 +214,16 @@ namespace SourceGit.Commands
return;
}
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine));
_last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine);
_result.TextDiff.Lines.Add(_last);
_oldLine++;
_newLine++;
}
}
else if (line.Equals("\\ No newline at end of file", StringComparison.Ordinal))
{
_last.NoNewLineEndOfFile = true;
}
}
}
@ -253,6 +274,7 @@ namespace SourceGit.Commands
private readonly Models.DiffResult _result = new Models.DiffResult();
private readonly List<Models.TextDiffLine> _deleted = new List<Models.TextDiffLine>();
private readonly List<Models.TextDiffLine> _added = new List<Models.TextDiffLine>();
private Models.TextDiffLine _last = null;
private int _oldLine = 0;
private int _newLine = 0;
}

View file

@ -1,39 +1,95 @@
using System;
using System.Collections.Generic;
using System.IO;
using Avalonia.Threading;
namespace SourceGit.Commands
{
public static class Discard
{
public static void All(string repo, bool includeIgnored)
/// <summary>
/// Discard all local changes (unstaged & staged)
/// </summary>
/// <param name="repo"></param>
/// <param name="includeIgnored"></param>
/// <param name="log"></param>
public static void All(string repo, bool includeIgnored, Models.ICommandLog log)
{
new Restore(repo).Exec();
new Clean(repo, includeIgnored).Exec();
var changes = new QueryLocalChanges(repo).Result();
try
{
foreach (var c in changes)
{
if (c.WorkTree == Models.ChangeState.Untracked ||
c.WorkTree == Models.ChangeState.Added ||
c.Index == Models.ChangeState.Added ||
c.Index == Models.ChangeState.Renamed)
{
var fullPath = Path.Combine(repo, c.Path);
if (Directory.Exists(fullPath))
Directory.Delete(fullPath, true);
else
File.Delete(fullPath);
}
}
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}");
});
}
new Reset(repo, "HEAD", "--hard") { Log = log }.Exec();
if (includeIgnored)
new Clean(repo) { Log = log }.Exec();
}
public static void Changes(string repo, List<Models.Change> changes)
/// <summary>
/// Discard selected changes (only unstaged).
/// </summary>
/// <param name="repo"></param>
/// <param name="changes"></param>
/// <param name="log"></param>
public static void Changes(string repo, List<Models.Change> changes, Models.ICommandLog log)
{
var needClean = new List<string>();
var needCheckout = new List<string>();
var restores = new List<string>();
foreach (var c in changes)
try
{
if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added)
needClean.Add(c.Path);
else
needCheckout.Add(c.Path);
foreach (var c in changes)
{
if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added)
{
var fullPath = Path.Combine(repo, c.Path);
if (Directory.Exists(fullPath))
Directory.Delete(fullPath, true);
else
File.Delete(fullPath);
}
else
{
restores.Add(c.Path);
}
}
}
catch (Exception e)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(repo, $"Failed to discard changes. Reason: {e.Message}");
});
}
for (int i = 0; i < needClean.Count; i += 10)
if (restores.Count > 0)
{
var count = Math.Min(10, needClean.Count - i);
new Clean(repo, needClean.GetRange(i, count)).Exec();
}
for (int i = 0; i < needCheckout.Count; i += 10)
{
var count = Math.Min(10, needCheckout.Count - i);
new Restore(repo, needCheckout.GetRange(i, count), "--worktree --recurse-submodules").Exec();
var pathSpecFile = Path.GetTempFileName();
File.WriteAllLines(pathSpecFile, restores);
new Restore(repo, pathSpecFile, false) { Log = log }.Exec();
File.Delete(pathSpecFile);
}
}
}

View file

@ -27,7 +27,7 @@ namespace SourceGit.Commands
}
}
public static void RunAndWait(string repo, string file, string args, Action<string> outputHandler)
public static void RunAndWait(string repo, string file, string args, Models.ICommandLog log)
{
var start = new ProcessStartInfo();
start.FileName = file;
@ -40,20 +40,22 @@ namespace SourceGit.Commands
start.StandardErrorEncoding = Encoding.UTF8;
start.WorkingDirectory = repo;
log?.AppendLine($"$ {file} {args}\n");
var proc = new Process() { StartInfo = start };
var builder = new StringBuilder();
proc.OutputDataReceived += (_, e) =>
{
if (e.Data != null)
outputHandler?.Invoke(e.Data);
log?.AppendLine(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (e.Data != null)
{
outputHandler?.Invoke(e.Data);
log?.AppendLine(e.Data);
builder.AppendLine(e.Data);
}
};

View file

@ -1,15 +1,11 @@
using System;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public class Fetch : Command
{
public Fetch(string repo, string remote, bool noTags, bool force, Action<string> outputHandler)
public Fetch(string repo, string remote, bool noTags, bool force)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "fetch --progress --verbose ";
@ -24,21 +20,12 @@ namespace SourceGit.Commands
Args += remote;
}
public Fetch(string repo, Models.Branch local, Models.Branch remote, Action<string> outputHandler)
public Fetch(string repo, Models.Branch local, Models.Branch remote)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote.Remote}.sshkey");
Args = $"fetch --progress --verbose {remote.Remote} {remote.Name}:{local.Name}";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
}

View file

@ -6,6 +6,7 @@
{
WorkingDirectory = repo;
Context = repo;
Editor = EditorType.None;
Args = $"format-patch {commit} -1 --output=\"{saveTo}\"";
}
}

View file

@ -1,23 +1,12 @@
using System;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public class GC : Command
{
public GC(string repo, Action<string> outputHandler)
public GC(string repo)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
Args = "gc --prune=now";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
}

View file

@ -1,52 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Threading;
namespace SourceGit.Commands
{
public static class GitFlow
{
public class BranchDetectResult
public static bool Init(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log)
{
public bool IsGitFlowBranch { get; set; } = false;
public string Type { get; set; } = string.Empty;
public string Prefix { get; set; } = string.Empty;
}
public static bool IsEnabled(string repo, List<Models.Branch> branches)
{
var localBrancheNames = new HashSet<string>();
foreach (var branch in branches)
{
if (branch.IsLocal)
localBrancheNames.Add(branch.Name);
}
var config = new Config(repo).ListAll();
if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master))
return false;
if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop))
return false;
return config.ContainsKey("gitflow.prefix.feature") &&
config.ContainsKey("gitflow.prefix.release") &&
config.ContainsKey("gitflow.prefix.hotfix");
}
public static bool Init(string repo, List<Models.Branch> branches, string master, string develop, string feature, string release, string hotfix, string version)
{
var current = branches.Find(x => x.IsCurrent);
var masterBranch = branches.Find(x => x.Name == master);
if (masterBranch == null && current != null)
Branch.Create(repo, master, current.Head);
var devBranch = branches.Find(x => x.Name == develop);
if (devBranch == null && current != null)
Branch.Create(repo, develop, current.Head);
var config = new Config(repo);
config.Set("gitflow.branch.master", master);
config.Set("gitflow.branch.develop", develop);
@ -61,104 +21,72 @@ namespace SourceGit.Commands
init.WorkingDirectory = repo;
init.Context = repo;
init.Args = "flow init -d";
init.Log = log;
return init.Exec();
}
public static string GetPrefix(string repo, string type)
public static bool Start(string repo, Models.GitFlowBranchType type, string name, Models.ICommandLog log)
{
return new Config(repo).Get($"gitflow.prefix.{type}");
}
public static BranchDetectResult DetectType(string repo, List<Models.Branch> branches, string branch)
{
var rs = new BranchDetectResult();
var localBrancheNames = new HashSet<string>();
foreach (var b in branches)
{
if (b.IsLocal)
localBrancheNames.Add(b.Name);
}
var config = new Config(repo).ListAll();
if (!config.TryGetValue("gitflow.branch.master", out string master) || !localBrancheNames.Contains(master))
return rs;
if (!config.TryGetValue("gitflow.branch.develop", out string develop) || !localBrancheNames.Contains(develop))
return rs;
if (!config.TryGetValue("gitflow.prefix.feature", out var feature) ||
!config.TryGetValue("gitflow.prefix.release", out var release) ||
!config.TryGetValue("gitflow.prefix.hotfix", out var hotfix))
return rs;
if (branch.StartsWith(feature, StringComparison.Ordinal))
{
rs.IsGitFlowBranch = true;
rs.Type = "feature";
rs.Prefix = feature;
}
else if (branch.StartsWith(release, StringComparison.Ordinal))
{
rs.IsGitFlowBranch = true;
rs.Type = "release";
rs.Prefix = release;
}
else if (branch.StartsWith(hotfix, StringComparison.Ordinal))
{
rs.IsGitFlowBranch = true;
rs.Type = "hotfix";
rs.Prefix = hotfix;
}
return rs;
}
public static bool Start(string repo, string type, string name)
{
if (!SUPPORTED_BRANCH_TYPES.Contains(type))
{
Dispatcher.UIThread.Post(() =>
{
App.RaiseException(repo, "Bad branch type!!!");
});
return false;
}
var start = new Command();
start.WorkingDirectory = repo;
start.Context = repo;
start.Args = $"flow {type} start {name}";
switch (type)
{
case Models.GitFlowBranchType.Feature:
start.Args = $"flow feature start {name}";
break;
case Models.GitFlowBranchType.Release:
start.Args = $"flow release start {name}";
break;
case Models.GitFlowBranchType.Hotfix:
start.Args = $"flow hotfix start {name}";
break;
default:
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!"));
return false;
}
start.Log = log;
return start.Exec();
}
public static bool Finish(string repo, string type, string name, bool keepBranch)
public static bool Finish(string repo, Models.GitFlowBranchType type, string name, bool squash, bool push, bool keepBranch, Models.ICommandLog log)
{
if (!SUPPORTED_BRANCH_TYPES.Contains(type))
{
Dispatcher.UIThread.Post(() =>
{
App.RaiseException(repo, "Bad branch type!!!");
});
var builder = new StringBuilder();
builder.Append("flow ");
return false;
switch (type)
{
case Models.GitFlowBranchType.Feature:
builder.Append("feature");
break;
case Models.GitFlowBranchType.Release:
builder.Append("release");
break;
case Models.GitFlowBranchType.Hotfix:
builder.Append("hotfix");
break;
default:
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, "Bad git-flow branch type!!!"));
return false;
}
var option = keepBranch ? "-k" : string.Empty;
builder.Append(" finish ");
if (squash)
builder.Append("--squash ");
if (push)
builder.Append("--push ");
if (keepBranch)
builder.Append("-k ");
builder.Append(name);
var finish = new Command();
finish.WorkingDirectory = repo;
finish.Context = repo;
finish.Args = $"flow {type} finish {option} {name}";
finish.Args = builder.ToString();
finish.Log = log;
return finish.Exec();
}
private static readonly List<string> SUPPORTED_BRANCH_TYPES = new List<string>()
{
"feature",
"release",
"bugfix",
"hotfix",
"support",
};
}
}

View file

@ -8,7 +8,14 @@ namespace SourceGit.Commands
{
var file = Path.Combine(repo, ".gitignore");
if (!File.Exists(file))
{
File.WriteAllLines(file, [pattern]);
return;
}
var org = File.ReadAllText(file);
if (!org.EndsWith('\n'))
File.AppendAllLines(file, ["", pattern]);
else
File.AppendAllLines(file, [pattern]);
}

View file

@ -11,7 +11,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"diff 4b825dc642cb6eb9a060e54bf8d69288fbee4904 {commit} --numstat -- \"{path}\"";
Args = $"diff {Models.Commit.EmptyTreeSHA1} {commit} --numstat -- \"{path}\"";
RaiseError = false;
}

View file

@ -10,5 +10,10 @@
Context = repo;
Args = $"diff -a --ignore-cr-at-eol --check {opt}";
}
public bool Result()
{
return ReadToEnd().IsSuccess;
}
}
}

View file

@ -10,23 +10,15 @@ namespace SourceGit.Commands
[GeneratedRegex(@"^(.+)\s+([\w.]+)\s+\w+:(\d+)$")]
private static partial Regex REG_LOCK();
class SubCmd : Command
private class SubCmd : Command
{
public SubCmd(string repo, string args, Action<string> onProgress)
public SubCmd(string repo, string args, Models.ICommandLog log)
{
WorkingDirectory = repo;
Context = repo;
Args = args;
TraitErrorAsOutput = true;
_outputHandler = onProgress;
Log = log;
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
public LFS(string repo)
@ -44,35 +36,35 @@ namespace SourceGit.Commands
return content.Contains("git lfs pre-push");
}
public bool Install()
public bool Install(Models.ICommandLog log)
{
return new SubCmd(_repo, "lfs install --local", null).Exec();
return new SubCmd(_repo, "lfs install --local", log).Exec();
}
public bool Track(string pattern, bool isFilenameMode = false)
public bool Track(string pattern, bool isFilenameMode, Models.ICommandLog log)
{
var opt = isFilenameMode ? "--filename" : "";
return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", null).Exec();
return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", log).Exec();
}
public void Fetch(string remote, Action<string> outputHandler)
public void Fetch(string remote, Models.ICommandLog log)
{
new SubCmd(_repo, $"lfs fetch {remote}", outputHandler).Exec();
new SubCmd(_repo, $"lfs fetch {remote}", log).Exec();
}
public void Pull(string remote, Action<string> outputHandler)
public void Pull(string remote, Models.ICommandLog log)
{
new SubCmd(_repo, $"lfs pull {remote}", outputHandler).Exec();
new SubCmd(_repo, $"lfs pull {remote}", log).Exec();
}
public void Push(string remote, Action<string> outputHandler)
public void Push(string remote, Models.ICommandLog log)
{
new SubCmd(_repo, $"lfs push {remote}", outputHandler).Exec();
new SubCmd(_repo, $"lfs push {remote}", log).Exec();
}
public void Prune(Action<string> outputHandler)
public void Prune(Models.ICommandLog log)
{
new SubCmd(_repo, "lfs prune", outputHandler).Exec();
new SubCmd(_repo, "lfs prune", log).Exec();
}
public List<Models.LFSLock> Locks(string remote)
@ -101,21 +93,21 @@ namespace SourceGit.Commands
return locks;
}
public bool Lock(string remote, string file)
public bool Lock(string remote, string file, Models.ICommandLog log)
{
return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", null).Exec();
return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", log).Exec();
}
public bool Unlock(string remote, string file, bool force)
public bool Unlock(string remote, string file, bool force, Models.ICommandLog log)
{
var opt = force ? "-f" : "";
return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", null).Exec();
return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", log).Exec();
}
public bool Unlock(string remote, long id, bool force)
public bool Unlock(string remote, long id, bool force, Models.ICommandLog log)
{
var opt = force ? "-f" : "";
return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", null).Exec();
return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", log).Exec();
}
private readonly string _repo;

View file

@ -1,26 +1,30 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
public class Merge : Command
{
public Merge(string repo, string source, string mode, Action<string> outputHandler)
public Merge(string repo, string source, string mode, bool edit)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
Args = $"merge --progress {source} {mode}";
Editor = EditorType.CoreEditor;
var builder = new StringBuilder();
builder.Append("merge --progress ");
builder.Append(edit ? "--edit " : "--no-edit ");
builder.Append(source);
builder.Append(' ');
builder.Append(mode);
Args = builder.ToString();
}
public Merge(string repo, List<string> targets, bool autoCommit, string strategy, Action<string> outputHandler)
public Merge(string repo, List<string> targets, bool autoCommit, string strategy)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
var builder = new StringBuilder();
builder.Append("merge --progress ");
@ -37,12 +41,5 @@ namespace SourceGit.Commands
Args = builder.ToString();
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler = null;
}
}

View file

@ -24,7 +24,7 @@ namespace SourceGit.Commands
if (!File.Exists(toolPath))
{
Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT found external merge tool in '{toolPath}'!"));
Dispatcher.UIThread.Post(() => App.RaiseException(repo, $"Can NOT find external merge tool in '{toolPath}'!"));
return false;
}
@ -54,7 +54,7 @@ namespace SourceGit.Commands
if (!File.Exists(toolPath))
{
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT found external diff tool in '{toolPath}'!"));
Dispatcher.UIThread.Invoke(() => App.RaiseException(repo, $"Can NOT find external diff tool in '{toolPath}'!"));
return false;
}

View file

@ -1,32 +1,18 @@
using System;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public class Pull : Command
{
public Pull(string repo, string remote, string branch, bool useRebase, bool noTags, Action<string> outputHandler)
public Pull(string repo, string remote, string branch, bool useRebase)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "pull --verbose --progress ";
if (useRebase)
Args += "--rebase=true ";
if (noTags)
Args += "--no-tags ";
Args += $"{remote} {branch}";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler;
}
}

View file

@ -1,16 +1,11 @@
using System;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public class Push : Command
{
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force, Action<string> onProgress)
public Push(string repo, string local, string remote, string remoteBranch, bool withTags, bool checkSubmodules, bool track, bool force)
{
_outputHandler = onProgress;
WorkingDirectory = repo;
Context = repo;
TraitErrorAsOutput = true;
SSHKey = new Config(repo).Get($"remote.{remote}.sshkey");
Args = "push --progress --verbose ";
@ -38,12 +33,5 @@ namespace SourceGit.Commands
Args += $"{remote} {refname}";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private readonly Action<string> _outputHandler = null;
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryAssumeUnchangedFiles : Command
{
[GeneratedRegex(@"^(\w)\s+(.+)$")]
private static partial Regex REG_PARSE();
public QueryAssumeUnchangedFiles(string repo)
{
WorkingDirectory = repo;
Args = "ls-files -v";
RaiseError = false;
}
public List<string> Result()
{
var outs = new List<string>();
var rs = ReadToEnd();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_PARSE().Match(line);
if (!match.Success)
continue;
if (match.Groups[1].Value == "h")
outs.Add(match.Groups[2].Value);
}
return outs;
}
}
}

View file

@ -14,18 +14,20 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = "branch -l --all -v --format=\"%(refname)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\"";
Args = "branch -l --all -v --format=\"%(refname)%00%(committerdate:unix)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\"";
}
public List<Models.Branch> Result()
public List<Models.Branch> Result(out int localBranchesCount)
{
localBranchesCount = 0;
var branches = new List<Models.Branch>();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return branches;
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var remoteBranches = new HashSet<string>();
var remoteHeads = new Dictionary<string, string>();
foreach (var line in lines)
{
var b = ParseLine(line);
@ -33,14 +35,27 @@ namespace SourceGit.Commands
{
branches.Add(b);
if (!b.IsLocal)
remoteBranches.Add(b.FullName);
remoteHeads.Add(b.FullName, b.Head);
else
localBranchesCount++;
}
}
foreach (var b in branches)
{
if (b.IsLocal && !string.IsNullOrEmpty(b.Upstream))
b.IsUpsteamGone = !remoteBranches.Contains(b.Upstream);
{
if (remoteHeads.TryGetValue(b.Upstream, out var upstreamHead))
{
b.IsUpstreamGone = false;
b.TrackStatus ??= new QueryTrackStatus(WorkingDirectory, b.Head, upstreamHead).Result();
}
else
{
b.IsUpstreamGone = true;
b.TrackStatus ??= new Models.BranchTrackStatus();
}
}
}
return branches;
@ -49,7 +64,7 @@ namespace SourceGit.Commands
private Models.Branch ParseLine(string line)
{
var parts = line.Split('\0');
if (parts.Length != 5)
if (parts.Length != 6)
return null;
var branch = new Models.Branch();
@ -83,14 +98,16 @@ namespace SourceGit.Commands
}
branch.FullName = refName;
branch.Head = parts[1];
branch.IsCurrent = parts[2] == "*";
branch.Upstream = parts[3];
branch.IsUpsteamGone = false;
branch.CommitterDate = ulong.Parse(parts[1]);
branch.Head = parts[2];
branch.IsCurrent = parts[3] == "*";
branch.Upstream = parts[4];
branch.IsUpstreamGone = false;
if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal))
branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result();
else
if (!branch.IsLocal ||
string.IsNullOrEmpty(branch.Upstream) ||
string.IsNullOrEmpty(parts[5]) ||
parts[5].Equals("=", StringComparison.Ordinal))
branch.TrackStatus = new Models.BranchTrackStatus();
return branch;

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace SourceGit.Commands
{
@ -14,17 +15,21 @@ namespace SourceGit.Commands
public List<string> Result()
{
Exec();
return _lines;
}
var rs = ReadToEnd();
var outs = new List<string>();
if (rs.IsSuccess)
{
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains(_commit))
outs.Add(line.Substring(0, 40));
}
}
protected override void OnReadline(string line)
{
if (line.Contains(_commit))
_lines.Add(line.Substring(0, 40));
return outs;
}
private string _commit;
private List<string> _lines = new List<string>();
}
}

View file

@ -26,11 +26,7 @@ namespace SourceGit.Commands
{
search += $"-i --committer=\"{filter}\"";
}
else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
}
else
else if (method == Models.CommitSearchMethod.ByMessage)
{
var argsBuilder = new StringBuilder();
argsBuilder.Append(search);
@ -45,10 +41,18 @@ namespace SourceGit.Commands
search = argsBuilder.ToString();
}
else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
}
else
{
search = $"-G\"{filter}\"";
}
WorkingDirectory = repo;
Context = repo;
Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s " + search;
Args = $"log -1000 --date-order --no-show-signature --decorate=full --format=%H%n%P%n%D%n%aN±%aE%n%at%n%cN±%cE%n%ct%n%s {search}";
_findFirstMerged = false;
}

View file

@ -90,6 +90,6 @@ namespace SourceGit.Commands
private List<Models.InteractiveCommit> _commits = [];
private Models.InteractiveCommit _current = null;
private string _boundary = "";
private readonly string _boundary;
}
}

View file

@ -35,5 +35,39 @@ namespace SourceGit.Commands
return stream;
}
public static Stream FromLFS(string repo, string oid, long size)
{
var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitExecutable;
starter.Arguments = $"lfs smudge";
starter.UseShellExecute = false;
starter.CreateNoWindow = true;
starter.WindowStyle = ProcessWindowStyle.Hidden;
starter.RedirectStandardInput = true;
starter.RedirectStandardOutput = true;
var stream = new MemoryStream();
try
{
var proc = new Process() { StartInfo = starter };
proc.Start();
proc.StandardInput.WriteLine("version https://git-lfs.github.com/spec/v1");
proc.StandardInput.WriteLine($"oid sha256:{oid}");
proc.StandardInput.WriteLine($"size {size}");
proc.StandardOutput.BaseStream.CopyTo(stream);
proc.WaitForExit();
proc.Close();
stream.Position = 0;
}
catch (Exception e)
{
App.RaiseException(repo, $"Failed to query file content: {e}");
}
return stream;
}
}
}

View file

@ -16,9 +16,6 @@ namespace SourceGit.Commands
public long Result()
{
if (_result != 0)
return _result;
var rs = ReadToEnd();
if (rs.IsSuccess)
{
@ -29,7 +26,5 @@ namespace SourceGit.Commands
return 0;
}
private readonly long _result = 0;
}
}

View file

@ -1,6 +1,9 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Avalonia.Threading;
namespace SourceGit.Commands
{
public partial class QueryLocalChanges : Command
@ -18,139 +21,145 @@ namespace SourceGit.Commands
public List<Models.Change> Result()
{
Exec();
return _changes;
}
protected override void OnReadline(string line)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
return;
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status)
var outs = new List<Models.Change>();
var rs = ReadToEnd();
if (!rs.IsSuccess)
{
case " M":
change.Set(Models.ChangeState.None, Models.ChangeState.Modified);
break;
case " T":
change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged);
break;
case " A":
change.Set(Models.ChangeState.None, Models.ChangeState.Added);
break;
case " D":
change.Set(Models.ChangeState.None, Models.ChangeState.Deleted);
break;
case " R":
change.Set(Models.ChangeState.None, Models.ChangeState.Renamed);
break;
case " C":
change.Set(Models.ChangeState.None, Models.ChangeState.Copied);
break;
case "M":
change.Set(Models.ChangeState.Modified);
break;
case "MM":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified);
break;
case "MT":
change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged);
break;
case "MD":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted);
break;
case "T":
change.Set(Models.ChangeState.TypeChanged);
break;
case "TM":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified);
break;
case "TT":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged);
break;
case "TD":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted);
break;
case "A":
change.Set(Models.ChangeState.Added);
break;
case "AM":
change.Set(Models.ChangeState.Added, Models.ChangeState.Modified);
break;
case "AT":
change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged);
break;
case "AD":
change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted);
break;
case "D":
change.Set(Models.ChangeState.Deleted);
break;
case "R":
change.Set(Models.ChangeState.Renamed);
break;
case "RM":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified);
break;
case "RT":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged);
break;
case "RD":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted);
break;
case "C":
change.Set(Models.ChangeState.Copied);
break;
case "CM":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified);
break;
case "CT":
change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged);
break;
case "CD":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted);
break;
case "DR":
change.Set(Models.ChangeState.Deleted, Models.ChangeState.Renamed);
break;
case "DC":
change.Set(Models.ChangeState.Deleted, Models.ChangeState.Copied);
break;
case "DD":
change.Set(Models.ChangeState.Deleted, Models.ChangeState.Deleted);
break;
case "AU":
change.Set(Models.ChangeState.Added, Models.ChangeState.Unmerged);
break;
case "UD":
change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Deleted);
break;
case "UA":
change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Added);
break;
case "DU":
change.Set(Models.ChangeState.Deleted, Models.ChangeState.Unmerged);
break;
case "AA":
change.Set(Models.ChangeState.Added, Models.ChangeState.Added);
break;
case "UU":
change.Set(Models.ChangeState.Unmerged, Models.ChangeState.Unmerged);
break;
case "??":
change.Set(Models.ChangeState.Untracked, Models.ChangeState.Untracked);
break;
default:
return;
Dispatcher.UIThread.Post(() => App.RaiseException(Context, rs.StdErr));
return outs;
}
_changes.Add(change);
}
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
continue;
private readonly List<Models.Change> _changes = new List<Models.Change>();
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status)
{
case " M":
change.Set(Models.ChangeState.None, Models.ChangeState.Modified);
break;
case " T":
change.Set(Models.ChangeState.None, Models.ChangeState.TypeChanged);
break;
case " A":
change.Set(Models.ChangeState.None, Models.ChangeState.Added);
break;
case " D":
change.Set(Models.ChangeState.None, Models.ChangeState.Deleted);
break;
case " R":
change.Set(Models.ChangeState.None, Models.ChangeState.Renamed);
break;
case " C":
change.Set(Models.ChangeState.None, Models.ChangeState.Copied);
break;
case "M":
change.Set(Models.ChangeState.Modified);
break;
case "MM":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Modified);
break;
case "MT":
change.Set(Models.ChangeState.Modified, Models.ChangeState.TypeChanged);
break;
case "MD":
change.Set(Models.ChangeState.Modified, Models.ChangeState.Deleted);
break;
case "T":
change.Set(Models.ChangeState.TypeChanged);
break;
case "TM":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Modified);
break;
case "TT":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.TypeChanged);
break;
case "TD":
change.Set(Models.ChangeState.TypeChanged, Models.ChangeState.Deleted);
break;
case "A":
change.Set(Models.ChangeState.Added);
break;
case "AM":
change.Set(Models.ChangeState.Added, Models.ChangeState.Modified);
break;
case "AT":
change.Set(Models.ChangeState.Added, Models.ChangeState.TypeChanged);
break;
case "AD":
change.Set(Models.ChangeState.Added, Models.ChangeState.Deleted);
break;
case "D":
change.Set(Models.ChangeState.Deleted);
break;
case "R":
change.Set(Models.ChangeState.Renamed);
break;
case "RM":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Modified);
break;
case "RT":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.TypeChanged);
break;
case "RD":
change.Set(Models.ChangeState.Renamed, Models.ChangeState.Deleted);
break;
case "C":
change.Set(Models.ChangeState.Copied);
break;
case "CM":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Modified);
break;
case "CT":
change.Set(Models.ChangeState.Copied, Models.ChangeState.TypeChanged);
break;
case "CD":
change.Set(Models.ChangeState.Copied, Models.ChangeState.Deleted);
break;
case "DD":
change.ConflictReason = Models.ConflictReason.BothDeleted;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "AU":
change.ConflictReason = Models.ConflictReason.AddedByUs;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "UD":
change.ConflictReason = Models.ConflictReason.DeletedByThem;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "UA":
change.ConflictReason = Models.ConflictReason.AddedByThem;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "DU":
change.ConflictReason = Models.ConflictReason.DeletedByUs;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "AA":
change.ConflictReason = Models.ConflictReason.BothAdded;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "UU":
change.ConflictReason = Models.ConflictReason.BothModified;
change.Set(Models.ChangeState.None, Models.ChangeState.Conflicted);
break;
case "??":
change.Set(Models.ChangeState.None, Models.ChangeState.Untracked);
break;
}
if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None)
outs.Add(change);
}
return outs;
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
@ -17,27 +18,31 @@ namespace SourceGit.Commands
public List<Models.Remote> Result()
{
Exec();
return _loaded;
}
var outs = new List<Models.Remote>();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return outs;
protected override void OnReadline(string line)
{
var match = REG_REMOTE().Match(line);
if (!match.Success)
return;
var remote = new Models.Remote()
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
Name = match.Groups[1].Value,
URL = match.Groups[2].Value,
};
var match = REG_REMOTE().Match(line);
if (!match.Success)
continue;
if (_loaded.Find(x => x.Name == remote.Name) != null)
return;
_loaded.Add(remote);
var remote = new Models.Remote()
{
Name = match.Groups[1].Value,
URL = match.Groups[2].Value,
};
if (outs.Find(x => x.Name == remote.Name) != null)
continue;
outs.Add(remote);
}
return outs;
}
private readonly List<Models.Remote> _loaded = new List<Models.Remote>();
}
}

View file

@ -6,87 +6,87 @@ namespace SourceGit.Commands
{
public partial class QueryStagedChangesWithAmend : Command
{
[GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMTUX])\d{0,6}\t(.*)$")]
[GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMT])\d{0,6}\t(.*)$")]
private static partial Regex REG_FORMAT1();
[GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")]
private static partial Regex REG_FORMAT2();
public QueryStagedChangesWithAmend(string repo)
public QueryStagedChangesWithAmend(string repo, string parent)
{
WorkingDirectory = repo;
Context = repo;
Args = "diff-index --cached -M HEAD^";
Args = $"diff-index --cached -M {parent}";
_parent = parent;
}
public List<Models.Change> Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
if (!rs.IsSuccess)
return [];
var changes = new List<Models.Change>();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var changes = new List<Models.Change>();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
var match = REG_FORMAT2().Match(line);
if (match.Success)
{
var match = REG_FORMAT2().Match(line);
if (match.Success)
var change = new Models.Change()
{
var change = new Models.Change()
Path = match.Groups[3].Value,
DataForAmend = new Models.ChangeDataForAmend()
{
Path = match.Groups[3].Value,
DataForAmend = new Models.ChangeDataForAmend()
{
FileMode = match.Groups[1].Value,
ObjectHash = match.Groups[2].Value,
},
};
change.Set(Models.ChangeState.Renamed);
changes.Add(change);
continue;
}
match = REG_FORMAT1().Match(line);
if (match.Success)
{
var change = new Models.Change()
{
Path = match.Groups[4].Value,
DataForAmend = new Models.ChangeDataForAmend()
{
FileMode = match.Groups[1].Value,
ObjectHash = match.Groups[2].Value,
},
};
var type = match.Groups[3].Value;
switch (type)
{
case "A":
change.Set(Models.ChangeState.Added);
break;
case "C":
change.Set(Models.ChangeState.Copied);
break;
case "D":
change.Set(Models.ChangeState.Deleted);
break;
case "M":
change.Set(Models.ChangeState.Modified);
break;
case "T":
change.Set(Models.ChangeState.TypeChanged);
break;
case "U":
change.Set(Models.ChangeState.Unmerged);
break;
}
changes.Add(change);
}
FileMode = match.Groups[1].Value,
ObjectHash = match.Groups[2].Value,
ParentSHA = _parent,
},
};
change.Set(Models.ChangeState.Renamed);
changes.Add(change);
continue;
}
return changes;
match = REG_FORMAT1().Match(line);
if (match.Success)
{
var change = new Models.Change()
{
Path = match.Groups[4].Value,
DataForAmend = new Models.ChangeDataForAmend()
{
FileMode = match.Groups[1].Value,
ObjectHash = match.Groups[2].Value,
ParentSHA = _parent,
},
};
var type = match.Groups[3].Value;
switch (type)
{
case "A":
change.Set(Models.ChangeState.Added);
break;
case "C":
change.Set(Models.ChangeState.Copied);
break;
case "D":
change.Set(Models.ChangeState.Deleted);
break;
case "M":
change.Set(Models.ChangeState.Modified);
break;
case "T":
change.Set(Models.ChangeState.TypeChanged);
break;
}
changes.Add(change);
}
}
return [];
return changes;
}
private readonly string _parent;
}
}

View file

@ -1,76 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
/// <summary>
/// Query stash changes. Requires git >= 2.32.0
/// </summary>
public partial class QueryStashChanges : Command
{
[GeneratedRegex(@"^([MADC])\s+(.+)$")]
private static partial Regex REG_FORMAT();
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")]
private static partial Regex REG_RENAME_FORMAT();
public QueryStashChanges(string repo, string stash)
{
WorkingDirectory = repo;
Context = repo;
Args = $"stash show -u --name-status \"{stash}\"";
}
public List<Models.Change> Result()
{
var rs = ReadToEnd();
if (!rs.IsSuccess)
return [];
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var outs = new List<Models.Change>();
foreach (var line in lines)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)
{
match = REG_RENAME_FORMAT().Match(line);
if (match.Success)
{
var renamed = new Models.Change() { Path = match.Groups[1].Value };
renamed.Set(Models.ChangeState.Renamed);
outs.Add(renamed);
}
continue;
}
var change = new Models.Change() { Path = match.Groups[2].Value };
var status = match.Groups[1].Value;
switch (status[0])
{
case 'M':
change.Set(Models.ChangeState.Modified);
outs.Add(change);
break;
case 'A':
change.Set(Models.ChangeState.Added);
outs.Add(change);
break;
case 'D':
change.Set(Models.ChangeState.Deleted);
outs.Add(change);
break;
case 'C':
change.Set(Models.ChangeState.Copied);
outs.Add(change);
break;
}
}
outs.Sort((l, r) => string.Compare(l.Path, r.Path, StringComparison.Ordinal));
return outs;
}
}
}

View file

@ -9,52 +9,60 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = "stash list --format=%H%n%P%n%ct%n%gd%n%s";
Args = $"stash list -z --no-show-signature --format=\"%H%n%P%n%ct%n%gd%n%B\"";
}
public List<Models.Stash> Result()
{
Exec();
return _stashes;
}
var outs = new List<Models.Stash>();
var rs = ReadToEnd();
if (!rs.IsSuccess)
return outs;
protected override void OnReadline(string line)
{
switch (_nextLineIdx)
var items = rs.StdOut.Split('\0', StringSplitOptions.RemoveEmptyEntries);
foreach (var item in items)
{
case 0:
_current = new Models.Stash() { SHA = line };
_stashes.Add(_current);
break;
case 1:
ParseParent(line);
break;
case 2:
_current.Time = ulong.Parse(line);
break;
case 3:
_current.Name = line;
break;
case 4:
_current.Message = line;
break;
var current = new Models.Stash();
var nextPartIdx = 0;
var start = 0;
var end = item.IndexOf('\n', start);
while (end > 0 && nextPartIdx < 4)
{
var line = item.Substring(start, end - start);
switch (nextPartIdx)
{
case 0:
current.SHA = line;
break;
case 1:
if (line.Length > 6)
current.Parents.AddRange(line.Split(' ', StringSplitOptions.RemoveEmptyEntries));
break;
case 2:
current.Time = ulong.Parse(line);
break;
case 3:
current.Name = line;
break;
}
nextPartIdx++;
start = end + 1;
if (start >= item.Length - 1)
break;
end = item.IndexOf('\n', start);
}
if (start < item.Length)
current.Message = item.Substring(start);
outs.Add(current);
}
_nextLineIdx++;
if (_nextLineIdx > 4)
_nextLineIdx = 0;
return outs;
}
private void ParseParent(string data)
{
if (data.Length < 8)
return;
_current.Parents.AddRange(data.Split(separator: ' ', options: StringSplitOptions.RemoveEmptyEntries));
}
private readonly List<Models.Stash> _stashes = new List<Models.Stash>();
private Models.Stash _current = null;
private int _nextLineIdx = 0;
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
@ -6,12 +7,12 @@ namespace SourceGit.Commands
{
public partial class QuerySubmodules : Command
{
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)\s\(.*\)$")]
private static partial Regex REG_FORMAT1();
[GeneratedRegex(@"^[\-\+ ][0-9a-f]+\s(.*)$")]
private static partial Regex REG_FORMAT2();
[GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")]
[GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")]
private static partial Regex REG_FORMAT_STATUS();
[GeneratedRegex(@"^\s?[\w\?]{1,4}\s+(.+)$")]
private static partial Regex REG_FORMAT_DIRTY();
[GeneratedRegex(@"^submodule\.(\S*)\.(\w+)=(.*)$")]
private static partial Regex REG_FORMAT_MODULE_INFO();
public QuerySubmodules(string repo)
{
@ -25,52 +26,117 @@ namespace SourceGit.Commands
var submodules = new List<Models.Submodule>();
var rs = ReadToEnd();
var builder = new StringBuilder();
var lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries);
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
var map = new Dictionary<string, Models.Submodule>();
var needCheckLocalChanges = false;
foreach (var line in lines)
{
var match = REG_FORMAT1().Match(line);
var match = REG_FORMAT_STATUS().Match(line);
if (match.Success)
{
var path = match.Groups[1].Value;
builder.Append($"\"{path}\" ");
submodules.Add(new Models.Submodule() { Path = path });
continue;
}
var stat = match.Groups[1].Value;
var sha = match.Groups[2].Value;
var path = match.Groups[3].Value;
match = REG_FORMAT2().Match(line);
if (match.Success)
{
var path = match.Groups[1].Value;
builder.Append($"\"{path}\" ");
submodules.Add(new Models.Submodule() { Path = path });
var module = new Models.Submodule() { Path = path, SHA = sha };
switch (stat[0])
{
case '-':
module.Status = Models.SubmoduleStatus.NotInited;
break;
case '+':
module.Status = Models.SubmoduleStatus.RevisionChanged;
break;
case 'U':
module.Status = Models.SubmoduleStatus.Unmerged;
break;
default:
module.Status = Models.SubmoduleStatus.Normal;
needCheckLocalChanges = true;
break;
}
map.Add(path, module);
submodules.Add(module);
}
}
if (submodules.Count > 0)
{
Args = $"--no-optional-locks status -uno --porcelain -- {builder}";
Args = "config --file .gitmodules --list";
rs = ReadToEnd();
if (rs.IsSuccess)
{
var modules = new Dictionary<string, ModuleInfo>();
lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT_MODULE_INFO().Match(line);
if (match.Success)
{
var name = match.Groups[1].Value;
var key = match.Groups[2].Value;
var val = match.Groups[3].Value;
if (!modules.TryGetValue(name, out var m))
{
m = new ModuleInfo();
modules.Add(name, m);
}
if (key.Equals("path", StringComparison.Ordinal))
m.Path = val;
else if (key.Equals("url", StringComparison.Ordinal))
m.URL = val;
}
}
foreach (var kv in modules)
{
if (map.TryGetValue(kv.Value.Path, out var m))
m.URL = kv.Value.URL;
}
}
}
if (needCheckLocalChanges)
{
var builder = new StringBuilder();
foreach (var kv in map)
{
if (kv.Value.Status == Models.SubmoduleStatus.Normal)
{
builder.Append('"');
builder.Append(kv.Key);
builder.Append("\" ");
}
}
Args = $"--no-optional-locks status --porcelain -- {builder}";
rs = ReadToEnd();
if (!rs.IsSuccess)
return submodules;
var dirty = new HashSet<string>();
lines = rs.StdOut.Split(['\r', '\n'], System.StringSplitOptions.RemoveEmptyEntries);
lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT_STATUS().Match(line);
var match = REG_FORMAT_DIRTY().Match(line);
if (match.Success)
{
var path = match.Groups[1].Value;
dirty.Add(path);
if (map.TryGetValue(path, out var m))
m.Status = Models.SubmoduleStatus.Modified;
}
}
foreach (var submodule in submodules)
submodule.IsDirty = dirty.Contains(submodule.Path);
}
return submodules;
}
private class ModuleInfo
{
public string Path { get; set; } = string.Empty;
public string URL { get; set; } = string.Empty;
}
}
}

View file

@ -11,7 +11,7 @@ namespace SourceGit.Commands
Context = repo;
WorkingDirectory = repo;
Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\"";
Args = $"tag -l --format=\"{_boundary}%(refname)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(creatordate:unix)%00%(contents:subject)%0a%0a%(contents:body)\"";
}
public List<Models.Tag> Result()
@ -24,17 +24,22 @@ namespace SourceGit.Commands
var records = rs.StdOut.Split(_boundary, StringSplitOptions.RemoveEmptyEntries);
foreach (var record in records)
{
var subs = record.Split('\0', StringSplitOptions.None);
if (subs.Length != 5)
var subs = record.Split('\0');
if (subs.Length != 6)
continue;
var message = subs[4].Trim();
var name = subs[0].Substring(10);
var message = subs[5].Trim();
if (!string.IsNullOrEmpty(message) && message.Equals(name, StringComparison.Ordinal))
message = null;
tags.Add(new Models.Tag()
{
Name = subs[0].Substring(10),
SHA = string.IsNullOrEmpty(subs[2]) ? subs[1] : subs[2],
CreatorDate = ulong.Parse(subs[3]),
Message = string.IsNullOrEmpty(message) ? null : message,
Name = name,
IsAnnotated = subs[1].Equals("tag", StringComparison.Ordinal),
SHA = string.IsNullOrEmpty(subs[3]) ? subs[2] : subs[3],
CreatorDate = ulong.Parse(subs[4]),
Message = message,
});
}

View file

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SourceGit.Commands
{
public partial class QueryUpdatableSubmodules : Command
{
[GeneratedRegex(@"^([U\-\+ ])([0-9a-f]+)\s(.*?)(\s\(.*\))?$")]
private static partial Regex REG_FORMAT_STATUS();
public QueryUpdatableSubmodules(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "submodule status";
}
public List<string> Result()
{
var submodules = new List<string>();
var rs = ReadToEnd();
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var match = REG_FORMAT_STATUS().Match(line);
if (match.Success)
{
var stat = match.Groups[1].Value;
var path = match.Groups[3].Value;
if (!stat.StartsWith(' '))
submodules.Add(path);
}
}
return submodules;
}
}
}

View file

@ -1,33 +1,7 @@
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
namespace SourceGit.Commands
{
public class Reset : Command
{
public Reset(string repo)
{
WorkingDirectory = repo;
Context = repo;
Args = "reset";
}
public Reset(string repo, List<Models.Change> changes)
{
WorkingDirectory = repo;
Context = repo;
var builder = new StringBuilder();
builder.Append("reset --");
foreach (var c in changes)
{
builder.Append(" \"");
builder.Append(c.Path);
builder.Append("\"");
}
Args = builder.ToString();
}
public Reset(string repo, string revision, string mode)
{
WorkingDirectory = repo;

View file

@ -1,29 +1,52 @@
using System.Collections.Generic;
using System.Text;
using System.Text;
namespace SourceGit.Commands
{
public class Restore : Command
{
public Restore(string repo)
/// <summary>
/// Only used for single staged change.
/// </summary>
/// <param name="repo"></param>
/// <param name="stagedChange"></param>
public Restore(string repo, Models.Change stagedChange)
{
WorkingDirectory = repo;
Context = repo;
Args = "restore . --source=HEAD --staged --worktree --recurse-submodules";
var builder = new StringBuilder();
builder.Append("restore --staged -- \"");
builder.Append(stagedChange.Path);
builder.Append('"');
if (stagedChange.Index == Models.ChangeState.Renamed)
{
builder.Append(" \"");
builder.Append(stagedChange.OriginalPath);
builder.Append('"');
}
Args = builder.ToString();
}
public Restore(string repo, List<string> files, string extra)
/// <summary>
/// Restore changes given in a path-spec file.
/// </summary>
/// <param name="repo"></param>
/// <param name="pathspecFile"></param>
/// <param name="isStaged"></param>
public Restore(string repo, string pathspecFile, bool isStaged)
{
WorkingDirectory = repo;
Context = repo;
StringBuilder builder = new StringBuilder();
var builder = new StringBuilder();
builder.Append("restore ");
if (!string.IsNullOrEmpty(extra))
builder.Append(extra).Append(" ");
builder.Append("--");
foreach (var f in files)
builder.Append(' ').Append('"').Append(f).Append('"');
builder.Append(isStaged ? "--staged " : "--worktree --recurse-submodules ");
builder.Append("--pathspec-from-file=\"");
builder.Append(pathspecFile);
builder.Append('"');
Args = builder.ToString();
}
}

View file

@ -10,15 +10,15 @@ namespace SourceGit.Commands
{
public static void Run(string repo, string revision, string file, string saveTo)
{
var dir = Path.GetDirectoryName(saveTo);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result();
if (isLFSFiltered)
{
var tmpFile = saveTo + ".tmp";
if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile))
{
ExecCmd(repo, $"lfs smudge", saveTo, tmpFile);
}
File.Delete(tmpFile);
var pointerStream = QueryFileContent.Run(repo, revision, file);
ExecCmd(repo, $"lfs smudge", saveTo, pointerStream);
}
else
{
@ -26,7 +26,7 @@ namespace SourceGit.Commands
}
}
private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null)
private static void ExecCmd(string repo, string args, string outputFile, Stream input = null)
{
var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo;
@ -45,27 +45,11 @@ namespace SourceGit.Commands
{
var proc = new Process() { StartInfo = starter };
proc.Start();
if (inputFile != null)
{
using (StreamReader sr = new StreamReader(inputFile))
{
while (true)
{
var line = sr.ReadLine();
if (line == null)
break;
proc.StandardInput.WriteLine(line);
}
}
}
if (input != null)
proc.StandardInput.Write(new StreamReader(input).ReadToEnd());
proc.StandardOutput.BaseStream.CopyTo(sw);
proc.WaitForExit();
var rs = proc.ExitCode == 0;
proc.Close();
return rs;
}
catch (Exception e)
{
@ -73,7 +57,6 @@ namespace SourceGit.Commands
{
App.RaiseException(repo, "Save file failed: " + e.Message);
});
return false;
}
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
namespace SourceGit.Commands
{
@ -40,7 +40,7 @@ namespace SourceGit.Commands
if (dateEndIdx == -1)
return;
var dateStr = line.Substring(0, dateEndIdx);
var dateStr = line.AsSpan(0, dateEndIdx);
if (double.TryParse(dateStr, out var date))
statistics.AddCommit(line.Substring(dateEndIdx + 1), date);
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SourceGit.Commands
{
@ -10,10 +11,9 @@ namespace SourceGit.Commands
Context = repo;
}
public bool Add(string url, string relativePath, bool recursive, Action<string> outputHandler)
public bool Add(string url, string relativePath, bool recursive)
{
_outputHandler = outputHandler;
Args = $"submodule add {url} \"{relativePath}\"";
Args = $"-c protocol.file.allow=always submodule add \"{url}\" \"{relativePath}\"";
if (!Exec())
return false;
@ -29,38 +29,38 @@ namespace SourceGit.Commands
}
}
public bool Update(string module, bool init, bool recursive, bool useRemote, Action<string> outputHandler)
public bool Update(List<string> modules, bool init, bool recursive, bool useRemote = false)
{
Args = "submodule update";
var builder = new StringBuilder();
builder.Append("submodule update");
if (init)
Args += " --init";
builder.Append(" --init");
if (recursive)
Args += " --recursive";
builder.Append(" --recursive");
if (useRemote)
Args += " --remote";
if (!string.IsNullOrEmpty(module))
Args += $" -- \"{module}\"";
builder.Append(" --remote");
if (modules.Count > 0)
{
builder.Append(" --");
foreach (var module in modules)
builder.Append($" \"{module}\"");
}
_outputHandler = outputHandler;
Args = builder.ToString();
return Exec();
}
public bool Delete(string relativePath)
public bool Deinit(string module, bool force)
{
Args = $"submodule deinit -f \"{relativePath}\"";
if (!Exec())
return false;
Args = $"rm -rf \"{relativePath}\"";
Args = force ? $"submodule deinit -f -- \"{module}\"" : $"submodule deinit -- \"{module}\"";
return Exec();
}
protected override void OnReadline(string line)
public bool Delete(string module)
{
_outputHandler?.Invoke(line);
Args = $"rm -rf \"{module}\"";
return Exec();
}
private Action<string> _outputHandler;
}
}

View file

@ -1,57 +1,51 @@
using System.Collections.Generic;
using System.IO;
using System.IO;
namespace SourceGit.Commands
{
public static class Tag
{
public static bool Add(string repo, string name, string basedOn)
public static bool Add(string repo, string name, string basedOn, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"tag {name} {basedOn}";
cmd.Args = $"tag --no-sign {name} {basedOn}";
cmd.Log = log;
return cmd.Exec();
}
public static bool Add(string repo, string name, string basedOn, string message, bool sign)
public static bool Add(string repo, string name, string basedOn, string message, bool sign, Models.ICommandLog log)
{
var param = sign ? "--sign -a" : "--no-sign -a";
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"tag {param} {name} {basedOn} ";
cmd.Log = log;
if (!string.IsNullOrEmpty(message))
{
string tmp = Path.GetTempFileName();
File.WriteAllText(tmp, message);
cmd.Args += $"-F \"{tmp}\"";
}
else
{
cmd.Args += $"-m {name}";
var succ = cmd.Exec();
File.Delete(tmp);
return succ;
}
cmd.Args += $"-m {name}";
return cmd.Exec();
}
public static bool Delete(string repo, string name, List<Models.Remote> remotes)
public static bool Delete(string repo, string name, Models.ICommandLog log)
{
var cmd = new Command();
cmd.WorkingDirectory = repo;
cmd.Context = repo;
cmd.Args = $"tag --delete {name}";
if (!cmd.Exec())
return false;
if (remotes != null)
{
foreach (var r in remotes)
new Push(repo, r.Name, $"refs/tags/{name}", true).Exec();
}
return true;
cmd.Log = log;
return cmd.Exec();
}
}
}

View file

@ -23,13 +23,11 @@ namespace SourceGit.Commands
_patchBuilder.Append(c.DataForAmend.ObjectHash);
_patchBuilder.Append("\t");
_patchBuilder.Append(c.OriginalPath);
_patchBuilder.Append("\n");
}
else if (c.Index == Models.ChangeState.Added)
{
_patchBuilder.Append("0 0000000000000000000000000000000000000000\t");
_patchBuilder.Append(c.Path);
_patchBuilder.Append("\n");
}
else if (c.Index == Models.ChangeState.Deleted)
{
@ -37,7 +35,6 @@ namespace SourceGit.Commands
_patchBuilder.Append(c.DataForAmend.ObjectHash);
_patchBuilder.Append("\t");
_patchBuilder.Append(c.Path);
_patchBuilder.Append("\n");
}
else
{
@ -46,8 +43,9 @@ namespace SourceGit.Commands
_patchBuilder.Append(c.DataForAmend.ObjectHash);
_patchBuilder.Append("\t");
_patchBuilder.Append(c.Path);
_patchBuilder.Append("\n");
}
_patchBuilder.Append("\n");
}
}

View file

@ -1,23 +0,0 @@
using System;
namespace SourceGit.Commands
{
public class UpdateRef : Command
{
public UpdateRef(string repo, string refName, string toRevision, Action<string> outputHandler)
{
_outputHandler = outputHandler;
WorkingDirectory = repo;
Context = repo;
Args = $"update-ref {refName} {toRevision}";
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler;
}
}

View file

@ -56,7 +56,7 @@ namespace SourceGit.Commands
return worktrees;
}
public bool Add(string fullpath, string name, bool createNew, string tracking, Action<string> outputHandler)
public bool Add(string fullpath, string name, bool createNew, string tracking)
{
Args = "worktree add ";
@ -78,14 +78,12 @@ namespace SourceGit.Commands
else if (!string.IsNullOrEmpty(name) && !createNew)
Args += name;
_outputHandler = outputHandler;
return Exec();
}
public bool Prune(Action<string> outputHandler)
public bool Prune()
{
Args = "worktree prune -v";
_outputHandler = outputHandler;
return Exec();
}
@ -101,22 +99,14 @@ namespace SourceGit.Commands
return Exec();
}
public bool Remove(string fullpath, bool force, Action<string> outputHandler)
public bool Remove(string fullpath, bool force)
{
if (force)
Args = $"worktree remove -f \"{fullpath}\"";
else
Args = $"worktree remove \"{fullpath}\"";
_outputHandler = outputHandler;
return Exec();
}
protected override void OnReadline(string line)
{
_outputHandler?.Invoke(line);
}
private Action<string> _outputHandler = null;
}
}

View file

@ -7,8 +7,11 @@ namespace SourceGit.Converters
{
public static class ListConverters
{
public static readonly FuncValueConverter<IList, string> Count =
new FuncValueConverter<IList, string>(v => v == null ? "0" : $"{v.Count}");
public static readonly FuncValueConverter<IList, string> ToCount =
new FuncValueConverter<IList, string>(v => v == null ? " (0)" : $" ({v.Count})");
new FuncValueConverter<IList, string>(v => v == null ? "(0)" : $"({v.Count})");
public static readonly FuncValueConverter<IList, bool> IsNullOrEmpty =
new FuncValueConverter<IList, bool>(v => v == null || v.Count == 0);

View file

@ -0,0 +1,27 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace SourceGit.Converters
{
public static class ObjectConverters
{
public class IsTypeOfConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || parameter == null)
return false;
return value.GetType().IsAssignableTo((Type)parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return new NotImplementedException();
}
}
public static readonly IsTypeOfConverter IsTypeOf = new IsTypeOfConverter();
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using Avalonia.Data.Converters;
@ -22,7 +22,7 @@ namespace SourceGit.Converters
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length;
if (v.StartsWith(home, StringComparison.Ordinal))
return "~" + v.Substring(prefixLen);
return $"~{v.AsSpan(prefixLen)}";
return v;
});

View file

@ -17,7 +17,7 @@ namespace SourceGit.Models
{
public interface IAvatarHost
{
void OnAvatarResourceChanged(string email);
void OnAvatarResourceChanged(string email, Bitmap image);
}
public partial class AvatarManager
@ -26,10 +26,7 @@ namespace SourceGit.Models
{
get
{
if (_instance == null)
_instance = new AvatarManager();
return _instance;
return _instance ??= new AvatarManager();
}
}
@ -38,7 +35,7 @@ namespace SourceGit.Models
[GeneratedRegex(@"^(?:(\d+)\+)?(.+?)@.+\.github\.com$")]
private static partial Regex REG_GITHUB_USER_EMAIL();
private object _synclock = new object();
private readonly Lock _synclock = new();
private string _storePath;
private List<IAvatarHost> _avatars = new List<IAvatarHost>();
private Dictionary<string, Bitmap> _resources = new Dictionary<string, Bitmap>();
@ -119,7 +116,7 @@ namespace SourceGit.Models
Dispatcher.UIThread.InvokeAsync(() =>
{
_resources[email] = img;
NotifyResourceChanged(email);
NotifyResourceChanged(email, img);
});
}
@ -144,14 +141,13 @@ namespace SourceGit.Models
if (_defaultAvatars.Contains(email))
return null;
if (_resources.ContainsKey(email))
_resources.Remove(email);
_resources.Remove(email);
var localFile = Path.Combine(_storePath, GetEmailHash(email));
if (File.Exists(localFile))
File.Delete(localFile);
NotifyResourceChanged(email);
NotifyResourceChanged(email, null);
}
else
{
@ -179,13 +175,40 @@ namespace SourceGit.Models
lock (_synclock)
{
if (!_requesting.Contains(email))
_requesting.Add(email);
_requesting.Add(email);
}
return null;
}
public void SetFromLocal(string email, string file)
{
try
{
Bitmap image = null;
using (var stream = File.OpenRead(file))
{
image = Bitmap.DecodeToWidth(stream, 128);
}
if (image == null)
return;
_resources[email] = image;
_requesting.Remove(email);
var store = Path.Combine(_storePath, GetEmailHash(email));
File.Copy(file, store, true);
NotifyResourceChanged(email, image);
}
catch
{
// ignore
}
}
private void LoadDefaultAvatar(string key, string img)
{
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/{img}", UriKind.RelativeOrAbsolute));
@ -196,19 +219,17 @@ namespace SourceGit.Models
private string GetEmailHash(string email)
{
var lowered = email.ToLower(CultureInfo.CurrentCulture).Trim();
var hash = MD5.HashData(Encoding.Default.GetBytes(lowered).AsSpan());
var hash = MD5.HashData(Encoding.Default.GetBytes(lowered));
var builder = new StringBuilder(hash.Length * 2);
foreach (var c in hash)
builder.Append(c.ToString("x2"));
return builder.ToString();
}
private void NotifyResourceChanged(string email)
private void NotifyResourceChanged(string email, Bitmap image)
{
foreach (var avatar in _avatars)
{
avatar.OnAvatarResourceChanged(email);
}
avatar.OnAvatarResourceChanged(email, image);
}
}
}

35
src/Models/Bisect.cs Normal file
View file

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
namespace SourceGit.Models
{
public enum BisectState
{
None = 0,
WaitingForRange,
Detecting,
}
[Flags]
public enum BisectCommitFlag
{
None = 0,
Good = 1 << 0,
Bad = 1 << 1,
}
public class Bisect
{
public HashSet<string> Bads
{
get;
set;
} = [];
public HashSet<string> Goods
{
get;
set;
} = [];
}
}

View file

@ -23,10 +23,17 @@ namespace SourceGit.Models
}
}
public enum BranchSortMode
{
Name = 0,
CommitterDate,
}
public class Branch
{
public string Name { get; set; }
public string FullName { get; set; }
public ulong CommitterDate { get; set; }
public string Head { get; set; }
public bool IsLocal { get; set; }
public bool IsCurrent { get; set; }
@ -34,7 +41,7 @@ namespace SourceGit.Models
public string Upstream { get; set; }
public BranchTrackStatus TrackStatus { get; set; }
public string Remote { get; set; }
public bool IsUpsteamGone { get; set; }
public bool IsUpstreamGone { get; set; }
public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}";
}

View file

@ -18,14 +18,27 @@ namespace SourceGit.Models
Deleted,
Renamed,
Copied,
Unmerged,
Untracked
Untracked,
Conflicted,
}
public enum ConflictReason
{
None,
BothDeleted,
AddedByUs,
DeletedByThem,
AddedByThem,
DeletedByUs,
BothAdded,
BothModified,
}
public class ChangeDataForAmend
{
public string FileMode { get; set; } = "";
public string ObjectHash { get; set; } = "";
public string ParentSHA { get; set; } = "";
}
public class Change
@ -35,20 +48,14 @@ namespace SourceGit.Models
public string Path { get; set; } = "";
public string OriginalPath { get; set; } = "";
public ChangeDataForAmend DataForAmend { get; set; } = null;
public ConflictReason ConflictReason { get; set; } = ConflictReason.None;
public bool IsConflict
{
get
{
if (Index == ChangeState.Unmerged || WorkTree == ChangeState.Unmerged)
return true;
if (Index == ChangeState.Added && WorkTree == ChangeState.Added)
return true;
if (Index == ChangeState.Deleted && WorkTree == ChangeState.Deleted)
return true;
return false;
}
}
public bool IsConflicted => WorkTree == ChangeState.Conflicted;
public string ConflictMarker => CONFLICT_MARKERS[(int)ConflictReason];
public string ConflictDesc => CONFLICT_DESCS[(int)ConflictReason];
public string WorkTreeDesc => TYPE_DESCS[(int)WorkTree];
public string IndexDesc => TYPE_DESCS[(int)Index];
public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
{
@ -76,8 +83,44 @@ namespace SourceGit.Models
if (Path[0] == '"')
Path = Path.Substring(1, Path.Length - 2);
if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"')
OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2);
}
private static readonly string[] TYPE_DESCS =
[
"Unknown",
"Modified",
"Type Changed",
"Added",
"Deleted",
"Renamed",
"Copied",
"Untracked",
"Conflict"
];
private static readonly string[] CONFLICT_MARKERS =
[
string.Empty,
"DD",
"AU",
"UD",
"UA",
"DU",
"AA",
"UU"
];
private static readonly string[] CONFLICT_DESCS =
[
string.Empty,
"Both deleted",
"Added by us",
"Deleted by them",
"Added by them",
"Deleted by us",
"Both added",
"Both modified"
];
}
}

View file

@ -13,10 +13,14 @@ namespace SourceGit.Models
ByCommitter,
ByMessage,
ByFile,
ByContent,
}
public class Commit
{
// As retrieved by: git mktree </dev/null
public const string EmptyTreeSHA1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
public static double OpacityForNotMerged
{
get;
@ -29,14 +33,14 @@ namespace SourceGit.Models
public User Committer { get; set; } = User.Invalid;
public ulong CommitterTime { get; set; } = 0;
public string Subject { get; set; } = string.Empty;
public List<string> Parents { get; set; } = new List<string>();
public List<Decorator> Decorators { get; set; } = new List<Decorator>();
public List<string> Parents { get; set; } = new();
public List<Decorator> Decorators { get; set; } = new();
public bool HasDecorators => Decorators.Count > 0;
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateTime);
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Actived.DateOnly);
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime);
public string CommitterTimeStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime);
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly);
public string CommitterTimeShortStr => DateTime.UnixEpoch.AddSeconds(CommitterTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly);
public bool IsMerged { get; set; } = false;
public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime;
@ -45,7 +49,7 @@ namespace SourceGit.Models
public int Color { get; set; } = 0;
public double Opacity => IsMerged ? 1 : OpacityForNotMerged;
public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular;
public Thickness Margin { get; set; } = new Thickness(0);
public Thickness Margin { get; set; } = new(0);
public IBrush Brush => CommitGraph.Pens[Color].Brush;
public void ParseDecorators(string data)
@ -109,7 +113,7 @@ namespace SourceGit.Models
if (l.Type != r.Type)
return (int)l.Type - (int)r.Type;
else
return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
return NumericSort.Compare(l.Name, r.Name);
});
}
}
@ -117,6 +121,6 @@ namespace SourceGit.Models
public class CommitFullMessage
{
public string Message { get; set; } = string.Empty;
public List<Hyperlink> Links { get; set; } = [];
public InlineElementCollector Inlines { get; set; } = new();
}
}

View file

@ -64,8 +64,8 @@ namespace SourceGit.Models
{
const double unitWidth = 12;
const double halfWidth = 6;
const double unitHeight = 28;
const double halfHeight = 14;
const double unitHeight = 1;
const double halfHeight = 0.5;
var temp = new CommitGraph();
var unsolved = new List<PathHelper>();

View file

@ -1,8 +1,49 @@
namespace SourceGit.Models
using System;
using System.Collections.Generic;
namespace SourceGit.Models
{
public class CommitLink
{
public string Name { get; set; } = null;
public string URLPrefix { get; set; } = null;
public CommitLink(string name, string prefix)
{
Name = name;
URLPrefix = prefix;
}
public static List<CommitLink> Get(List<Remote> remotes)
{
var outs = new List<CommitLink>();
foreach (var remote in remotes)
{
if (remote.TryGetVisitURL(out var url))
{
var trimmedUrl = url.AsSpan();
if (url.EndsWith(".git"))
trimmedUrl = url.AsSpan(0, url.Length - 4);
if (url.StartsWith("https://github.com/", StringComparison.Ordinal))
outs.Add(new($"Github ({trimmedUrl.Slice(19)})", $"{url}/commit/"));
else if (url.StartsWith("https://gitlab.", StringComparison.Ordinal))
outs.Add(new($"GitLab ({trimmedUrl.Slice(trimmedUrl.Slice(15).IndexOf('/') + 16)})", $"{url}/-/commit/"));
else if (url.StartsWith("https://gitee.com/", StringComparison.Ordinal))
outs.Add(new($"Gitee ({trimmedUrl.Slice(18)})", $"{url}/commit/"));
else if (url.StartsWith("https://bitbucket.org/", StringComparison.Ordinal))
outs.Add(new($"BitBucket ({trimmedUrl.Slice(22)})", $"{url}/commits/"));
else if (url.StartsWith("https://codeberg.org/", StringComparison.Ordinal))
outs.Add(new($"Codeberg ({trimmedUrl.Slice(21)})", $"{url}/commit/"));
else if (url.StartsWith("https://gitea.org/", StringComparison.Ordinal))
outs.Add(new($"Gitea ({trimmedUrl.Slice(18)})", $"{url}/commit/"));
else if (url.StartsWith("https://git.sr.ht/", StringComparison.Ordinal))
outs.Add(new($"sourcehut ({trimmedUrl.Slice(18)})", $"{url}/commit/"));
}
}
return outs;
}
}
}

View file

@ -4,7 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Models
{
public partial class CommitTemplate : ObservableObject
public class CommitTemplate : ObservableObject
{
public string Name
{

View file

@ -4,23 +4,28 @@ namespace SourceGit.Models
{
public class ConventionalCommitType
{
public string Type { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Name { get; set; }
public string Type { get; set; }
public string Description { get; set; }
public static readonly List<ConventionalCommitType> Supported = new List<ConventionalCommitType>()
{
new ConventionalCommitType("feat", "Adding a new feature"),
new ConventionalCommitType("fix", "Fixing a bug"),
new ConventionalCommitType("docs", "Updating documentation"),
new ConventionalCommitType("style", "Elements or code styles without changing the code logic"),
new ConventionalCommitType("test", "Adding or updating tests"),
new ConventionalCommitType("chore", "Making changes to the build process or auxiliary tools and libraries"),
new ConventionalCommitType("revert", "Undoing a previous commit"),
new ConventionalCommitType("refactor", "Restructuring code without changing its external behavior")
};
public static readonly List<ConventionalCommitType> Supported = [
new("Features", "feat", "Adding a new feature"),
new("Bug Fixes", "fix", "Fixing a bug"),
new("Work In Progress", "wip", "Still being developed and not yet complete"),
new("Reverts", "revert", "Undoing a previous commit"),
new("Code Refactoring", "refactor", "Restructuring code without changing its external behavior"),
new("Performance Improvements", "perf", "Improves performance"),
new("Builds", "build", "Changes that affect the build system or external dependencies"),
new("Continuous Integrations", "ci", "Changes to CI configuration files and scripts"),
new("Documentations", "docs", "Updating documentation"),
new("Styles", "style", "Elements or code styles without changing the code logic"),
new("Tests", "test", "Adding or updating tests"),
new("Chores", "chore", "Other changes that don't modify src or test files"),
];
public ConventionalCommitType(string type, string description)
public ConventionalCommitType(string name, string type, string description)
{
Name = name;
Type = type;
Description = description;
}

19
src/Models/Count.cs Normal file
View file

@ -0,0 +1,19 @@
using System;
namespace SourceGit.Models
{
public class Count : IDisposable
{
public int Value { get; set; } = 0;
public Count(int value)
{
Value = value;
}
public void Dispose()
{
// Ignore
}
}
}

View file

@ -25,7 +25,7 @@ namespace SourceGit.Models
set;
} = 0;
public static DateTimeFormat Actived
public static DateTimeFormat Active
{
get => Supported[ActiveIndex];
}

View file

@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace SourceGit.Models
{
public class DealWithChangesAfterStashing
{
public string Label { get; set; }
public string Desc { get; set; }
public static readonly List<DealWithChangesAfterStashing> Supported = [
new ("Discard", "All (or selected) changes will be discarded"),
new ("Keep Index", "Staged changes are left intact"),
new ("Keep All", "All (or selected) changes are left intact"),
];
public DealWithChangesAfterStashing(string label, string desc)
{
Label = label;
Desc = desc;
}
}
}

View file

@ -5,6 +5,15 @@ namespace SourceGit.Models
{
public class DiffOption
{
/// <summary>
/// Enable `--ignore-cr-at-eol` by default?
/// </summary>
public static bool IgnoreCRAtEOL
{
get;
set;
} = true;
public Change WorkingCopyChange => _workingCopyChange;
public bool IsUnstaged => _isUnstaged;
public List<string> Revisions => _revisions;
@ -40,7 +49,7 @@ namespace SourceGit.Models
else
{
if (change.DataForAmend != null)
_extra = "--cached HEAD^";
_extra = $"--cached {change.DataForAmend.ParentSHA}";
else
_extra = "--cached";
@ -56,7 +65,7 @@ namespace SourceGit.Models
/// <param name="change"></param>
public DiffOption(Commit commit, Change change)
{
var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^";
_revisions.Add(baseRevision);
_revisions.Add(commit.SHA);
_path = change.Path;
@ -70,7 +79,7 @@ namespace SourceGit.Models
/// <param name="file"></param>
public DiffOption(Commit commit, string file)
{
var baseRevision = commit.Parents.Count == 0 ? "4b825dc642cb6eb9a060e54bf8d69288fbee4904" : $"{commit.SHA}^";
var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^";
_revisions.Add(baseRevision);
_revisions.Add(commit.SHA);
_path = file;
@ -115,6 +124,6 @@ namespace SourceGit.Models
private readonly string _path;
private readonly string _orgPath = string.Empty;
private readonly string _extra = string.Empty;
private readonly List<string> _revisions = new List<string>();
private readonly List<string> _revisions = [];
}
}

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
@ -16,11 +16,10 @@ namespace SourceGit.Models
Deleted,
}
public class TextInlineRange
public class TextInlineRange(int p, int n)
{
public int Start { get; set; }
public int End { get; set; }
public TextInlineRange(int p, int n) { Start = p; End = p + n - 1; }
public int Start { get; set; } = p;
public int End { get; set; } = p + n - 1;
}
public class TextDiffLine
@ -30,6 +29,7 @@ namespace SourceGit.Models
public int OldLineNumber { get; set; } = 0;
public int NewLineNumber { get; set; } = 0;
public List<TextInlineRange> Highlights { get; set; } = new List<TextInlineRange>();
public bool NoNewLineEndOfFile { get; set; } = false;
public string OldLine => OldLineNumber == 0 ? string.Empty : OldLineNumber.ToString();
public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString();
@ -146,7 +146,7 @@ namespace SourceGit.Models
public void GenerateNewPatchFromSelection(Change change, string fileBlobGuid, TextDiffSelection selection, bool revert, string output)
{
var isTracked = !string.IsNullOrEmpty(fileBlobGuid);
var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000";
var fileGuid = isTracked ? fileBlobGuid : "00000000";
var builder = new StringBuilder();
builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n');
@ -555,7 +555,7 @@ namespace SourceGit.Models
}
else if (test.Type == TextDiffLineType.Added)
{
if (i < start - 1)
if (i < start - 1 || isOldSide)
{
if (revert)
{
@ -565,18 +565,7 @@ namespace SourceGit.Models
}
else
{
if (isOldSide)
{
if (revert)
{
newCount++;
oldCount++;
}
}
else
{
newCount++;
}
newCount++;
}
if (i == end - 1 && tailed)
@ -654,9 +643,7 @@ namespace SourceGit.Models
public string NewImageSize => New != null ? $"{New.PixelSize.Width} x {New.PixelSize.Height}" : "0 x 0";
}
public class NoOrEOLChange
{
}
public class NoOrEOLChange;
public class FileModeDiff
{

12
src/Models/DirtyState.cs Normal file
View file

@ -0,0 +1,12 @@
using System;
namespace SourceGit.Models
{
[Flags]
public enum DirtyState
{
None = 0,
HasLocalChanges = 1 << 0,
HasPendingPullOrPush = 1 << 1,
}
}

View file

@ -43,6 +43,7 @@ namespace SourceGit.Models
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\""),
new ExternalMerger(10, "plastic_merge", "Plastic SCM", "mergetool.exe", "-s=\"$REMOTE\" -b=\"$BASE\" -d=\"$LOCAL\" -r=\"$MERGED\" --automatic", "-s=\"$LOCAL\" -d=\"$REMOTE\""),
new ExternalMerger(11, "meld", "Meld", "Meld.exe", "\"$LOCAL\" \"$BASE\" \"$REMOTE\" --output \"$MERGED\"", "\"$LOCAL\" \"$REMOTE\""),
};
}
else if (OperatingSystem.IsMacOS())

View file

@ -107,8 +107,7 @@ namespace SourceGit.Models
// Ignore
}
if (_customPaths == null)
_customPaths = new ExternalToolPaths();
_customPaths ??= new ExternalToolPaths();
}
public void TryAdd(string name, string icon, Func<string> finder, Func<string, string> execArgsGenerator = null)

46
src/Models/GitFlow.cs Normal file
View file

@ -0,0 +1,46 @@
namespace SourceGit.Models
{
public enum GitFlowBranchType
{
None = 0,
Feature,
Release,
Hotfix,
}
public class GitFlow
{
public string Master { get; set; } = string.Empty;
public string Develop { get; set; } = string.Empty;
public string FeaturePrefix { get; set; } = string.Empty;
public string ReleasePrefix { get; set; } = string.Empty;
public string HotfixPrefix { get; set; } = string.Empty;
public bool IsValid
{
get
{
return !string.IsNullOrEmpty(Master) &&
!string.IsNullOrEmpty(Develop) &&
!string.IsNullOrEmpty(FeaturePrefix) &&
!string.IsNullOrEmpty(ReleasePrefix) &&
!string.IsNullOrEmpty(HotfixPrefix);
}
}
public string GetPrefix(GitFlowBranchType type)
{
switch (type)
{
case GitFlowBranchType.Feature:
return FeaturePrefix;
case GitFlowBranchType.Release:
return ReleasePrefix;
case GitFlowBranchType.Hotfix:
return HotfixPrefix;
default:
return string.Empty;
}
}
}
}

View file

@ -5,26 +5,16 @@
/// <summary>
/// The minimal version of Git that required by this app.
/// </summary>
public static readonly System.Version MINIMAL = new System.Version(2, 23, 0);
/// <summary>
/// The minimal version of Git that supports the `add` command with the `--pathspec-from-file` option.
/// </summary>
public static readonly System.Version ADD_WITH_PATHSPECFILE = new System.Version(2, 25, 0);
public static readonly System.Version MINIMAL = new(2, 25, 1);
/// <summary>
/// The minimal version of Git that supports the `stash push` command with the `--pathspec-from-file` option.
/// </summary>
public static readonly System.Version STASH_PUSH_WITH_PATHSPECFILE = new System.Version(2, 26, 0);
public static readonly System.Version STASH_PUSH_WITH_PATHSPECFILE = new(2, 26, 0);
/// <summary>
/// The minimal version of Git that supports the `stash push` command with the `--staged` option.
/// </summary>
public static readonly System.Version STASH_PUSH_ONLY_STAGED = new System.Version(2, 35, 0);
/// <summary>
/// The minimal version of Git that supports the `stash show` command with the `-u` option.
/// </summary>
public static readonly System.Version STASH_SHOW_WITH_UNTRACKED = new System.Version(2, 32, 0);
public static readonly System.Version STASH_PUSH_ONLY_STAGED = new(2, 35, 0);
}
}

View file

@ -1,29 +0,0 @@
namespace SourceGit.Models
{
public class Hyperlink
{
public int Start { get; set; } = 0;
public int Length { get; set; } = 0;
public string Link { get; set; } = "";
public bool IsCommitSHA { get; set; } = false;
public Hyperlink(int start, int length, string link, bool isCommitSHA = false)
{
Start = start;
Length = length;
Link = link;
IsCommitSHA = isCommitSHA;
}
public bool Intersect(int start, int length)
{
if (start == Start)
return true;
if (start < Start)
return start + length > Start;
return start < Start + Length;
}
}
}

View file

@ -0,0 +1,7 @@
namespace SourceGit.Models
{
public interface ICommandLog
{
void AppendLine(string line);
}
}

View file

@ -2,6 +2,8 @@
{
public interface IRepository
{
bool MayHaveSubmodules();
void RefreshBranches();
void RefreshWorktrees();
void RefreshTags();

View file

@ -0,0 +1,10 @@
namespace SourceGit.Models
{
public enum ImageDecoder
{
None = 0,
Builtin,
Pfim,
Tiff,
}
}

View file

@ -0,0 +1,37 @@
namespace SourceGit.Models
{
public enum InlineElementType
{
Keyword = 0,
Link,
CommitSHA,
Code,
}
public class InlineElement
{
public InlineElementType Type { get; }
public int Start { get; }
public int Length { get; }
public string Link { get; }
public InlineElement(InlineElementType type, int start, int length, string link)
{
Type = type;
Start = start;
Length = length;
Link = link;
}
public bool IsIntersecting(int start, int length)
{
if (start == Start)
return true;
if (start < Start)
return start + length > Start;
return start < Start + Length;
}
}
}

View file

@ -0,0 +1,38 @@
using System.Collections.Generic;
namespace SourceGit.Models
{
public class InlineElementCollector
{
public int Count => _implementation.Count;
public InlineElement this[int index] => _implementation[index];
public InlineElement Intersect(int start, int length)
{
foreach (var elem in _implementation)
{
if (elem.IsIntersecting(start, length))
return elem;
}
return null;
}
public void Add(InlineElement element)
{
_implementation.Add(element);
}
public void Sort()
{
_implementation.Sort((l, r) => l.Start.CompareTo(r.Start));
}
public void Clear()
{
_implementation.Clear();
}
private readonly List<InlineElement> _implementation = [];
}
}

View file

@ -27,6 +27,8 @@ namespace SourceGit.Models
public class InteractiveRebaseJobCollection
{
public string OrigHead { get; set; } = string.Empty;
public string Onto { get; set; } = string.Empty;
public List<InteractiveRebaseJob> Jobs { get; set; } = new List<InteractiveRebaseJob>();
}
}

100
src/Models/IpcChannel.cs Normal file
View file

@ -0,0 +1,100 @@
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
namespace SourceGit.Models
{
public class IpcChannel : IDisposable
{
public bool IsFirstInstance { get; }
public event Action<string> MessageReceived;
public IpcChannel()
{
try
{
_singletonLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
IsFirstInstance = true;
_server = new NamedPipeServerStream(
"SourceGitIPCChannel" + Environment.UserName,
PipeDirection.In,
-1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
_cancellationTokenSource = new CancellationTokenSource();
Task.Run(StartServer);
}
catch
{
IsFirstInstance = false;
}
}
public void SendToFirstInstance(string cmd)
{
try
{
using (var client = new NamedPipeClientStream(".", "SourceGitIPCChannel" + Environment.UserName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly))
{
client.Connect(1000);
if (!client.IsConnected)
return;
using (var writer = new StreamWriter(client))
{
writer.WriteLine(cmd);
writer.Flush();
}
if (OperatingSystem.IsWindows())
client.WaitForPipeDrain();
else
Thread.Sleep(1000);
}
}
catch
{
// IGNORE
}
}
public void Dispose()
{
_cancellationTokenSource?.Cancel();
_singletonLock?.Dispose();
}
private async void StartServer()
{
using var reader = new StreamReader(_server);
while (!_cancellationTokenSource.IsCancellationRequested)
{
try
{
await _server.WaitForConnectionAsync(_cancellationTokenSource.Token);
if (!_cancellationTokenSource.IsCancellationRequested)
{
var line = await reader.ReadToEndAsync(_cancellationTokenSource.Token);
MessageReceived?.Invoke(line?.Trim());
}
_server.Disconnect();
}
catch
{
if (!_cancellationTokenSource.IsCancellationRequested && _server.IsConnected)
_server.Disconnect();
}
}
}
private FileStream _singletonLock = null;
private NamedPipeServerStream _server = null;
private CancellationTokenSource _cancellationTokenSource = null;
}
}

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
@ -46,7 +45,7 @@ namespace SourceGit.Models
set => SetProperty(ref _urlTemplate, value);
}
public void Matches(List<Hyperlink> outs, string message)
public void Matches(InlineElementCollector outs, string message)
{
if (_regex == null || string.IsNullOrEmpty(_urlTemplate))
return;
@ -60,17 +59,7 @@ namespace SourceGit.Models
var start = match.Index;
var len = match.Length;
var intersect = false;
foreach (var exist in outs)
{
if (exist.Intersect(start, len))
{
intersect = true;
break;
}
}
if (intersect)
if (outs.Intersect(start, len) != null)
continue;
var link = _urlTemplate;
@ -81,8 +70,7 @@ namespace SourceGit.Models
link = link.Replace($"${j}", group.Value);
}
var range = new Hyperlink(start, len, link);
outs.Add(range);
outs.Add(new InlineElement(InlineElementType.Link, start, len, link));
}
}

View file

@ -1,8 +1,22 @@
namespace SourceGit.Models
using System.Text.RegularExpressions;
namespace SourceGit.Models
{
public class LFSObject
public partial class LFSObject
{
[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_FORMAT();
public string Oid { get; set; } = string.Empty;
public long Size { get; set; } = 0;
public static LFSObject Parse(string content)
{
var match = REG_FORMAT().Match(content);
if (match.Success)
return new() { Oid = match.Groups[1].Value, Size = long.Parse(match.Groups[2].Value) };
return null;
}
}
}

View file

@ -14,6 +14,7 @@ namespace SourceGit.Models
new Locale("Français", "fr_FR"),
new Locale("Italiano", "it_IT"),
new Locale("Português (Brasil)", "pt_BR"),
new Locale("Українська", "uk_UA"),
new Locale("Русский", "ru_RU"),
new Locale("简体中文", "zh_CN"),
new Locale("繁體中文", "zh_TW"),

View file

@ -1,6 +1,4 @@
namespace SourceGit.Models
{
public class Null
{
}
public class Null;
}

View file

@ -1,4 +1,6 @@
namespace SourceGit.Models
using System;
namespace SourceGit.Models
{
public static class NumericSort
{
@ -10,52 +12,35 @@
int marker1 = 0;
int marker2 = 0;
char[] tmp1 = new char[len1];
char[] tmp2 = new char[len2];
while (marker1 < len1 && marker2 < len2)
{
char c1 = s1[marker1];
char c2 = s2[marker2];
int loc1 = 0;
int loc2 = 0;
bool isDigit1 = char.IsDigit(c1);
bool isDigit2 = char.IsDigit(c2);
if (isDigit1 != isDigit2)
return c1.CompareTo(c2);
do
{
tmp1[loc1] = c1;
loc1++;
marker1++;
int subLen1 = 1;
while (marker1 + subLen1 < len1 && char.IsDigit(s1[marker1 + subLen1]) == isDigit1)
subLen1++;
if (marker1 < len1)
c1 = s1[marker1];
else
break;
} while (char.IsDigit(c1) == isDigit1);
int subLen2 = 1;
while (marker2 + subLen2 < len2 && char.IsDigit(s2[marker2 + subLen2]) == isDigit2)
subLen2++;
do
{
tmp2[loc2] = c2;
loc2++;
marker2++;
string sub1 = s1.Substring(marker1, subLen1);
string sub2 = s2.Substring(marker2, subLen2);
if (marker2 < len2)
c2 = s2[marker2];
else
break;
} while (char.IsDigit(c2) == isDigit2);
marker1 += subLen1;
marker2 += subLen2;
string sub1 = new string(tmp1, 0, loc1);
string sub2 = new string(tmp2, 0, loc2);
int result;
if (isDigit1)
result = loc1 == loc2 ? string.CompareOrdinal(sub1, sub2) : loc1 - loc2;
result = (subLen1 == subLen2) ? string.CompareOrdinal(sub1, sub2) : (subLen1 - subLen2);
else
result = string.CompareOrdinal(sub1, sub2);
result = string.Compare(sub1, sub2, StringComparison.OrdinalIgnoreCase);
if (result != 0)
return result;

Some files were not shown because too many files have changed in this diff Show more