Skip to content

Commit e3b4e51

Browse files
committed
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.
1 parent a7da278 commit e3b4e51

File tree

7 files changed

+201
-37
lines changed

7 files changed

+201
-37
lines changed

Justfile.cross

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,10 +490,40 @@ push path="" branch="" force="false" yes="false" message="": check-initialized
490490
list: check-deps
491491
#!/usr/bin/env fish
492492
pushd "{{REPO_DIR}}" >/dev/null
493-
if test -d .git
494-
just cross _log info "Configured Remotes:"
495-
git remote -v | awk '{printf "%-20s %-50s %s\n", $1, $2, $3}'
496-
echo ""
493+
if test -f .git/cross/metadata.json
494+
# Get unique remotes used by patches
495+
set used_remotes (jq -r '.patches[].remote' .git/cross/metadata.json | sort -u)
496+
497+
if test (count $used_remotes) -gt 0
498+
just cross _log info "Configured Remotes:"
499+
printf "%-20s %s\n" "NAME" "URL"
500+
printf "%s\n" (string repeat -n 70 "-")
501+
502+
# Build grep pattern for used remotes
503+
set pattern (string join "|" $used_remotes)
504+
505+
# Get remotes, filter by used, deduplicate fetch/push
506+
git remote -v | grep -E "^($pattern)\s" | awk '
507+
{
508+
name=$1; url=$2; type=$3
509+
if (!(name in seen)) {
510+
seen[name] = url
511+
types[name] = type
512+
} else if (seen[name] != url) {
513+
# Different fetch/push URLs
514+
printf "%-20s %s\n", name, seen[name] " " types[name]
515+
printf "%-20s %s\n", name, url " " type
516+
delete seen[name]
517+
}
518+
}
519+
END {
520+
for (name in seen) {
521+
printf "%-20s %s\n", name, seen[name]
522+
}
523+
}
524+
'
525+
echo ""
526+
end
497527
end
498528

499529
if not test -f Crossfile

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@
1616
| **Upstream sync** | ✅ Bidirectional | ⚠️ Complex | ⚠️ Merge commits |
1717
| **Commit visibility** | ✅ In main repo | ❌ Separate | ✅ In main repo |
1818
| **Reproducibility** | ✅ Crossfile | ⚠️ .gitmodules | ⚠️ Manual |
19-
| **Native CLI** | ✅ Go (Primary) | ❌ N/A | ❌ Bash |
2019

21-
## Implementation Note
20+
## What it is not
21+
22+
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.
23+
24+
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.
25+
26+
## Implementation status
27+
28+
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.
2229

2330
The project provides three implementations, with **Go being the primary native version for production use.**
2431

TODO.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333

