diff --git a/CHANGELOG.md b/CHANGELOG.md index df0b5610..c6449676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ All notable changes to AngKorGit are documented here. The format follows to fall apart once the view scrolled: copy returned one line and the highlight moved when you scrolled back. The selection now grows to the last visible line while you hold the mouse below the diff, and copy returns every selected line. +- **Push with tags failed** with `not a valid reference 'refs/tags/*'`. libgit2 does not + expand the glob git uses, so the engine now names each local tag in its own refspec. (#34) ## [0.16.0] — 2026-09-20 diff --git a/CLAUDE.md b/CLAUDE.md index 3cf39d51..e5f11325 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1399,7 +1399,8 @@ features/ │ like GitKraken"): ui.fileView 'list' | 'tree' | 'all' │ (persisted; the persist merge maps the old boolean fileTree │ → 'tree'), three aria-pressed header buttons List/FolderTree/ -│ Files. 'all' renders EVERY file — commit: ipc.treeFiles +│ FolderOpen (lucide `Files` was tried first and read as a +│ copy icon, owner 2026-09-22). 'all' renders EVERY file — commit: ipc.treeFiles │ (engine files.rs) unioned with the change list via core │ allFiles (deleted files stay, keyed by path); working copy: │ ipc.indexFiles ∪ status paths, reloaded on statusVersion diff --git a/apps/desktop/src-tauri/src/core/remote.rs b/apps/desktop/src-tauri/src/core/remote.rs index a8a277c5..97fd1ab8 100644 --- a/apps/desktop/src-tauri/src/core/remote.rs +++ b/apps/desktop/src-tauri/src/core/remote.rs @@ -511,11 +511,25 @@ fn pull_rebase_configured(repo: &Repository) -> bool { .unwrap_or(false) } -pub(crate) fn push_refspecs(branch: &str, force: bool, with_tags: bool) -> Vec { +fn local_tag_names(repo: &Repository) -> AppResult> { + let mut names = Vec::new(); + repo.tag_foreach(|_oid, name_bytes| { + let full = String::from_utf8_lossy(name_bytes); + if let Some(name) = full.strip_prefix("refs/tags/") { + if !name.is_empty() && !name.contains(':') && !name.contains('\0') { + names.push(name.to_string()); + } + } + true + })?; + Ok(names) +} + +pub(crate) fn push_refspecs(branch: &str, force: bool, tags: &[String]) -> Vec { let prefix = if force { "+" } else { "" }; let mut refspecs = vec![format!("{prefix}refs/heads/{branch}:refs/heads/{branch}")]; - if with_tags { - refspecs.push("refs/tags/*:refs/tags/*".to_string()); + for tag in tags { + refspecs.push(format!("refs/tags/{tag}:refs/tags/{tag}")); } refspecs } @@ -538,7 +552,12 @@ pub fn push( .to_string(), }; - let refspecs = push_refspecs(&branch_name, force, with_tags); + let tags = if with_tags { + local_tag_names(&repo)? + } else { + Vec::new() + }; + let refspecs = push_refspecs(&branch_name, force, &tags); let track_upstream = |repo: &Repository| -> AppResult<()> { if set_upstream { @@ -916,10 +935,10 @@ mod tests { #[test] fn plain_push_refspecs_have_no_force_prefix() { assert_eq!( - push_refspecs("main", false, true), + push_refspecs("main", false, &["v1".into()]), vec![ "refs/heads/main:refs/heads/main".to_string(), - "refs/tags/*:refs/tags/*".to_string(), + "refs/tags/v1:refs/tags/v1".to_string(), ] ); } @@ -927,10 +946,10 @@ mod tests { #[test] fn force_push_forces_only_the_branch_refspec() { assert_eq!( - push_refspecs("main", true, true), + push_refspecs("main", true, &["v1".into()]), vec![ "+refs/heads/main:refs/heads/main".to_string(), - "refs/tags/*:refs/tags/*".to_string(), + "refs/tags/v1:refs/tags/v1".to_string(), ] ); } @@ -938,7 +957,7 @@ mod tests { #[test] fn push_without_tags_sends_only_the_branch() { assert_eq!( - push_refspecs("feature/x", true, false), + push_refspecs("feature/x", true, &[]), vec!["+refs/heads/feature/x:refs/heads/feature/x".to_string()] ); } diff --git a/apps/desktop/src-tauri/tests/git_engine.rs b/apps/desktop/src-tauri/tests/git_engine.rs index 4903edc1..1f1f56eb 100644 --- a/apps/desktop/src-tauri/tests/git_engine.rs +++ b/apps/desktop/src-tauri/tests/git_engine.rs @@ -2324,6 +2324,27 @@ fn push_reports_up_to_date_instead_of_pushing_again() { let _ = std::fs::remove_dir_all(&origin); } +#[test] +fn push_with_tags_sends_named_tag_refs() { + let local = TempRepo::new(); + local.write("a.txt", "one\n"); + commit_all(&local, "one"); + let origin = bare_origin(&local); + + core::tag_create(local.path(), "v1.0.0", None, None).unwrap(); + let outcome = core::push(local.path(), "origin", None, false, true, true).unwrap(); + assert_eq!(outcome.status, "ok"); + + let listed = Command::new("git") + .args(["tag", "-l", "v1.0.0"]) + .current_dir(&origin) + .output() + .unwrap(); + assert_eq!(String::from_utf8_lossy(&listed.stdout).trim(), "v1.0.0"); + + let _ = std::fs::remove_dir_all(&origin); +} + fn clone_of(origin: &std::path::Path, local: &TempRepo, suffix: &str) -> TempRepo { let dir = local.dir.with_file_name(format!( "{}-{suffix}", diff --git a/apps/desktop/src/features/inspector/Inspector.tsx b/apps/desktop/src/features/inspector/Inspector.tsx index ddfee49a..9e23e890 100644 --- a/apps/desktop/src/features/inspector/Inspector.tsx +++ b/apps/desktop/src/features/inspector/Inspector.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { toast } from 'sonner'; import type { CommitFileInfo, CommitInfo } from '@angkorgit/core'; -import { Files, FolderTree, List, Search, X } from 'lucide-react'; +import { FolderOpen, FolderTree, List, Search, X } from 'lucide-react'; import { Hint, Button, cn } from '@angkorgit/design-system'; import { useGraph } from '@/features/graph/store'; import { useRepo } from '@/features/repository/store'; @@ -129,7 +129,7 @@ export function Inspector() { className={cn(fileView === 'all' && 'bg-surface-raised text-foreground')} onClick={() => setFileView('all')} > - + {(commit || commitError) && (