Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25d7f27d7a | ||
|
|
c5f058beb1 | ||
|
|
4181925047 | ||
|
|
927e0c3e92 | ||
|
|
0dac3e9f09 | ||
|
|
4f1131e855 | ||
|
|
f24c39c573 | ||
|
|
f487090b4b | ||
|
|
c8247f2b2a |
@@ -123,11 +123,17 @@ jobs:
|
|||||||
publish-arch:
|
publish-arch:
|
||||||
name: Build and publish AUR packages
|
name: Build and publish AUR packages
|
||||||
needs: publish-ubuntu
|
needs: publish-ubuntu
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
runs-on: archlinux
|
runs-on: archlinux
|
||||||
environment: production
|
environment: production
|
||||||
env:
|
env:
|
||||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
ARTIFACT_BASE_URL: ${{ vars.ARTIFACT_BASE_URL }}
|
GITEA_API: https://git.cbsk-tech.de/api/v1
|
||||||
|
OWNER: Christoph
|
||||||
|
REPO: GitLite
|
||||||
|
GITEA_TOKEN: ${{ secrets.ACTIONS_TOKEN }}
|
||||||
|
GITEA_FALLBACK_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -236,44 +242,121 @@ jobs:
|
|||||||
runuser -u builder -- \
|
runuser -u builder -- \
|
||||||
bash -lc "cd '$AUR_SOURCE_DIR' && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO"
|
bash -lc "cd '$AUR_SOURCE_DIR' && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO"
|
||||||
|
|
||||||
|
ARCH_PACKAGE_PATH="$(find "$AUR_SOURCE_DIR" -maxdepth 1 -type f -name 'gitty-desktop-*.pkg.tar.zst' -print -quit)"
|
||||||
|
if [ -z "$ARCH_PACKAGE_PATH" ]; then
|
||||||
|
echo "The native Arch package was not created" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "AUR_SOURCE_DIR=$AUR_SOURCE_DIR" >> "$GITHUB_ENV"
|
echo "AUR_SOURCE_DIR=$AUR_SOURCE_DIR" >> "$GITHUB_ENV"
|
||||||
|
echo "ARCH_PACKAGE_PATH=$ARCH_PACKAGE_PATH" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Attach native Arch package to Gitea release
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TOKEN="${GITEA_TOKEN:-${GITEA_FALLBACK_TOKEN:-}}"
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "A Gitea token is required to upload the native Arch package" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_JSON="$(curl --fail --location --silent --show-error \
|
||||||
|
--header "Authorization: token $TOKEN" \
|
||||||
|
"$GITEA_API/repos/$OWNER/$REPO/releases/tags/$RELEASE_TAG")"
|
||||||
|
export RELEASE_JSON
|
||||||
|
RELEASE_ID="$(node -e "const r=JSON.parse(process.env.RELEASE_JSON); if (!r.id) process.exit(1); process.stdout.write(String(r.id))")"
|
||||||
|
PACKAGE_NAME="${ARCH_PACKAGE_PATH##*/}"
|
||||||
|
export PACKAGE_NAME
|
||||||
|
|
||||||
|
if node -e "const r=JSON.parse(process.env.RELEASE_JSON); process.exit((r.assets || []).some(a => a.name === process.env.PACKAGE_NAME) ? 0 : 1)"; then
|
||||||
|
echo "Release asset $PACKAGE_NAME already exists; skipping upload."
|
||||||
|
else
|
||||||
|
curl --fail --location --silent --show-error \
|
||||||
|
--request POST \
|
||||||
|
--header "Authorization: token $TOKEN" \
|
||||||
|
--form "attachment=@$ARCH_PACKAGE_PATH;type=application/octet-stream" \
|
||||||
|
"$GITEA_API/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets?name=$PACKAGE_NAME"
|
||||||
|
echo
|
||||||
|
echo "Attached $PACKAGE_NAME to Gitea release $RELEASE_TAG."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Generate and build binary AUR package
|
- name: Generate and build binary AUR package
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
LATEST_JSON="$(curl --fail --location --silent --show-error \
|
TOKEN="${GITEA_TOKEN:-${GITEA_FALLBACK_TOKEN:-}}"
|
||||||
"$ARTIFACT_BASE_URL/gitty/latest.json")"
|
if [ -z "$TOKEN" ]; then
|
||||||
export LATEST_JSON
|
echo "A Gitea token is required to locate the native Arch package" >&2
|
||||||
APPIMAGE_URL="$(node -e "const p=JSON.parse(process.env.LATEST_JSON); const a=p.platforms?.['linux-x86_64']?.url; if (!a) process.exit(1); process.stdout.write(a)")"
|
exit 1
|
||||||
APPIMAGE_NAME="${APPIMAGE_URL##*/}"
|
fi
|
||||||
|
|
||||||
|
RELEASE_JSON="$(curl --fail --location --silent --show-error \
|
||||||
|
--header "Authorization: token $TOKEN" \
|
||||||
|
"$GITEA_API/repos/$OWNER/$REPO/releases/tags/$RELEASE_TAG")"
|
||||||
|
export RELEASE_JSON
|
||||||
|
PACKAGE_NAME="${ARCH_PACKAGE_PATH##*/}"
|
||||||
|
export PACKAGE_NAME
|
||||||
|
PACKAGE_URL="$(node -e "const r=JSON.parse(process.env.RELEASE_JSON); const a=(r.assets || []).find(a => a.name === process.env.PACKAGE_NAME); if (!a?.browser_download_url) process.exit(1); process.stdout.write(a.browser_download_url)")"
|
||||||
|
|
||||||
AUR_BIN_DIR="$(mktemp -d)"
|
AUR_BIN_DIR="$(mktemp -d)"
|
||||||
cp PKGBUILD-bin "$AUR_BIN_DIR/PKGBUILD"
|
cp PKGBUILD-bin "$AUR_BIN_DIR/PKGBUILD"
|
||||||
APPIMAGE_PATH="$AUR_BIN_DIR/$APPIMAGE_NAME"
|
PACKAGE_PATH="$AUR_BIN_DIR/$PACKAGE_NAME"
|
||||||
curl --fail --location --silent --show-error \
|
curl --fail --location --silent --show-error \
|
||||||
--output "$APPIMAGE_PATH" "$APPIMAGE_URL"
|
--header "Authorization: token $TOKEN" \
|
||||||
APPIMAGE_CHECKSUM="$(sha256sum "$APPIMAGE_PATH" | cut -d ' ' -f 1)"
|
--output "$PACKAGE_PATH" "$PACKAGE_URL"
|
||||||
ICON_PATH="$AUR_BIN_DIR/gitty-desktop.png"
|
PACKAGE_CHECKSUM="$(sha256sum "$PACKAGE_PATH" | cut -d ' ' -f 1)"
|
||||||
curl --fail --location --silent --show-error \
|
|
||||||
--output "$ICON_PATH" \
|
|
||||||
"https://git.cbsk-tech.de/Christoph/GitLite/raw/tag/$RELEASE_TAG/src-tauri/icons/icon.png"
|
|
||||||
ICON_CHECKSUM="$(sha256sum "$ICON_PATH" | cut -d ' ' -f 1)"
|
|
||||||
|
|
||||||
sed -i \
|
sed -i \
|
||||||
-e "s/^pkgver=.*/pkgver=$PACKAGE_VERSION/" \
|
-e "s/^pkgver=.*/pkgver=$PACKAGE_VERSION/" \
|
||||||
-e "s/^pkgrel=.*/pkgrel=1/" \
|
-e "s/^pkgrel=.*/pkgrel=1/" \
|
||||||
-e "s|^_appimage=.*|_appimage=\"$APPIMAGE_NAME\"|" \
|
-e "s|^_package=.*|_package=\"$PACKAGE_NAME\"|" \
|
||||||
-e "s|^_artifact_url=.*|_artifact_url=\"$APPIMAGE_URL\"|" \
|
-e "s|^_artifact_url=.*|_artifact_url=\"$PACKAGE_URL\"|" \
|
||||||
-e "s/^_tag=.*/_tag=$RELEASE_TAG/" \
|
-e "s/^sha256sums=.*/sha256sums=('$PACKAGE_CHECKSUM')/" \
|
||||||
-e "/^sha256sums=/,/^[[:space:]]*'SKIP')$/c\\sha256sums=('$APPIMAGE_CHECKSUM'\\n '$ICON_CHECKSUM')" \
|
|
||||||
"$AUR_BIN_DIR/PKGBUILD"
|
"$AUR_BIN_DIR/PKGBUILD"
|
||||||
|
|
||||||
chown -R builder:builder "$AUR_BIN_DIR"
|
chown -R builder:builder "$AUR_BIN_DIR"
|
||||||
runuser -u builder -- \
|
runuser -u builder -- \
|
||||||
bash -lc "cd '$AUR_BIN_DIR' && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO"
|
bash -lc "cd '$AUR_BIN_DIR' && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO"
|
||||||
|
|
||||||
|
ARCH_BIN_PACKAGE_PATH="$(find "$AUR_BIN_DIR" -maxdepth 1 -type f -name 'gitty-desktop-bin-*.pkg.tar.zst' -print -quit)"
|
||||||
|
if [ -z "$ARCH_BIN_PACKAGE_PATH" ]; then
|
||||||
|
echo "The binary Arch package was not created" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "AUR_BIN_DIR=$AUR_BIN_DIR" >> "$GITHUB_ENV"
|
echo "AUR_BIN_DIR=$AUR_BIN_DIR" >> "$GITHUB_ENV"
|
||||||
|
echo "ARCH_BIN_PACKAGE_PATH=$ARCH_BIN_PACKAGE_PATH" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Attach binary Arch package to Gitea release
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TOKEN="${GITEA_TOKEN:-${GITEA_FALLBACK_TOKEN:-}}"
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "A Gitea token is required to upload the Arch package" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_JSON="$(curl --fail --location --silent --show-error \
|
||||||
|
--header "Authorization: token $TOKEN" \
|
||||||
|
"$GITEA_API/repos/$OWNER/$REPO/releases/tags/$RELEASE_TAG")"
|
||||||
|
export RELEASE_JSON
|
||||||
|
RELEASE_ID="$(node -e "const r=JSON.parse(process.env.RELEASE_JSON); if (!r.id) process.exit(1); process.stdout.write(String(r.id))")"
|
||||||
|
PACKAGE_NAME="${ARCH_BIN_PACKAGE_PATH##*/}"
|
||||||
|
export PACKAGE_NAME
|
||||||
|
|
||||||
|
if node -e "const r=JSON.parse(process.env.RELEASE_JSON); process.exit((r.assets || []).some(a => a.name === process.env.PACKAGE_NAME) ? 0 : 1)"; then
|
||||||
|
echo "Release asset $PACKAGE_NAME already exists; skipping upload."
|
||||||
|
else
|
||||||
|
curl --fail --location --silent --show-error \
|
||||||
|
--request POST \
|
||||||
|
--header "Authorization: token $TOKEN" \
|
||||||
|
--form "attachment=@$ARCH_BIN_PACKAGE_PATH;type=application/octet-stream" \
|
||||||
|
"$GITEA_API/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets?name=$PACKAGE_NAME"
|
||||||
|
echo
|
||||||
|
echo "Attached $PACKAGE_NAME to Gitea release $RELEASE_TAG."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Publish PKGBUILD to AUR
|
- name: Publish PKGBUILD to AUR
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -4,6 +4,25 @@ All notable user-facing changes to Gitty are documented in this file.
|
|||||||
|
|
||||||
The project uses calendar-style versions in the form `YYYY.M.PATCH`.
|
The project uses calendar-style versions in the form `YYYY.M.PATCH`.
|
||||||
|
|
||||||
|
## [2026.8.4] - 2026-08-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Branch folders now have a context-menu action for deleting all contained
|
||||||
|
local or remote branches at once. The top-level remote folder such as
|
||||||
|
`origin` is protected, while its nested folders remain manageable. The
|
||||||
|
currently checked-out branch is kept,
|
||||||
|
and individual failures are reported after the remaining branches have been
|
||||||
|
processed.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The Compare selector is fully localized in German and uses the same visual
|
||||||
|
language as the external-tool selectors for its fields, groups, typography,
|
||||||
|
and dialog surfaces.
|
||||||
|
- Repository-tab close buttons are square and have clearer spacing, hover
|
||||||
|
behavior, and keyboard-focus feedback.
|
||||||
|
|
||||||
## [2026.8.3] - 2026-08-13
|
## [2026.8.3] - 2026-08-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -161,6 +180,7 @@ The project uses calendar-style versions in the form `YYYY.M.PATCH`.
|
|||||||
[2026.07.22]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.7.22
|
[2026.07.22]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.7.22
|
||||||
[2026.07.21]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.7.21
|
[2026.07.21]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.7.21
|
||||||
[2026.7.20]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.7.20
|
[2026.7.20]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.7.20
|
||||||
|
[2026.8.4]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.4
|
||||||
[2026.8.3]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.3
|
[2026.8.3]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.3
|
||||||
[2026.8.2]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.2
|
[2026.8.2]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.2
|
||||||
[2026.8.1]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.1
|
[2026.8.1]: https://git.cbsk-tech.de/Christoph/GitLite/releases/tag/2026.8.1
|
||||||
|
|||||||
+11
-31
@@ -1,45 +1,25 @@
|
|||||||
# Maintainer: Christoph Brandau <c.brandau91@googlemail.com>
|
# Maintainer: Christoph Brandau <c.brandau91@googlemail.com>
|
||||||
|
|
||||||
pkgname=gitty-desktop-bin
|
pkgname=gitty-desktop-bin
|
||||||
pkgver=2026.8.3
|
pkgver=2026.8.4
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="A lightweight, modern Git client built with Tauri (prebuilt AppImage)"
|
pkgdesc="A lightweight, modern Git client built with Tauri (prebuilt Arch package)"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
url="https://git.cbsk-tech.de/Christoph/GitLite"
|
url="https://git.cbsk-tech.de/Christoph/GitLite"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('fuse2' 'git' 'webkit2gtk-4.1' 'gtk3' 'hicolor-icon-theme' 'libappindicator-gtk3' 'librsvg' 'xdotool')
|
depends=('git' 'webkit2gtk-4.1' 'gtk3' 'hicolor-icon-theme' 'libappindicator-gtk3' 'librsvg' 'xdotool')
|
||||||
provides=('gitty-desktop')
|
provides=('gitty-desktop')
|
||||||
conflicts=('gitty-desktop')
|
conflicts=('gitty-desktop')
|
||||||
options=('!strip')
|
options=('!strip')
|
||||||
|
|
||||||
_appimage="Gitty_${pkgver}_amd64.AppImage"
|
_package="gitty-desktop-${pkgver}-1-x86_64.pkg.tar.zst"
|
||||||
_artifact_url="https://cdn.cbsk-tech.de/gitty/${pkgver}/${_appimage}"
|
_artifact_url="https://git.cbsk-tech.de/Christoph/GitLite/releases/download/${pkgver}/${_package}"
|
||||||
_tag=2026.8.3
|
source=("${_package}::${_artifact_url}")
|
||||||
source=("${_appimage}::${_artifact_url}"
|
noextract=("${_package}")
|
||||||
"gitty-desktop.png::${url}/raw/tag/${_tag}/src-tauri/icons/icon.png")
|
sha256sums=('SKIP')
|
||||||
sha256sums=('SKIP'
|
|
||||||
'SKIP')
|
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
install -Dm755 "$srcdir/$_appimage" \
|
# Extract only the native package payload, without carrying its package
|
||||||
"$pkgdir/opt/$pkgname/gitty-desktop.AppImage"
|
# metadata (.PKGINFO, .BUILDINFO and .MTREE) across.
|
||||||
install -d "$pkgdir/usr/bin"
|
bsdtar -xf "$srcdir/$_package" -C "$pkgdir" usr
|
||||||
ln -s "/opt/$pkgname/gitty-desktop.AppImage" \
|
|
||||||
"$pkgdir/usr/bin/gitty-desktop"
|
|
||||||
|
|
||||||
install -Dm644 "$srcdir/gitty-desktop.png" \
|
|
||||||
"$pkgdir/usr/share/icons/hicolor/512x512/apps/gitty-desktop.png"
|
|
||||||
|
|
||||||
install -d "$pkgdir/usr/share/applications"
|
|
||||||
cat > "$pkgdir/usr/share/applications/gitty-desktop.desktop" <<-EOF
|
|
||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Name=Gitty
|
|
||||||
Comment=$pkgdesc
|
|
||||||
Exec=gitty-desktop
|
|
||||||
Icon=gitty-desktop
|
|
||||||
Terminal=false
|
|
||||||
Categories=Development;RevisionControl;
|
|
||||||
StartupWMClass=gitty
|
|
||||||
EOF
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Install Gitty from the AUR with an AUR helper:
|
|||||||
yay -S gitty-desktop
|
yay -S gitty-desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
To install the prebuilt AppImage instead of compiling from source:
|
To install the prebuilt native Arch package instead of compiling from source:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yay -S gitty-desktop-bin
|
yay -S gitty-desktop-bin
|
||||||
@@ -75,8 +75,9 @@ makepkg -si
|
|||||||
```
|
```
|
||||||
|
|
||||||
The source recipe downloads the public Gitea release archive and builds Gitty.
|
The source recipe downloads the public Gitea release archive and builds Gitty.
|
||||||
The `-bin` recipe installs the prebuilt AppImage. The release pipeline updates
|
The `-bin` recipe repackages the native `.pkg.tar.zst` release artifact. The
|
||||||
both packages' versions, checksums, and `.SRCINFO` files.
|
release pipeline updates both packages' versions, checksums, and `.SRCINFO`
|
||||||
|
files.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "gitty",
|
"name": "gitty",
|
||||||
"version": "2026.8.3",
|
"version": "2026.8.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gitty",
|
"name": "gitty",
|
||||||
"version": "2026.8.3",
|
"version": "2026.8.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/svelte": "^1.21.0",
|
"@lucide/svelte": "^1.21.0",
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gitty",
|
"name": "gitty",
|
||||||
"version": "2026.8.3",
|
"version": "2026.8.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+119
-13
@@ -651,30 +651,136 @@ pub fn set_branch_upstream(
|
|||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command]
|
||||||
pub fn delete_remote_branch(
|
pub async fn delete_remote_branch(
|
||||||
path: String,
|
path: String,
|
||||||
remote: String,
|
remote: String,
|
||||||
branch: String,
|
branch: String,
|
||||||
|
username: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
) -> Result<GitStatus, String> {
|
) -> Result<GitStatus, String> {
|
||||||
log::info!(target: "gitty::remote", "delete_remote_branch invoked: path={path:?}, remote={remote:?}, branch={branch:?}");
|
run_git_task("Could not delete remote branch", move || {
|
||||||
let result = (|| {
|
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let remote = validate_remote_name(&repo, &remote, true)?;
|
let remote = validate_remote_name(&repo, &remote, true)?;
|
||||||
|
delete_remote_branches_core(
|
||||||
|
&repo,
|
||||||
|
&remote,
|
||||||
|
vec![branch],
|
||||||
|
username.as_deref().zip(password.as_deref()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_remote_branches(
|
||||||
|
path: String,
|
||||||
|
remote: String,
|
||||||
|
branches: Vec<String>,
|
||||||
|
username: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
|
run_git_task("Could not delete remote branch folder", move || {
|
||||||
|
let repo = resolve_repo(&path)?;
|
||||||
|
let remote = validate_remote_name(&repo, &remote, true)?;
|
||||||
|
delete_remote_branches_core(
|
||||||
|
&repo,
|
||||||
|
&remote,
|
||||||
|
branches,
|
||||||
|
username.as_deref().zip(password.as_deref()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_remote_branches_core(
|
||||||
|
repo: &Path,
|
||||||
|
remote: &str,
|
||||||
|
branches: Vec<String>,
|
||||||
|
credentials: Option<(&str, &str)>,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
|
let run_remote = |args: Vec<String>| match credentials {
|
||||||
|
Some((user, pass)) if !user.is_empty() || !pass.is_empty() => {
|
||||||
|
run_git_authenticated(repo, args, user, pass)
|
||||||
|
}
|
||||||
|
_ => run_git(repo, args),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut branch_names = Vec::new();
|
||||||
|
for branch in branches {
|
||||||
let branch = branch.trim();
|
let branch = branch.trim();
|
||||||
if branch.is_empty() || branch.starts_with('-') {
|
if branch.is_empty() || branch.starts_with('-') {
|
||||||
return Err("Invalid remote branch name.".to_string());
|
return Err("Invalid remote branch name.".to_string());
|
||||||
}
|
}
|
||||||
run_git(&repo, ["check-ref-format", "--branch", branch])?;
|
run_git(repo, ["check-ref-format", "--branch", branch])?;
|
||||||
log::info!(target: "gitty::remote", "deleting remote branch with git push: {remote}/{branch}");
|
if !branch_names.iter().any(|existing| existing == branch) {
|
||||||
run_git(&repo, ["push", remote.as_str(), "--delete", branch])?;
|
branch_names.push(branch.to_string());
|
||||||
log::info!(target: "gitty::remote", "remote branch deleted successfully: {remote}/{branch}");
|
|
||||||
status_for_repo(&repo)
|
|
||||||
})();
|
|
||||||
if let Err(error) = &result {
|
|
||||||
log::error!(target: "gitty::remote", "delete_remote_branch failed: {error}");
|
|
||||||
}
|
}
|
||||||
result
|
}
|
||||||
|
if branch_names.is_empty() {
|
||||||
|
return Err("No remote branches were selected.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local remote-tracking refs can be stale when a branch was deleted by
|
||||||
|
// another client. Query the server first so deletion remains idempotent.
|
||||||
|
let mut query_args = vec![
|
||||||
|
"ls-remote".to_string(),
|
||||||
|
"--heads".to_string(),
|
||||||
|
remote.to_string(),
|
||||||
|
];
|
||||||
|
query_args.extend(
|
||||||
|
branch_names
|
||||||
|
.iter()
|
||||||
|
.map(|branch| format!("refs/heads/{branch}")),
|
||||||
|
);
|
||||||
|
let remote_refs = String::from_utf8_lossy(&run_remote(query_args)?).to_string();
|
||||||
|
let existing_refs: BTreeSet<&str> = remote_refs
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| line.split_once('\t').map(|(_, reference)| reference.trim()))
|
||||||
|
.collect();
|
||||||
|
branch_names.retain(|branch| existing_refs.contains(format!("refs/heads/{branch}").as_str()));
|
||||||
|
|
||||||
|
if !branch_names.is_empty() {
|
||||||
|
let mut atomic_args = vec![
|
||||||
|
"push".to_string(),
|
||||||
|
"--atomic".to_string(),
|
||||||
|
remote.to_string(),
|
||||||
|
"--delete".to_string(),
|
||||||
|
];
|
||||||
|
atomic_args.extend(branch_names.iter().cloned());
|
||||||
|
|
||||||
|
if let Err(error) = run_remote(atomic_args) {
|
||||||
|
let message = error.to_lowercase();
|
||||||
|
let atomic_unsupported = message.contains("does not support --atomic")
|
||||||
|
|| message.contains("atomic push is not supported")
|
||||||
|
|| message.contains("does not support atomic push");
|
||||||
|
if !atomic_unsupported {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::warn!(
|
||||||
|
target: "gitty::remote",
|
||||||
|
"remote {remote} does not support atomic pushes; retrying branch-folder deletion as one regular push"
|
||||||
|
);
|
||||||
|
let mut fallback_args = vec![
|
||||||
|
"push".to_string(),
|
||||||
|
remote.to_string(),
|
||||||
|
"--delete".to_string(),
|
||||||
|
];
|
||||||
|
fallback_args.extend(branch_names);
|
||||||
|
run_remote(fallback_args)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also remove stale tracking refs for branches that were already absent.
|
||||||
|
if let Err(error) = run_remote(vec![
|
||||||
|
"fetch".to_string(),
|
||||||
|
"--prune".to_string(),
|
||||||
|
remote.to_string(),
|
||||||
|
]) {
|
||||||
|
log::warn!(target: "gitty::remote", "remote branches were deleted, but tracking refs could not be pruned: {error}");
|
||||||
|
}
|
||||||
|
status_for_repo(repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ use git::{
|
|||||||
commit_ai_load, commit_ai_local_models, commit_ai_review, commit_ai_split, commit_ai_status,
|
commit_ai_load, commit_ai_local_models, commit_ai_review, commit_ai_split, commit_ai_status,
|
||||||
compare_commits, compare_file_to_head, compare_file_to_parent, create_branch, create_tag,
|
compare_commits, compare_file_to_head, compare_file_to_parent, create_branch, create_tag,
|
||||||
cred_delete, cred_load, cred_save, delete_branch, delete_commit_note, delete_remote_branch,
|
cred_delete, cred_load, cred_save, delete_branch, delete_commit_note, delete_remote_branch,
|
||||||
delete_tag, diff_file_against_working_tree, fetch, fetch_commit_notes, get_commit_note,
|
delete_remote_branches, delete_tag, diff_file_against_working_tree, fetch, fetch_commit_notes,
|
||||||
get_file_blame, get_file_patch, get_remote_url, get_status, init_repository,
|
get_commit_note, get_file_blame, get_file_patch, get_remote_url, get_status, init_repository,
|
||||||
last_commit_message, list_branches, list_commits, list_file_history,
|
last_commit_message, list_branches, list_commits, list_file_history,
|
||||||
list_interactive_rebase_commits, list_reflog, list_remotes, list_repository_files,
|
list_interactive_rebase_commits, list_reflog, list_remotes, list_repository_files,
|
||||||
list_stashes, list_tags, list_worktrees, lock_worktree, merge_abort, merge_branch,
|
list_stashes, list_tags, list_worktrees, lock_worktree, merge_abort, merge_branch,
|
||||||
@@ -141,6 +141,7 @@ async fn main() {
|
|||||||
remove_remote,
|
remove_remote,
|
||||||
set_branch_upstream,
|
set_branch_upstream,
|
||||||
delete_remote_branch,
|
delete_remote_branch,
|
||||||
|
delete_remote_branches,
|
||||||
list_stashes,
|
list_stashes,
|
||||||
checkout_branch,
|
checkout_branch,
|
||||||
create_branch,
|
create_branch,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Gitty",
|
"productName": "Gitty",
|
||||||
"version": "2026.8.3",
|
"version": "2026.8.4",
|
||||||
"identifier": "com.gitty",
|
"identifier": "com.gitty",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
+111
-17
@@ -60,6 +60,7 @@
|
|||||||
deleteCommitNote,
|
deleteCommitNote,
|
||||||
deleteTag,
|
deleteTag,
|
||||||
deleteRemoteBranch,
|
deleteRemoteBranch,
|
||||||
|
deleteRemoteBranches,
|
||||||
initRepository,
|
initRepository,
|
||||||
diffFileAgainstWorkingTree,
|
diffFileAgainstWorkingTree,
|
||||||
compareFileToParent,
|
compareFileToParent,
|
||||||
@@ -188,7 +189,7 @@
|
|||||||
|
|
||||||
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
||||||
type AppView = "management" | "repository";
|
type AppView = "management" | "repository";
|
||||||
type CredentialAction = "push" | "pull" | "fetch" | "clone" | "rename";
|
type CredentialAction = "push" | "pull" | "fetch" | "clone" | "rename" | "delete";
|
||||||
type CredentialMode = "credentials" | "token";
|
type CredentialMode = "credentials" | "token";
|
||||||
type PendingDiscard =
|
type PendingDiscard =
|
||||||
| { kind: "file"; files: GitFileStatus[]; staged: boolean }
|
| { kind: "file"; files: GitFileStatus[]; staged: boolean }
|
||||||
@@ -411,6 +412,7 @@
|
|||||||
let credDialogOpen = false;
|
let credDialogOpen = false;
|
||||||
let credDialogAction: CredentialAction | null = null;
|
let credDialogAction: CredentialAction | null = null;
|
||||||
let pendingRemoteRename: { remote: string; oldBranch: string; newBranch: string } | null = null;
|
let pendingRemoteRename: { remote: string; oldBranch: string; newBranch: string } | null = null;
|
||||||
|
let pendingRemoteDelete: { remote: string; branches: string[]; label: string; folder: boolean } | null = null;
|
||||||
let credDialogError = "";
|
let credDialogError = "";
|
||||||
let credDialogKey: string | null = null;
|
let credDialogKey: string | null = null;
|
||||||
let credDialogUsername = "";
|
let credDialogUsername = "";
|
||||||
@@ -1974,6 +1976,7 @@
|
|||||||
globalSearchResults = [];
|
globalSearchResults = [];
|
||||||
deleteBranchTarget = null;
|
deleteBranchTarget = null;
|
||||||
deleteBranchForce = false;
|
deleteBranchForce = false;
|
||||||
|
pendingRemoteDelete = null;
|
||||||
worktreeDialogOpen = false;
|
worktreeDialogOpen = false;
|
||||||
worktreeInitialBranch = "";
|
worktreeInitialBranch = "";
|
||||||
worktrees = [];
|
worktrees = [];
|
||||||
@@ -2656,6 +2659,72 @@
|
|||||||
trackEvent("branch_delete_dialog_opened");
|
trackEvent("branch_delete_dialog_opened");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteBranchFolder(folderName: string, folderBranches: GitBranchInfo[], folderDepth: number) {
|
||||||
|
if (!activeRepoPath || isBusy) return;
|
||||||
|
const remoteFolder = folderBranches.every((branch) => branch.remote);
|
||||||
|
if (remoteFolder && folderDepth === 0) {
|
||||||
|
errorMessage = "The top-level remote folder cannot be deleted as a group.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const deletableBranches = folderBranches.filter((branch) => !branch.current);
|
||||||
|
const currentBranchKept = folderBranches.some((branch) => branch.current);
|
||||||
|
if (deletableBranches.length === 0) {
|
||||||
|
errorMessage = "The folder only contains the current branch, which cannot be deleted.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = remoteFolder ? "remote" : "local";
|
||||||
|
const currentNote = currentBranchKept ? "\n\nThe current branch will be kept." : "";
|
||||||
|
if (!window.confirm(`Delete ${deletableBranches.length} ${scope} branches in “${folderName}”?${currentNote}`)) return;
|
||||||
|
|
||||||
|
const repoPath = activeRepoPath;
|
||||||
|
if (remoteFolder) {
|
||||||
|
const remoteBranches = deletableBranches.map((branch) => {
|
||||||
|
const slash = branch.name.indexOf("/");
|
||||||
|
if (slash < 1) throw new Error(`Could not determine the remote for ${branch.name}.`);
|
||||||
|
return { remote: branch.name.slice(0, slash), name: branch.name.slice(slash + 1) };
|
||||||
|
});
|
||||||
|
const remote = remoteBranches[0]?.remote;
|
||||||
|
if (!remote || remoteBranches.some((branch) => branch.remote !== remote)) {
|
||||||
|
errorMessage = "A remote branch folder must belong to exactly one remote.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingRemoteDelete = { remote, branches: remoteBranches.map((branch) => branch.name), label: folderName, folder: true };
|
||||||
|
const key = await currentCredKey("delete");
|
||||||
|
const stored = await loadStoredCredential(key);
|
||||||
|
if (stored && (!key || !rejectedCredentialKeys.has(key))) {
|
||||||
|
await doActualRemoteDelete(stored.username, stored.password, key, true, credentialModeFor(stored));
|
||||||
|
} else {
|
||||||
|
await openCredentialDialog("delete", key, stored);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const failures: string[] = [];
|
||||||
|
operation = `Deleting branches in ${folderName}`;
|
||||||
|
errorMessage = "";
|
||||||
|
try {
|
||||||
|
for (const branch of deletableBranches) {
|
||||||
|
try {
|
||||||
|
applyStatus(await deleteBranch(repoPath, branch.name, false));
|
||||||
|
} catch (error) {
|
||||||
|
failures.push(`${branch.name}: ${errorToMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await refreshRepositoryViews(repoPath);
|
||||||
|
trackEvent("branch_folder_deleted", {
|
||||||
|
attempted: deletableBranches.length,
|
||||||
|
failed: failures.length,
|
||||||
|
remote: 0,
|
||||||
|
});
|
||||||
|
if (failures.length > 0) {
|
||||||
|
errorMessage = `${failures.length} branch${failures.length === 1 ? "" : "es"} could not be deleted:\n${failures.join("\n")}`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
operation = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmDeleteBranch() {
|
async function confirmDeleteBranch() {
|
||||||
const branch = deleteBranchTarget;
|
const branch = deleteBranchTarget;
|
||||||
if (!activeRepoPath || !branch || branch.current || isBusy) return;
|
if (!activeRepoPath || !branch || branch.current || isBusy) return;
|
||||||
@@ -2665,17 +2734,13 @@
|
|||||||
if (slash < 1) { errorMessage = "Could not determine remote name."; return; }
|
if (slash < 1) { errorMessage = "Could not determine remote name."; return; }
|
||||||
const remote = branch.name.slice(0, slash);
|
const remote = branch.name.slice(0, slash);
|
||||||
const remoteBranch = branch.name.slice(slash + 1);
|
const remoteBranch = branch.name.slice(slash + 1);
|
||||||
operation = `Deleting ${branch.name} from remote`;
|
pendingRemoteDelete = { remote, branches: [remoteBranch], label: branch.name, folder: false };
|
||||||
errorMessage = "";
|
const key = await currentCredKey("delete");
|
||||||
try {
|
const stored = await loadStoredCredential(key);
|
||||||
applyStatus(await deleteRemoteBranch(activeRepoPath, remote, remoteBranch));
|
if (stored && (!key || !rejectedCredentialKeys.has(key))) {
|
||||||
deleteBranchTarget = null;
|
await doActualRemoteDelete(stored.username, stored.password, key, true, credentialModeFor(stored));
|
||||||
await refreshRefsAndCommitGraph(activeRepoPath);
|
} else {
|
||||||
trackEvent("remote_branch_deleted");
|
await openCredentialDialog("delete", key, stored);
|
||||||
} catch (error) {
|
|
||||||
errorMessage = errorToMessage(error);
|
|
||||||
} finally {
|
|
||||||
operation = "";
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3234,11 +3299,11 @@
|
|||||||
|
|
||||||
// Resolve the keychain key (host/org) from the exact remote URL used by the
|
// Resolve the keychain key (host/org) from the exact remote URL used by the
|
||||||
// operation. Push URLs may intentionally differ from fetch URLs.
|
// operation. Push URLs may intentionally differ from fetch URLs.
|
||||||
async function currentCredKey(action: "push" | "pull" | "fetch" | "rename" = "fetch"): Promise<string | null> {
|
async function currentCredKey(action: "push" | "pull" | "fetch" | "rename" | "delete" = "fetch"): Promise<string | null> {
|
||||||
if (!activeRepoPath) return null;
|
if (!activeRepoPath) return null;
|
||||||
try {
|
try {
|
||||||
const remote = action === "rename" ? pendingRemoteRename?.remote : selectedRemote;
|
const remote = action === "rename" ? pendingRemoteRename?.remote : action === "delete" ? pendingRemoteDelete?.remote : selectedRemote;
|
||||||
const url = await getRemoteUrl(activeRepoPath, remote || undefined, action === "push" || action === "rename");
|
const url = await getRemoteUrl(activeRepoPath, remote || undefined, action === "push" || action === "rename" || action === "delete");
|
||||||
return url ? orgKeyFromUrl(url) : null;
|
return url ? orgKeyFromUrl(url) : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -3277,7 +3342,7 @@
|
|||||||
// so a temporary 401/403 cannot erase a valid token; the key is only skipped
|
// so a temporary 401/403 cannot erase a valid token; the key is only skipped
|
||||||
// for the rest of this session until the user replaces it successfully.
|
// for the rest of this session until the user replaces it successfully.
|
||||||
function handleRemoteResult(
|
function handleRemoteResult(
|
||||||
action: "push" | "pull" | "fetch" | "rename",
|
action: "push" | "pull" | "fetch" | "rename" | "delete",
|
||||||
key: string | null,
|
key: string | null,
|
||||||
fromStore: boolean,
|
fromStore: boolean,
|
||||||
username: string,
|
username: string,
|
||||||
@@ -3445,6 +3510,32 @@
|
|||||||
handleRemoteResult("rename", key, fromStore, username, mode);
|
handleRemoteResult("rename", key, fromStore, username, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function doActualRemoteDelete(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
key: string | null,
|
||||||
|
fromStore: boolean,
|
||||||
|
mode: CredentialMode,
|
||||||
|
) {
|
||||||
|
const deletion = pendingRemoteDelete;
|
||||||
|
if (!activeRepoPath || !deletion) return;
|
||||||
|
errorMessage = "";
|
||||||
|
await runOperation(`Deleting ${deletion.label} from remote`, async () => {
|
||||||
|
applyStatus(deletion.folder
|
||||||
|
? await deleteRemoteBranches(activeRepoPath, deletion.remote, deletion.branches, username, password)
|
||||||
|
: await deleteRemoteBranch(activeRepoPath, deletion.remote, deletion.branches[0], username, password));
|
||||||
|
deleteBranchTarget = null;
|
||||||
|
pendingRemoteDelete = null;
|
||||||
|
await refreshRefsAndCommitGraph(activeRepoPath);
|
||||||
|
if (deletion.folder) {
|
||||||
|
trackEvent("branch_folder_deleted", { attempted: deletion.branches.length, failed: 0, remote: 1 });
|
||||||
|
} else {
|
||||||
|
trackEvent("remote_branch_deleted");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
handleRemoteResult("delete", key, fromStore, username, mode);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleCredentialSubmit(
|
async function handleCredentialSubmit(
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
@@ -3468,6 +3559,7 @@
|
|||||||
else if (credDialogAction === "push") await doActualPush(username, password, key, false, mode);
|
else if (credDialogAction === "push") await doActualPush(username, password, key, false, mode);
|
||||||
else if (credDialogAction === "fetch") await doActualFetch(username, password, key, false, mode);
|
else if (credDialogAction === "fetch") await doActualFetch(username, password, key, false, mode);
|
||||||
else if (credDialogAction === "rename") await doActualRemoteRename(username, password, key, false, mode);
|
else if (credDialogAction === "rename") await doActualRemoteRename(username, password, key, false, mode);
|
||||||
|
else if (credDialogAction === "delete") await doActualRemoteDelete(username, password, key, false, mode);
|
||||||
else if (credDialogAction === "clone" && pendingClone) {
|
else if (credDialogAction === "clone" && pendingClone) {
|
||||||
await cloneRepo(
|
await cloneRepo(
|
||||||
pendingClone.remoteUrl,
|
pendingClone.remoteUrl,
|
||||||
@@ -4962,6 +5054,7 @@
|
|||||||
onRenameBranch={openRenameBranchDialog}
|
onRenameBranch={openRenameBranchDialog}
|
||||||
onDeleteBranch={deleteLocalBranch}
|
onDeleteBranch={deleteLocalBranch}
|
||||||
onDeleteRemoteBranch={deleteTrackedRemoteBranch}
|
onDeleteRemoteBranch={deleteTrackedRemoteBranch}
|
||||||
|
onDeleteBranchFolder={deleteBranchFolder}
|
||||||
onCreateTag={createNewTag}
|
onCreateTag={createNewTag}
|
||||||
onDeleteTag={deleteLocalTag}
|
onDeleteTag={deleteLocalTag}
|
||||||
onPushTag={pushLocalTag}
|
onPushTag={pushLocalTag}
|
||||||
@@ -5564,7 +5657,7 @@
|
|||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Credential dialog for push/pull -->
|
<!-- Credential dialog for authenticated remote actions -->
|
||||||
{#if credDialogOpen && credDialogAction}
|
{#if credDialogOpen && credDialogAction}
|
||||||
<CredentialDialog
|
<CredentialDialog
|
||||||
action={credDialogAction}
|
action={credDialogAction}
|
||||||
@@ -5576,6 +5669,7 @@
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
credDialogOpen = false;
|
credDialogOpen = false;
|
||||||
credDialogAction = null;
|
credDialogAction = null;
|
||||||
|
pendingRemoteDelete = null;
|
||||||
credDialogError = "";
|
credDialogError = "";
|
||||||
credDialogKey = null;
|
credDialogKey = null;
|
||||||
credDialogUsername = "";
|
credDialogUsername = "";
|
||||||
|
|||||||
+27
-8
@@ -1112,10 +1112,29 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-ink-faint);
|
color: var(--color-ink-faint);
|
||||||
}
|
}
|
||||||
.repo-tab-close { opacity: 0.58; transition: opacity 120ms ease, color 120ms ease, background 120ms ease; }
|
.repo-tab-close {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-self: center;
|
||||||
|
min-width: 26px;
|
||||||
|
width: 26px;
|
||||||
|
max-width: 26px;
|
||||||
|
min-height: 26px;
|
||||||
|
height: 26px;
|
||||||
|
max-height: 26px;
|
||||||
|
flex: 0 0 26px;
|
||||||
|
margin-right: 6px;
|
||||||
|
border-radius: 5px;
|
||||||
|
opacity: 0.58;
|
||||||
|
transition: opacity 120ms ease, color 120ms ease, background 120ms ease;
|
||||||
|
}
|
||||||
.repo-tab-wrap:hover .repo-tab-close,
|
.repo-tab-wrap:hover .repo-tab-close,
|
||||||
.repo-tab-wrap.active .repo-tab-close { opacity: 1; }
|
.repo-tab-wrap.active .repo-tab-close { opacity: 1; }
|
||||||
.repo-tab-close:hover:not(:disabled) { color: var(--color-ink); background: var(--color-surface-hover); }
|
.repo-tab-close:hover:not(:disabled),
|
||||||
|
.repo-tab-close:focus-visible:not(:disabled) {
|
||||||
|
color: #ff6673;
|
||||||
|
background: rgba(255, 90, 103, 0.13);
|
||||||
|
}
|
||||||
.repo-tab-add {
|
.repo-tab-add {
|
||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
border-left: 1px solid var(--color-border-subtle);
|
border-left: 1px solid var(--color-border-subtle);
|
||||||
@@ -3334,8 +3353,8 @@
|
|||||||
align-items: end;
|
align-items: end;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.compare-field { display: grid; gap: 4px; min-width: 0; }
|
.compare-field { display: grid; gap: 6px; min-width: 0; }
|
||||||
.compare-field span { color: var(--color-ink-faint); font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; }
|
.compare-field > span { color: var(--color-ink-muted); font-size: 10.5px; font-weight: 750; }
|
||||||
.compare-target-help {
|
.compare-target-help {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -3509,8 +3528,8 @@
|
|||||||
.compare-dialog-title { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
.compare-dialog-title { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||||
.compare-dialog-title > div { min-width: 0; }
|
.compare-dialog-title > div { min-width: 0; }
|
||||||
.compare-dialog-mark { display: grid; place-items: center; flex: 0 0 auto; width: 38px; height: 38px; border: 1px solid color-mix(in srgb, var(--color-accent) 28%, var(--color-border)); border-radius: 10px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 9%, transparent); }
|
.compare-dialog-mark { display: grid; place-items: center; flex: 0 0 auto; width: 38px; height: 38px; border: 1px solid color-mix(in srgb, var(--color-accent) 28%, var(--color-border)); border-radius: 10px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 9%, transparent); }
|
||||||
.compare-dialog-title h2 { margin: 0; color: var(--color-accent); font-size: 18px; font-weight: 700; line-height: 1.2; }
|
.compare-dialog-title h2 { margin: 0; color: var(--color-ink); font-size: 18px; font-weight: 700; line-height: 1.2; }
|
||||||
.compare-dialog-title p { margin: 3px 0 0; color: var(--color-ink); font-size: 11px; font-weight: 400; line-height: 1.35; }
|
.compare-dialog-title p { margin: 3px 0 0; color: var(--color-ink-dim); font-size: 11px; font-weight: 400; line-height: 1.35; }
|
||||||
.global-search-dialog {
|
.global-search-dialog {
|
||||||
width: min(1180px, calc(100vw - 32px));
|
width: min(1180px, calc(100vw - 32px));
|
||||||
height: min(840px, calc(100vh - 32px));
|
height: min(840px, calc(100vh - 32px));
|
||||||
@@ -3535,8 +3554,8 @@
|
|||||||
.compare-select-shell { display: grid; grid-template-rows: minmax(0, 1fr) auto; min-height: 0; }
|
.compare-select-shell { display: grid; grid-template-rows: minmax(0, 1fr) auto; min-height: 0; }
|
||||||
.compare-select-body { display: grid; align-content: start; gap: 12px; min-height: 0; padding: 18px 20px 20px; overflow: auto; }
|
.compare-select-body { display: grid; align-content: start; gap: 12px; min-height: 0; padding: 18px 20px 20px; overflow: auto; }
|
||||||
.compare-target-panel { display: grid; gap: 16px; padding: 16px; border: 1px solid var(--color-border-subtle); border-radius: 12px; background: color-mix(in srgb, var(--app-settings-row-bg) 72%, transparent); }
|
.compare-target-panel { display: grid; gap: 16px; padding: 16px; border: 1px solid var(--color-border-subtle); border-radius: 12px; background: color-mix(in srgb, var(--app-settings-row-bg) 72%, transparent); }
|
||||||
.compare-target-heading h3 { margin: 0; color: var(--color-accent); font-size: 18px; font-weight: 700; line-height: 1.2; }
|
.compare-target-heading h3 { margin: 0; color: var(--color-ink); font-size: 14px; font-weight: 700; line-height: 1.2; }
|
||||||
.compare-target-heading p { margin: 4px 0 0; color: var(--color-ink); font-size: 11px; font-weight: 400; line-height: 1.45; }
|
.compare-target-heading p { margin: 3px 0 0; color: var(--color-ink-dim); font-size: 10.5px; font-weight: 400; line-height: 1.4; }
|
||||||
.compare-arrow-shell { display: grid; place-items: center; width: 40px; height: 40px; border: 1px solid var(--color-border-subtle); border-radius: 9px; color: var(--color-accent); background: var(--color-surface-raised); }
|
.compare-arrow-shell { display: grid; place-items: center; width: 40px; height: 40px; border: 1px solid var(--color-border-subtle); border-radius: 9px; color: var(--color-accent); background: var(--color-surface-raised); }
|
||||||
.compare-select-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 62px; padding: 11px 16px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
|
.compare-select-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 62px; padding: 11px 16px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
|
||||||
.compare-select-footer > span { color: var(--color-ink-faint); font-size: 9.5px; }
|
.compare-select-footer > span { color: var(--color-ink-faint); font-size: 9.5px; }
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
aria-label={language === "de" ? `${repo.name} schließen` : `Close ${repo.name}`}
|
aria-label={language === "de" ? `${repo.name} schließen` : `Close ${repo.name}`}
|
||||||
title={language === "de" ? "Repository-Tab schließen" : "Close repository tab"}
|
title={language === "de" ? "Repository-Tab schließen" : "Close repository tab"}
|
||||||
>
|
>
|
||||||
<X size={13} aria-hidden="true" />
|
<X size={11} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
children: BranchTreeNode[];
|
children: BranchTreeNode[];
|
||||||
branchCount: number;
|
branchCount: number;
|
||||||
current: boolean;
|
current: boolean;
|
||||||
|
branches: GitBranchInfo[];
|
||||||
folders: Map<string, BranchFolderNode>;
|
folders: Map<string, BranchFolderNode>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
depth: number;
|
depth: number;
|
||||||
branchCount: number;
|
branchCount: number;
|
||||||
current: boolean;
|
current: boolean;
|
||||||
|
branches: GitBranchInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BranchLeafRow {
|
interface BranchLeafRow {
|
||||||
@@ -57,6 +59,7 @@
|
|||||||
onRenameBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
onRenameBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||||
onDeleteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
onDeleteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||||
onDeleteRemoteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
onDeleteRemoteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||||
|
onDeleteBranchFolder: (folderName: string, branches: GitBranchInfo[], depth: number) => void | Promise<void>;
|
||||||
onCreateTag: (name: string, message: string) => void | Promise<void>;
|
onCreateTag: (name: string, message: string) => void | Promise<void>;
|
||||||
onDeleteTag: (tag: GitTag) => void | Promise<void>;
|
onDeleteTag: (tag: GitTag) => void | Promise<void>;
|
||||||
onPushTag: (tag: GitTag) => void | Promise<void>;
|
onPushTag: (tag: GitTag) => void | Promise<void>;
|
||||||
@@ -81,6 +84,7 @@
|
|||||||
onRenameBranch = () => {},
|
onRenameBranch = () => {},
|
||||||
onDeleteBranch = () => {},
|
onDeleteBranch = () => {},
|
||||||
onDeleteRemoteBranch = () => {},
|
onDeleteRemoteBranch = () => {},
|
||||||
|
onDeleteBranchFolder = () => {},
|
||||||
onCreateTag = () => {},
|
onCreateTag = () => {},
|
||||||
onDeleteTag = () => {},
|
onDeleteTag = () => {},
|
||||||
onPushTag = () => {},
|
onPushTag = () => {},
|
||||||
@@ -101,6 +105,7 @@
|
|||||||
let newTagMessage = $state("");
|
let newTagMessage = $state("");
|
||||||
let tagCreateInput = $state<HTMLInputElement | null>(null);
|
let tagCreateInput = $state<HTMLInputElement | null>(null);
|
||||||
let contextBranch = $state<GitBranchInfo | null>(null);
|
let contextBranch = $state<GitBranchInfo | null>(null);
|
||||||
|
let contextFolder = $state<BranchFolderRow | null>(null);
|
||||||
let branchContextMenuElement = $state<HTMLElement | null>(null);
|
let branchContextMenuElement = $state<HTMLElement | null>(null);
|
||||||
let contextMenuX = $state(0);
|
let contextMenuX = $state(0);
|
||||||
let contextMenuY = $state(0);
|
let contextMenuY = $state(0);
|
||||||
@@ -121,6 +126,7 @@
|
|||||||
children: [],
|
children: [],
|
||||||
branchCount: 0,
|
branchCount: 0,
|
||||||
current: false,
|
current: false,
|
||||||
|
branches: [],
|
||||||
folders: new Map(),
|
folders: new Map(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -147,6 +153,7 @@
|
|||||||
|
|
||||||
folder.branchCount += 1;
|
folder.branchCount += 1;
|
||||||
folder.current ||= branch.current;
|
folder.current ||= branch.current;
|
||||||
|
folder.branches.push(branch);
|
||||||
parent = folder;
|
parent = folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +195,7 @@
|
|||||||
depth,
|
depth,
|
||||||
branchCount: node.branchCount,
|
branchCount: node.branchCount,
|
||||||
current: node.current,
|
current: node.current,
|
||||||
|
branches: node.branches,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isBranchFolderOpen(node.id)) {
|
if (isBranchFolderOpen(node.id)) {
|
||||||
@@ -272,8 +280,25 @@
|
|||||||
contextMenuY = position.y;
|
contextMenuY = position.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openFolderContextMenu(event: MouseEvent, folder: BranchFolderRow) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (isBusy || (folder.depth === 0 && folder.branches.every((branch) => branch.remote))) return;
|
||||||
|
|
||||||
|
contextFolder = folder;
|
||||||
|
contextMenuX = event.clientX + 2;
|
||||||
|
contextMenuY = event.clientY + 2;
|
||||||
|
await tick();
|
||||||
|
if (contextFolder !== folder) return;
|
||||||
|
|
||||||
|
const position = fitContextMenuToViewport(branchContextMenuElement, event.clientX, event.clientY);
|
||||||
|
contextMenuX = position.x;
|
||||||
|
contextMenuY = position.y;
|
||||||
|
}
|
||||||
|
|
||||||
function closeBranchContextMenu() {
|
function closeBranchContextMenu() {
|
||||||
contextBranch = null;
|
contextBranch = null;
|
||||||
|
contextFolder = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameContextBranch() {
|
async function renameContextBranch() {
|
||||||
@@ -297,6 +322,13 @@
|
|||||||
if (branch.remote) await onDeleteRemoteBranch(branch); else await onDeleteBranch(branch);
|
if (branch.remote) await onDeleteRemoteBranch(branch); else await onDeleteBranch(branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteContextFolder() {
|
||||||
|
const folder = contextFolder;
|
||||||
|
if (!folder || isBusy || (folder.depth === 0 && folder.branches.every((branch) => branch.remote))) return;
|
||||||
|
closeBranchContextMenu();
|
||||||
|
await onDeleteBranchFolder(folder.name, folder.branches, folder.depth);
|
||||||
|
}
|
||||||
|
|
||||||
async function createContextWorktree() {
|
async function createContextWorktree() {
|
||||||
const branch = contextBranch;
|
const branch = contextBranch;
|
||||||
closeBranchContextMenu();
|
closeBranchContextMenu();
|
||||||
@@ -484,6 +516,7 @@
|
|||||||
style={`--branch-indent: ${row.depth * 16}px;`}
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => toggleBranchFolder(row.id)}
|
onclick={() => toggleBranchFolder(row.id)}
|
||||||
|
oncontextmenu={(event) => openFolderContextMenu(event, row)}
|
||||||
aria-expanded={isBranchFolderOpen(row.id)}
|
aria-expanded={isBranchFolderOpen(row.id)}
|
||||||
title={`${row.name} (${row.branchCount})`}
|
title={`${row.name} (${row.branchCount})`}
|
||||||
>
|
>
|
||||||
@@ -551,6 +584,7 @@
|
|||||||
style={`--branch-indent: ${row.depth * 16}px;`}
|
style={`--branch-indent: ${row.depth * 16}px;`}
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => toggleBranchFolder(row.id)}
|
onclick={() => toggleBranchFolder(row.id)}
|
||||||
|
oncontextmenu={(event) => openFolderContextMenu(event, row)}
|
||||||
aria-expanded={isBranchFolderOpen(row.id)}
|
aria-expanded={isBranchFolderOpen(row.id)}
|
||||||
title={`${row.name} (${row.branchCount})`}
|
title={`${row.name} (${row.branchCount})`}
|
||||||
>
|
>
|
||||||
@@ -735,6 +769,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if contextFolder}
|
||||||
|
<div
|
||||||
|
bind:this={branchContextMenuElement}
|
||||||
|
class="branch-context-menu"
|
||||||
|
style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
|
||||||
|
role="menu"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label={`Actions for branch folder ${contextFolder.name}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="danger"
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onclick={deleteContextFolder}
|
||||||
|
disabled={isBusy || contextFolder.branches.every((branch) => branch.current)}
|
||||||
|
title={contextFolder.current ? "The current branch will be kept" : "Delete all branches in this folder"}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} aria-hidden="true" />
|
||||||
|
Delete {contextFolder.branches.filter((branch) => !branch.current).length} branches
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if contextTag}
|
{#if contextTag}
|
||||||
<div
|
<div
|
||||||
bind:this={tagContextMenuElement}
|
bind:this={tagContextMenuElement}
|
||||||
|
|||||||
@@ -47,9 +47,21 @@
|
|||||||
let remoteBranches = $derived(branches.filter((branch) => branch.remote));
|
let remoteBranches = $derived(branches.filter((branch) => branch.remote));
|
||||||
let targetCount = $derived(branches.length + commits.length);
|
let targetCount = $derived(branches.length + commits.length);
|
||||||
let compareOptions = $derived([
|
let compareOptions = $derived([
|
||||||
...localBranches.map((branch) => ({ value: branchValue(branch), label: `${branch.name}${branch.current ? " (current)" : ""}`, group: "Branches - Local" })),
|
...localBranches.map((branch) => ({
|
||||||
...remoteBranches.map((branch) => ({ value: branchValue(branch), label: branch.name, group: "Branches - Remote" })),
|
value: branchValue(branch),
|
||||||
...commits.map((item) => ({ value: item.hash, label: commitOptionLabel(item), group: "Commits" })),
|
label: `${branch.name}${branch.current ? (isGerman ? " (aktuell)" : " (current)") : ""}`,
|
||||||
|
group: isGerman ? "Lokale Branches" : "Local branches",
|
||||||
|
})),
|
||||||
|
...remoteBranches.map((branch) => ({
|
||||||
|
value: branchValue(branch),
|
||||||
|
label: branch.name,
|
||||||
|
group: isGerman ? "Remote-Branches" : "Remote branches",
|
||||||
|
})),
|
||||||
|
...commits.map((item) => ({
|
||||||
|
value: item.hash,
|
||||||
|
label: commitOptionLabel(item),
|
||||||
|
group: "Commits",
|
||||||
|
})),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function handleSubmit(event: SubmitEvent) {
|
function handleSubmit(event: SubmitEvent) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
action: "push" | "pull" | "fetch" | "clone" | "rename";
|
action: "push" | "pull" | "fetch" | "clone" | "rename" | "delete";
|
||||||
error: string;
|
error: string;
|
||||||
isBusy: boolean;
|
isBusy: boolean;
|
||||||
initialUsername?: string;
|
initialUsername?: string;
|
||||||
@@ -47,19 +47,21 @@
|
|||||||
password.trim().length > 0 &&
|
password.trim().length > 0 &&
|
||||||
username.trim().length > 0,
|
username.trim().length > 0,
|
||||||
);
|
);
|
||||||
let actionLabel = $derived(action === "push" ? "Push" : action === "fetch" ? "Fetch" : action === "clone" ? "Clone" : action === "rename" ? "Rename" : "Pull");
|
let actionLabel = $derived(action === "push" ? "Push" : action === "fetch" ? "Fetch" : action === "clone" ? "Clone" : action === "rename" ? "Rename" : action === "delete" ? "Delete" : "Pull");
|
||||||
let actionTitle = $derived(
|
let actionTitle = $derived(
|
||||||
action === "push"
|
action === "push"
|
||||||
? "Authenticate push"
|
? "Authenticate push"
|
||||||
: action === "rename"
|
: action === "rename"
|
||||||
? "Authenticate remote rename"
|
? "Authenticate remote rename"
|
||||||
|
: action === "delete"
|
||||||
|
? "Authenticate remote deletion"
|
||||||
: action === "fetch"
|
: action === "fetch"
|
||||||
? "Authenticate fetch"
|
? "Authenticate fetch"
|
||||||
: action === "clone"
|
: action === "clone"
|
||||||
? "Authenticate clone"
|
? "Authenticate clone"
|
||||||
: "Authenticate pull",
|
: "Authenticate pull",
|
||||||
);
|
);
|
||||||
let actionHint = $derived(action === "push" || action === "rename"
|
let actionHint = $derived(action === "push" || action === "rename" || action === "delete"
|
||||||
? "The remote needs write access. Use a password or a token with the appropriate repository permissions."
|
? "The remote needs write access. Use a password or a token with the appropriate repository permissions."
|
||||||
: action === "clone"
|
: action === "clone"
|
||||||
? "The repository needs access before it can be cloned. Use your Git credentials or a personal access token."
|
? "The repository needs access before it can be cloned. Use your Git credentials or a personal access token."
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
<div class="cred-hero">
|
<div class="cred-hero">
|
||||||
<div class="cred-hero-top">
|
<div class="cred-hero-top">
|
||||||
<div class="cred-hero-icon">
|
<div class="cred-hero-icon">
|
||||||
{#if action === "push" || action === "rename"}
|
{#if action === "push" || action === "rename" || action === "delete"}
|
||||||
<Upload size={27} aria-hidden="true" />
|
<Upload size={27} aria-hidden="true" />
|
||||||
{:else}
|
{:else}
|
||||||
<Download size={27} aria-hidden="true" />
|
<Download size={27} aria-hidden="true" />
|
||||||
|
|||||||
@@ -1399,6 +1399,17 @@
|
|||||||
label: "Neu in Gitty",
|
label: "Neu in Gitty",
|
||||||
description: "Änderungen seit der letzten veröffentlichten Version und wichtige Neuerungen früherer Releases.",
|
description: "Änderungen seit der letzten veröffentlichten Version und wichtige Neuerungen früherer Releases.",
|
||||||
sections: [
|
sections: [
|
||||||
|
{
|
||||||
|
id: "changelog-2026-8-4",
|
||||||
|
title: "Version 2026.8.4",
|
||||||
|
summary: "Dieses Release vereinfacht die Verwaltung zusammengehöriger Branches und sorgt für eine einheitlichere, klarere Oberfläche.",
|
||||||
|
steps: [
|
||||||
|
"Lokale und verschachtelte Remote-Branch-Ordner lassen sich über ihr Kontextmenü gesammelt löschen. Der oberste Remote-Ordner wie origin ist geschützt. Der aktuell ausgecheckte Branch bleibt erhalten und einzelne Fehler werden nach Abschluss verständlich aufgeführt.",
|
||||||
|
"Der Compare-Auswahldialog ist vollständig auf Deutsch verfügbar und orientiert sich bei Feldern, Gruppen, Typografie und Dialogflächen am Styling der externen Tools.",
|
||||||
|
"Die Schließen-Schaltflächen der Repository-Tabs sind quadratisch und haben ausgewogenere Abstände sowie deutlichere Hover- und Tastaturfokus-Zustände.",
|
||||||
|
],
|
||||||
|
note: "Der oberste Remote-Ordner wie origin kann nicht gesammelt gelöscht werden. Seine Unterordner können weiterhin gezielt verwaltet werden.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "changelog-2026-8-3",
|
id: "changelog-2026-8-3",
|
||||||
title: "Version 2026.8.3",
|
title: "Version 2026.8.3",
|
||||||
@@ -1488,6 +1499,17 @@
|
|||||||
label: "What's new",
|
label: "What's new",
|
||||||
description: "Changes since the latest published version and notable additions from earlier releases.",
|
description: "Changes since the latest published version and notable additions from earlier releases.",
|
||||||
sections: [
|
sections: [
|
||||||
|
{
|
||||||
|
id: "changelog-2026-8-4",
|
||||||
|
title: "Version 2026.8.4",
|
||||||
|
summary: "This release simplifies managing related branches and makes the interface more consistent and easier to read.",
|
||||||
|
steps: [
|
||||||
|
"Local and nested remote branch folders can be deleted in one action from their context menu. The top-level remote folder such as origin is protected. The currently checked-out branch is kept, and individual failures are summarized after processing.",
|
||||||
|
"The Compare selector is fully localized in German and now follows the external-tool selectors for fields, groups, typography, and dialog surfaces.",
|
||||||
|
"Repository-tab close buttons are square and have more balanced spacing and clearer hover and keyboard-focus states.",
|
||||||
|
],
|
||||||
|
note: "The top-level remote folder such as origin cannot be deleted in bulk. Its nested folders can still be managed selectively.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "changelog-2026-8-3",
|
id: "changelog-2026-8-3",
|
||||||
title: "Version 2026.8.3",
|
title: "Version 2026.8.3",
|
||||||
|
|||||||
+6
-1
@@ -107,7 +107,12 @@ export function removeRemote(path: string, name: string): Promise<GitRemote[]> {
|
|||||||
console.log("remove_remote")
|
console.log("remove_remote")
|
||||||
return invoke("remove_remote", { path, name }); }
|
return invoke("remove_remote", { path, name }); }
|
||||||
export function setBranchUpstream(path: string, branch: string, upstream?: string): Promise<GitStatus> { return invoke("set_branch_upstream", { path, branch, upstream: upstream || null }); }
|
export function setBranchUpstream(path: string, branch: string, upstream?: string): Promise<GitStatus> { return invoke("set_branch_upstream", { path, branch, upstream: upstream || null }); }
|
||||||
export function deleteRemoteBranch(path: string, remote: string, branch: string): Promise<GitStatus> { return invoke("delete_remote_branch", { path, remote, branch }); }
|
export function deleteRemoteBranch(path: string, remote: string, branch: string, username?: string, password?: string): Promise<GitStatus> {
|
||||||
|
return invoke("delete_remote_branch", { path, remote, branch, username: username ?? null, password: password ?? null });
|
||||||
|
}
|
||||||
|
export function deleteRemoteBranches(path: string, remote: string, branches: string[], username?: string, password?: string): Promise<GitStatus> {
|
||||||
|
return invoke("delete_remote_branches", { path, remote, branches, username: username ?? null, password: password ?? null });
|
||||||
|
}
|
||||||
|
|
||||||
export function listStashes(path: string): Promise<GitStash[]> {
|
export function listStashes(path: string): Promise<GitStash[]> {
|
||||||
return invoke<GitStash[]>("list_stashes", { path });
|
return invoke<GitStash[]>("list_stashes", { path });
|
||||||
|
|||||||
Reference in New Issue
Block a user