3434
- [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.
3535
- [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.
36-
- [ ] Implement `cross cut` to remove git remote repo registration from "cross use" command and ask user whether either remove all patches (like: cross remove)
36+
- [ ] 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)
3737
- [x] Re-implement `wt` (worktree) command in Go and Rust with full test coverage (align logic with Justfile).
38+
- [ ] Refactor `cross cd` to target local patched folder and output path (no subshell), supporting fzf.
39+
- [ ] 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".
3840
- [ ] Improve interactive `fzf` selection in native implementations.
3941

4042
## Known Issues (To FIX)

implementation-plan-release.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Implementation Plan - GitHub Releases & Architecture Refinement
2+
3+
## Context
4+
The `git-cross` project currently has three implementations:
5+
1. **Go:** A native implementation (Primary focus).
6+
2. **Shell/Justfile:** The original functional version.
7+
3. **Rust:** A native implementation (WIP).
8+
9+
To streamline distribution and maintenance, we are formalizing the preference for the Go implementation while maintaining the others for reference or future development.
10+
11+
## Architecture Decision Record (ADR) - Primary Implementation Choice
12+
**Decision: Prioritize Go as the primary native implementation for `git-cross`.**
13+
14+
### Rationale:
15+
- **Ecosystem:** Go has mature, high-level wrappers for both Git (`git-module`) and Rsync (`grsync`) that align well with our "wrapper" philosophy.
16+
- **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).
17+
- **Maintenance:** The Go implementation is currently more complete and matches the behavioral requirements of the PoC with less boilerplate than the current Rust approach.
18+
19+
### Consequences:
20+
1. **Rust Implementation:** Will be marked as **Work In Progress (WIP)** and experimental. Future feature development will land in Go first.
21+
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.
22+
23+
## Proposed Changes
24+
25+
### 1. Documentation & Status Updates
26+
- **`README.md`**: Update the "Implementation Note" to clearly state Go is the primary version and Rust is WIP.
27+
- **`src-rust/src/main.rs`**: Add a WIP warning to the CLI help description.
28+
- **`src-rust/Cargo.toml`**: Update metadata if needed.
29+
30+
### 2. GitHub Release Workflow Refinement
31+
- Update `.github/workflows/release.yml` to:
32+
- Build Go binaries using `goreleaser` (or a similar action).
33+
- Build Rust binaries for standard platforms.
34+
- Attach all binaries to the GitHub Release.
35+
- Use `softprops/action-gh-release` instead of the deprecated `actions/create-release`.
36+
37+
### 3. Implementation Details for Release Workflow
38+
#### Go Release (via GoReleaser):
39+
Create a `.goreleaser.yaml` in `src-go/` (or root) to handle:
40+
- Binaries: `git-cross` (from Go).
41+
- Platforms: `linux/amd64`, `linux/arm64`, `darwin/amd64`, `darwin/arm64`.
42+
43+
#### Rust Release:
44+
- Use `cross-rs` or simple `cargo build --release` in a matrix for Rust.
45+
46+
## Tasks
47+
- [ ] Update `README.md` status section.
48+
- [ ] Add WIP warning to Rust CLI.
49+
- [ ] Create `.goreleaser.yaml`.
50+
- [ ] Rewrite `.github/workflows/release.yml`.
51+
- [ ] Update `TODO.md` to reflect these documentation and release tasks.

