diff --git a/CHANGELOG.md b/CHANGELOG.md index df0b561..c644967 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 3cf39d5..e5f1132 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 a8a277c..97fd1ab 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 4903edc..1f1f56e 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 ddfee49..9e23e89 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) && (