Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions Justfile.cross
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**

Expand Down
4 changes: 3 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions implementation-plan-release.md
Original file line number Diff line number Diff line change
@@ -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.
49 changes: 44 additions & 5 deletions src-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,27 +631,66 @@ 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()
fmt.Println()
}
}

meta, _ := loadMetadata()
if len(meta.Patches) == 0 {
fmt.Println("No patches configured.")
return nil
Expand Down
81 changes: 57 additions & 24 deletions src-rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RemoteRow> = remotes
.lines()
.filter_map(|line| {
let metadata = load_metadata()?;

// Collect unique remote names from patches
let used_remotes: std::collections::HashSet<String> =
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<String, (String, String)> =
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<RemoteRow> = 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 {
Expand Down
4 changes: 3 additions & 1 deletion test/014_remove.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)..."
Expand Down