src-go/main.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -631,27 +631,66 @@ func main() {
631631
Use: "list",
632632
Short: "Show all configured patches and remotes",
633633
RunE: func(cmd *cobra.Command, args []string) error {
634+
meta, _ := loadMetadata()
635+
636+
// Collect unique remote names from patches
637+
usedRemotes := make(map[string]bool)
638+
for _, p := range meta.Patches {
639+
usedRemotes[p.Remote] = true
640+
}
641+
634642
repo, err := git.Open(".")
635-
if err == nil {
643+
if err == nil && len(usedRemotes) > 0 {
636644
remotes, _ := git.NewCommand("remote", "-v").RunInDir(repo.Path())
637645
if len(remotes) > 0 {
638646
logInfo("Configured Remotes:")
639647
remotesStr := strings.TrimSpace(string(remotes))
640648
lines := strings.Split(remotesStr, "\n")
641-
table := tablewriter.NewWriter(os.Stdout)
642-
table.Header("NAME", "URL", "TYPE")
649+
650+
// Map to track fetch/push URLs per remote for deduplication
651+
type remoteInfo struct {
652+
fetch string
653+
push string
654+
}
655+
remoteMap := make(map[string]*remoteInfo)
656+
643657
for _, line := range lines {
644658
fields := strings.Fields(line)
645659
if len(fields) >= 3 {
646-
table.Append(fields[0], fields[1], fields[2])
660+
name := fields[0]
661+
url := fields[1]
662+
rtype := fields[2] // (fetch) or (push)
663+
664+
if !usedRemotes[name] {
665+
continue
666+
}
667+
668+
if remoteMap[name] == nil {
669+
remoteMap[name] = &remoteInfo{}
670+
}
671+
if strings.Contains(rtype, "fetch") {
672+
remoteMap[name].fetch = url
673+
} else if strings.Contains(rtype, "push") {
674+
remoteMap[name].push = url
675+
}
676+
}
677+
}
678+
679+
table := tablewriter.NewWriter(os.Stdout)
680+
table.Header("NAME", "URL")
681+
for name, info := range remoteMap {
682+
if info.fetch == info.push || info.push == "" {
683+
table.Append(name, info.fetch)
684+
} else {
685+
table.Append(name, info.fetch+" (fetch)")
686+
table.Append(name, info.push+" (push)")
647687
}
648688
}
649689
table.Render()
650690
fmt.Println()
651691
}
652692
}
653693

654-
meta, _ := loadMetadata()
655694
if len(meta.Patches) == 0 {
656695
fmt.Println("No patches configured.")
657696
return nil

src-rust/src/main.rs

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -726,37 +726,70 @@ fn main() -> Result<()> {
726726
}
727727
}
728728
Commands::List => {
729-
if let Ok(remotes) = run_cmd(&["git", "remote", "-v"]) {
730-
if !remotes.is_empty() {
731-
log_info("Configured Remotes:");
732-
#[derive(Tabled)]
733-
struct RemoteRow {
734-
name: String,
735-
url: String,
736-
#[tabled(rename = "type")]
737-
rtype: String,
738-
}
739-
let rows: Vec<RemoteRow> = remotes
740-
.lines()
741-
.filter_map(|line| {
729+
let metadata = load_metadata()?;
730+
731+
// Collect unique remote names from patches
732+
let used_remotes: std::collections::HashSet<String> =
733+
metadata.patches.iter().map(|p| p.remote.clone()).collect();
734+
735+
if !used_remotes.is_empty() {
736+
if let Ok(remotes) = run_cmd(&["git", "remote", "-v"]) {
737+
if !remotes.is_empty() {
738+
log_info("Configured Remotes:");
739+
740+
// Map to track fetch/push URLs per remote for deduplication
741+
let mut remote_map: std::collections::HashMap<String, (String, String)> =
742+
std::collections::HashMap::new();
743+
744+
for line in remotes.lines() {
742745
let fields: Vec<&str> = line.split_whitespace().collect();
743746
if fields.len() >= 3 {
744-
Some(RemoteRow {
745-
name: fields[0].to_string(),
746-
url: fields[1].to_string(),
747-
rtype: fields[2].to_string(),
748-
})
747+
let name = fields[0];
748+
let url = fields[1];
749+
let rtype = fields[2];
750+
751+
if !used_remotes.contains(name) {
752+
continue;
753+
}
754+
755+
let entry = remote_map.entry(name.to_string()).or_insert_with(|| (String::new(), String::new()));
756+
if rtype.contains("fetch") {
757+
entry.0 = url.to_string();
758+
} else if rtype.contains("push") {
759+
entry.1 = url.to_string();
760+
}
761+
}
762+
}
763+
764+
#[derive(Tabled)]
765+
struct RemoteRow {
766+
name: String,
767+
url: String,
768+
}
769+
let mut rows: Vec<RemoteRow> = Vec::new();
770+
for (name, (fetch, push)) in &remote_map {
771+
if fetch == push || push.is_empty() {
772+
rows.push(RemoteRow {
773+
name: name.clone(),
774+
url: fetch.clone(),
775+
});
749776
} else {
750-
None
777+
rows.push(RemoteRow {
778+
name: name.clone(),
779+
url: format!("{} (fetch)", fetch),
780+
});
781+
rows.push(RemoteRow {
782+
name: name.clone(),
783+
url: format!("{} (push)", push),
784+
});
751785
}
752-
})
753-
.collect();
754-
println!("{}", Table::new(rows));
755-
println!();
786+
}
787+
println!("{}", Table::new(rows));
788+
println!();
789+
}
756790
}
757791
}
758792

759-
let metadata = load_metadata()?;
760793
if metadata.patches.is_empty() {
761794
println!("No patches configured.");
762795
} else {

test/014_remove.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ if [ -d "vendor/app-rust" ]; then fail "vendor/app-rust still exists after remov
3939
if grep -q "vendor/app-rust" Crossfile; then fail "Crossfile still contains patch entry"; fi
4040
if grep -q "vendor/app-rust" .git/cross/metadata.json; then fail "Metadata still contains patch entry"; fi
4141

42-
# 4. Test list command (Go)
42+
# 4. Test list command (Go) - need active patch for remotes to show
4343
echo "## Testing 'list' command (Go)..."
44+
just cross patch repo1:src/lib vendor/list-test
4445
list_output=$("$REPO_ROOT/src-go/git-cross-go" list)
4546
if ! echo "$list_output" | grep -q "Configured Remotes"; then fail "Go list missing Remotes section"; fi
4647
if ! echo "$list_output" | grep -q "repo1"; then fail "Go list missing repo1 remote"; fi
48+
just cross remove vendor/list-test
4749

4850
# 5. Test Crossfile deduplication (Go)
4951
echo "## Testing Crossfile deduplication (Go)..."

0 commit comments

Comments
 (0)