From e3b4e517b061f349f4b9910472d7f6bfa0ce0091 Mon Sep 17 00:00:00 2001
From: "p.michalec"
Date: Fri, 2 Jan 2026 09:43:47 +0100
Subject: [PATCH] Show only used remotes in list
Filter list remotes to those referenced by patches to reduce noise.
Deduplicate fetch/push URLs in Justfile, Go, and Rust outputs.
Update README wording, add a release plan doc, and adjust remove test.
Update TODO items to track prune and cd follow-ups.
---
Justfile.cross | 38 ++++++++++++++--
README.md | 11 ++++-
TODO.md | 4 +-
implementation-plan-release.md | 51 +++++++++++++++++++++
src-go/main.go | 49 +++++++++++++++++---
src-rust/src/main.rs | 81 ++++++++++++++++++++++++----------
test/014_remove.sh | 4 +-
7 files changed, 201 insertions(+), 37 deletions(-)
create mode 100644 implementation-plan-release.md
diff --git a/Justfile.cross b/Justfile.cross
index 6d4254fc8..4d1affb9e 100644
--- a/Justfile.cross
+++ b/Justfile.cross
@@ -490,10 +490,40 @@ push path="" branch="" force="false" yes="false" message="": check-initialized
list: check-deps
#!/usr/bin/env fish
pushd "{{REPO_DIR}}" >/dev/null
- if test -d .git
- just cross _log info "Configured Remotes:"
- git remote -v | awk '{printf "%-20s %-50s %s\n", $1, $2, $3}'
- echo ""
+ if test -f .git/cross/metadata.json
+ # Get unique remotes used by patches
+ set used_remotes (jq -r '.patches[].remote' .git/cross/metadata.json | sort -u)
+
+ if test (count $used_remotes) -gt 0
+ just cross _log info "Configured Remotes:"
+ printf "%-20s %s\n" "NAME" "URL"
+ printf "%s\n" (string repeat -n 70 "-")
+
+ # Build grep pattern for used remotes
+ set pattern (string join "|" $used_remotes)
+
+ # Get remotes, filter by used, deduplicate fetch/push
+ git remote -v | grep -E "^($pattern)\s" | awk '
+ {
+ name=$1; url=$2; type=$3
+ if (!(name in seen)) {
+ seen[name] = url
+ types[name] = type
+ } else if (seen[name] != url) {
+ # Different fetch/push URLs
+ printf "%-20s %s\n", name, seen[name] " " types[name]
+ printf "%-20s %s\n", name, url " " type
+ delete seen[name]
+ }
+ }
+ END {
+ for (name in seen) {
+ printf "%-20s %s\n", name, seen[name]
+ }
+ }
+ '
+ echo ""
+ end
end
if not test -f Crossfile
diff --git a/README.md b/README.md
index 998e78bb9..54f38b11a 100644
--- a/README.md
+++ b/README.md
@@ -16,9 +16,16 @@
| **Upstream sync** | ✅ Bidirectional | ⚠️ Complex | ⚠️ Merge commits |
| **Commit visibility** | ✅ In main repo | ❌ Separate | ✅ In main repo |
| **Reproducibility** | ✅ Crossfile | ⚠️ .gitmodules | ⚠️ Manual |
-| **Native CLI** | ✅ Go (Primary) | ❌ N/A | ❌ Bash |
-## Implementation Note
+## What it is not
+
+Git-cross is not a replacement for `git-subrepo` or `git-submodule`. -- It provides an alternative approach and simplifies otherwise manual and complex git workflow behind into intuitive commands.
+
+Git-cross does not directly link external repositories to your main repository. -- It provides separate worktrees for each upstream patch, and help with sync to local repository.
+
+## Implementation status
+
+The project is still in early days and Work In Progress. Just/Golang versions are tested by the author on best-effort basis. Most of the commands and structure of "Crossfile" is already freezed.
The project provides three implementations, with **Go being the primary native version for production use.**
diff --git a/TODO.md b/TODO.md
index 5d2c33b0f..c47720f63 100644
--- a/TODO.md
+++ b/TODO.md
@@ -33,8 +33,10 @@
- [x] `cross list` comand shall either print all cross remote repositories (REMOTE (alias), GIT URL) in separate table above the table with patches. Or directly inline with each patch.
- [x] Implement `cross remove` patch, to remove local_pathch patch and it's worktree. Finally clean up the Metadata an Crossfile. Once physically removed, `git worktree prune` will clenaup git itself.
-- [ ] Implement `cross cut` to remove git remote repo registration from "cross use" command and ask user whether either remove all patches (like: cross remove)
+- [ ] Implement `cross prune [remote name]` to remove git remote repo registration from "cross use" command and ask user whether either remove all git remotes without active cross patches (like after: cross remove), then `git worktree prune` to remove all worktrees. optional argument (an remote repo alias/name would enforce either removal of all it's patches altogther with worktrees and remotes)
- [x] Re-implement `wt` (worktree) command in Go and Rust with full test coverage (align logic with Justfile).
+- [ ] Refactor `cross cd` to target local patched folder and output path (no subshell), supporting fzf.
+- [ ] Review and propose implementation (tool and test) to be able patch even single file. If not easily possible without major refactoring, then evaluate new command "patch-file".
- [ ] Improve interactive `fzf` selection in native implementations.
## Known Issues (To FIX)
diff --git a/implementation-plan-release.md b/implementation-plan-release.md
new file mode 100644
index 000000000..e5011f0e2
--- /dev/null
+++ b/implementation-plan-release.md
@@ -0,0 +1,51 @@
+# Implementation Plan - GitHub Releases & Architecture Refinement
+
+## Context
+The `git-cross` project currently has three implementations:
+1. **Go:** A native implementation (Primary focus).
+2. **Shell/Justfile:** The original functional version.
+3. **Rust:** A native implementation (WIP).
+
+To streamline distribution and maintenance, we are formalizing the preference for the Go implementation while maintaining the others for reference or future development.
+
+## Architecture Decision Record (ADR) - Primary Implementation Choice
+**Decision: Prioritize Go as the primary native implementation for `git-cross`.**
+
+### Rationale:
+- **Ecosystem:** Go has mature, high-level wrappers for both Git (`git-module`) and Rsync (`grsync`) that align well with our "wrapper" philosophy.
+- **Distribution:** Go's static linking and cross-compilation simplicity make it ideal for a developer tool that needs to run in various environments (Mac, Linux, CI).
+- **Maintenance:** The Go implementation is currently more complete and matches the behavioral requirements of the PoC with less boilerplate than the current Rust approach.
+
+### Consequences:
+1. **Rust Implementation:** Will be marked as **Work In Progress (WIP)** and experimental. Future feature development will land in Go first.
+2. **Builds & Releases:** Focus on providing pre-built binaries for Go across platforms (Linux amd64/arm64, Darwin amd64/arm64). Rust binaries will be built but marked as experimental.
+
+## Proposed Changes
+
+### 1. Documentation & Status Updates
+- **`README.md`**: Update the "Implementation Note" to clearly state Go is the primary version and Rust is WIP.
+- **`src-rust/src/main.rs`**: Add a WIP warning to the CLI help description.
+- **`src-rust/Cargo.toml`**: Update metadata if needed.
+
+### 2. GitHub Release Workflow Refinement
+- Update `.github/workflows/release.yml` to:
+ - Build Go binaries using `goreleaser` (or a similar action).
+ - Build Rust binaries for standard platforms.
+ - Attach all binaries to the GitHub Release.
+ - Use `softprops/action-gh-release` instead of the deprecated `actions/create-release`.
+
+### 3. Implementation Details for Release Workflow
+#### Go Release (via GoReleaser):
+Create a `.goreleaser.yaml` in `src-go/` (or root) to handle:
+- Binaries: `git-cross` (from Go).
+- Platforms: `linux/amd64`, `linux/arm64`, `darwin/amd64`, `darwin/arm64`.
+
+#### Rust Release:
+- Use `cross-rs` or simple `cargo build --release` in a matrix for Rust.
+
+## Tasks
+- [ ] Update `README.md` status section.
+- [ ] Add WIP warning to Rust CLI.
+- [ ] Create `.goreleaser.yaml`.
+- [ ] Rewrite `.github/workflows/release.yml`.
+- [ ] Update `TODO.md` to reflect these documentation and release tasks.
diff --git a/src-go/main.go b/src-go/main.go
index 39ec87766..ebe0e8619 100644
--- a/src-go/main.go
+++ b/src-go/main.go
@@ -631,19 +631,59 @@ func main() {
Use: "list",
Short: "Show all configured patches and remotes",
RunE: func(cmd *cobra.Command, args []string) error {
+ meta, _ := loadMetadata()
+
+ // Collect unique remote names from patches
+ usedRemotes := make(map[string]bool)
+ for _, p := range meta.Patches {
+ usedRemotes[p.Remote] = true
+ }
+
repo, err := git.Open(".")
- if err == nil {
+ if err == nil && len(usedRemotes) > 0 {
remotes, _ := git.NewCommand("remote", "-v").RunInDir(repo.Path())
if len(remotes) > 0 {
logInfo("Configured Remotes:")
remotesStr := strings.TrimSpace(string(remotes))
lines := strings.Split(remotesStr, "\n")
- table := tablewriter.NewWriter(os.Stdout)
- table.Header("NAME", "URL", "TYPE")
+
+ // Map to track fetch/push URLs per remote for deduplication
+ type remoteInfo struct {
+ fetch string
+ push string
+ }
+ remoteMap := make(map[string]*remoteInfo)
+
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) >= 3 {
- table.Append(fields[0], fields[1], fields[2])
+ name := fields[0]
+ url := fields[1]
+ rtype := fields[2] // (fetch) or (push)
+
+ if !usedRemotes[name] {
+ continue
+ }
+
+ if remoteMap[name] == nil {
+ remoteMap[name] = &remoteInfo{}
+ }
+ if strings.Contains(rtype, "fetch") {
+ remoteMap[name].fetch = url
+ } else if strings.Contains(rtype, "push") {
+ remoteMap[name].push = url
+ }
+ }
+ }
+
+ table := tablewriter.NewWriter(os.Stdout)
+ table.Header("NAME", "URL")
+ for name, info := range remoteMap {
+ if info.fetch == info.push || info.push == "" {
+ table.Append(name, info.fetch)
+ } else {
+ table.Append(name, info.fetch+" (fetch)")
+ table.Append(name, info.push+" (push)")
}
}
table.Render()
@@ -651,7 +691,6 @@ func main() {
}
}
- meta, _ := loadMetadata()
if len(meta.Patches) == 0 {
fmt.Println("No patches configured.")
return nil
diff --git a/src-rust/src/main.rs b/src-rust/src/main.rs
index 39e560dca..740d8bb3b 100644
--- a/src-rust/src/main.rs
+++ b/src-rust/src/main.rs
@@ -726,37 +726,70 @@ fn main() -> Result<()> {
}
}
Commands::List => {
- if let Ok(remotes) = run_cmd(&["git", "remote", "-v"]) {
- if !remotes.is_empty() {
- log_info("Configured Remotes:");
- #[derive(Tabled)]
- struct RemoteRow {
- name: String,
- url: String,
- #[tabled(rename = "type")]
- rtype: String,
- }
- let rows: Vec = remotes
- .lines()
- .filter_map(|line| {
+ let metadata = load_metadata()?;
+
+ // Collect unique remote names from patches
+ let used_remotes: std::collections::HashSet =
+ metadata.patches.iter().map(|p| p.remote.clone()).collect();
+
+ if !used_remotes.is_empty() {
+ if let Ok(remotes) = run_cmd(&["git", "remote", "-v"]) {
+ if !remotes.is_empty() {
+ log_info("Configured Remotes:");
+
+ // Map to track fetch/push URLs per remote for deduplication
+ let mut remote_map: std::collections::HashMap =
+ std::collections::HashMap::new();
+
+ for line in remotes.lines() {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() >= 3 {
- Some(RemoteRow {
- name: fields[0].to_string(),
- url: fields[1].to_string(),
- rtype: fields[2].to_string(),
- })
+ let name = fields[0];
+ let url = fields[1];
+ let rtype = fields[2];
+
+ if !used_remotes.contains(name) {
+ continue;
+ }
+
+ let entry = remote_map.entry(name.to_string()).or_insert_with(|| (String::new(), String::new()));
+ if rtype.contains("fetch") {
+ entry.0 = url.to_string();
+ } else if rtype.contains("push") {
+ entry.1 = url.to_string();
+ }
+ }
+ }
+
+ #[derive(Tabled)]
+ struct RemoteRow {
+ name: String,
+ url: String,
+ }
+ let mut rows: Vec = Vec::new();
+ for (name, (fetch, push)) in &remote_map {
+ if fetch == push || push.is_empty() {
+ rows.push(RemoteRow {
+ name: name.clone(),
+ url: fetch.clone(),
+ });
} else {
- None
+ rows.push(RemoteRow {
+ name: name.clone(),
+ url: format!("{} (fetch)", fetch),
+ });
+ rows.push(RemoteRow {
+ name: name.clone(),
+ url: format!("{} (push)", push),
+ });
}
- })
- .collect();
- println!("{}", Table::new(rows));
- println!();
+ }
+ println!("{}", Table::new(rows));
+ println!();
+ }
}
}
- let metadata = load_metadata()?;
if metadata.patches.is_empty() {
println!("No patches configured.");
} else {
diff --git a/test/014_remove.sh b/test/014_remove.sh
index 92f2b5ad0..66504d4e8 100755
--- a/test/014_remove.sh
+++ b/test/014_remove.sh
@@ -39,11 +39,13 @@ if [ -d "vendor/app-rust" ]; then fail "vendor/app-rust still exists after remov
if grep -q "vendor/app-rust" Crossfile; then fail "Crossfile still contains patch entry"; fi
if grep -q "vendor/app-rust" .git/cross/metadata.json; then fail "Metadata still contains patch entry"; fi
-# 4. Test list command (Go)
+# 4. Test list command (Go) - need active patch for remotes to show
echo "## Testing 'list' command (Go)..."
+just cross patch repo1:src/lib vendor/list-test
list_output=$("$REPO_ROOT/src-go/git-cross-go" list)
if ! echo "$list_output" | grep -q "Configured Remotes"; then fail "Go list missing Remotes section"; fi
if ! echo "$list_output" | grep -q "repo1"; then fail "Go list missing repo1 remote"; fi
+just cross remove vendor/list-test
# 5. Test Crossfile deduplication (Go)
echo "## Testing Crossfile deduplication (Go)..."