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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.7.0] - 2026-04-23

### Added
- Install channel persisted in registry: `--channel` stored per package
- `gitclaw update` respects stored channel when checking for newer releases
- Optional `channel` field on `InstalledPackage` (backward compatible)
- New tests in `tests/channel_persist.rs`

## [0.6.0] - 2026-04-23

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "gitclaw"
version = "0.6.0"
version = "0.7.0"
edition = "2021"

[[bin]]
Expand Down
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Project-scoped installation isolated from the global registry:
gitclaw install --local user/repo
```

## 0.6.0 — Advanced Features ✅
## 0.6.0 — Advanced Features ✅

**Release channels**

Expand All @@ -76,7 +76,7 @@ gitclaw export > deps.toml
gitclaw import deps.toml
```

## 0.7.0 — Platform Integration
## 0.7.0 — Platform Integration

**Package manager awareness**

Expand Down
21 changes: 19 additions & 2 deletions src/core/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ pub async fn handle_install(
install_dir: pkg_install_dir.clone(),
asset_name: asset.name.clone(),
identifier: repo.clone(),
channel: channel.map(|c| c.to_string()),
};

reg.add(pkg);
Expand Down Expand Up @@ -230,7 +231,23 @@ async fn update_one(package: &str, config: &Config) -> Result<()> {
}

let client = GithubClient::new(config.github_token.clone())?;
let latest = client.get_release(&owner, &repo, "latest").await?;

let ch = match installed.channel.as_deref() {
Some(c) => Some(c.parse::<crate::core::channel::Channel>()?),
None => None,
};

let latest = match ch {
Some(channel) => {
let releases = client.get_releases(&owner, &repo).await?;
let filtered = crate::core::channel::filter_releases(&releases, channel, None);
if filtered.is_empty() {
bail!("No {} release found for {}/{}.", channel, owner, repo);
}
filtered.into_iter().next().unwrap()
}
None => client.get_release(&owner, &repo, "latest").await?,
};

if latest.tag_name == installed.version {
if !config.output.quiet {
Expand All @@ -248,7 +265,7 @@ async fn update_one(package: &str, config: &Config) -> Result<()> {
}

crate::core::registry::uninstall(package, &config.install_dir, config)?;
handle_install(package, false, false, false, config, None).await
handle_install(package, false, false, false, config, ch).await
}

async fn update_all(config: &Config) -> Result<()> {
Expand Down
1 change: 1 addition & 0 deletions src/core/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ mod tests {
install_dir: PathBuf::from("/tmp/test"),
asset_name: asset.to_string(),
identifier: repo.to_string(),
channel: None,
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub struct InstalledPackage {
pub asset_name: String,
#[serde(default)]
pub identifier: String,
#[serde(default)]
pub channel: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
Expand Down
100 changes: 100 additions & 0 deletions tests/channel_persist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::path::PathBuf;

use gitclaw::registry::{InstalledPackage, Registry};
use tempfile::TempDir;

fn make_pkg_with_channel(
name: &str,
owner: &str,
repo: &str,
version: &str,
channel: Option<&str>,
) -> InstalledPackage {
InstalledPackage {
name: name.to_string(),
owner: owner.to_string(),
repo: repo.to_string(),
version: version.to_string(),
installed_at: "2026-01-01T00:00:00Z".to_string(),
binary_path: PathBuf::from("/tmp/test"),
install_dir: PathBuf::from("/tmp/test"),
asset_name: format!("{}.tar.gz", repo),
identifier: repo.to_string(),
channel: channel.map(|s| s.to_string()),
}
}

#[test]
fn test_installed_package_with_channel() {
let pkg = make_pkg_with_channel(
"user/repo",
"user",
"repo",
"1.0.0-nightly",
Some("nightly"),
);
assert_eq!(pkg.channel, Some("nightly".to_string()));
}

#[test]
fn test_installed_package_without_channel() {
let pkg = make_pkg_with_channel("user/repo", "user", "repo", "1.0.0", None);
assert_eq!(pkg.channel, None);
}

#[test]
fn test_registry_save_load_with_channel() {
let dir = TempDir::new().unwrap();
let reg_path = dir.path().join("registry.toml");
std::fs::create_dir_all(dir.path()).unwrap();

let mut reg = Registry::load_from(&reg_path).unwrap();
reg.add(make_pkg_with_channel(
"BurntSushi/ripgrep",
"BurntSushi",
"ripgrep",
"14.0.0-nightly",
Some("nightly"),
));
reg.add(make_pkg_with_channel(
"sharkdp/fd",
"sharkdp",
"fd",
"8.7.0",
None,
));
reg.save().unwrap();

let loaded = Registry::load_from(&reg_path).unwrap();
let rg = loaded.packages.get("BurntSushi/ripgrep").unwrap();
assert_eq!(rg.channel, Some("nightly".to_string()));

let fd = loaded.packages.get("sharkdp/fd").unwrap();
assert_eq!(fd.channel, None);
}

#[test]
fn test_registry_backward_compat_no_channel_field() {
let dir = TempDir::new().unwrap();
let reg_path = dir.path().join("registry.toml");

let toml_str = r#"
[packages."user/repo"]
name = "user/repo"
owner = "user"
repo = "repo"
version = "1.0.0"
installed_at = "2026-01-01T00:00:00Z"
binary_path = "/tmp/test"
install_dir = "/tmp/test"
asset_name = "repo.tar.gz"
identifier = "repo"
"#;

std::fs::create_dir_all(dir.path()).unwrap();
std::fs::write(&reg_path, toml_str).unwrap();

let reg = Registry::load_from(&reg_path).unwrap();
let pkg = reg.packages.get("user/repo").unwrap();
assert_eq!(pkg.channel, None);
}
1 change: 1 addition & 0 deletions tests/export_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ fn sample_pkg(name: &str, owner: &str, repo: &str, version: &str) -> InstalledPa
install_dir: PathBuf::from("/tmp/install"),
asset_name: format!("{}-{}.tar.gz", repo, version),
identifier: repo.to_string(),
channel: None,
}
}

Expand Down
2 changes: 1 addition & 1 deletion tests/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ fn create_test_zip(dir: &TempDir, files: &[(&str, &[u8])]) -> std::path::PathBuf

for (name, content) in files {
writer.start_file(*name, options).unwrap();
writer.write_all(*content).unwrap();
writer.write_all(content).unwrap();
}

writer.finish().unwrap();
Expand Down
1 change: 1 addition & 0 deletions tests/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ fn test_local_registry_load_save() {
install_dir: local_dir.join("packages").join("sharkdp").join("bat"),
asset_name: "bat-v0.24.0-x86_64-linux.tar.gz".to_string(),
identifier: "bat".to_string(),
channel: None,
});
reg.save().unwrap();

Expand Down
1 change: 1 addition & 0 deletions tests/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fn make_pkg(name: &str, owner: &str, repo: &str, version: &str, asset: &str) ->
install_dir: PathBuf::from("/tmp/test"),
asset_name: asset.to_string(),
identifier: repo.to_string(),
channel: None,
}
}

Expand Down
10 changes: 9 additions & 1 deletion tests/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fn test_installed_package_struct() {
install_dir: PathBuf::from("/home/user/.gitclaw/packages/BurntSushi/ripgrep"),
asset_name: "ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz".to_string(),
identifier: "ripgrep".to_string(),
channel: None,
};

assert_eq!(pkg.name, "BurntSushi/ripgrep");
Expand Down Expand Up @@ -45,6 +46,7 @@ fn test_registry_add() {
install_dir: PathBuf::from("/home/user/.gitclaw/packages/test/package"),
asset_name: "package-1.0.0.tar.gz".to_string(),
identifier: "package".to_string(),
channel: None,
};

registry.packages.insert(pkg.name.clone(), pkg);
Expand All @@ -68,6 +70,7 @@ fn test_registry_remove() {
install_dir: PathBuf::from("/home/user/.gitclaw/packages/user1/pkg1"),
asset_name: "pkg1.tar.gz".to_string(),
identifier: "pkg1".to_string(),
channel: None,
},
);

Expand All @@ -83,6 +86,7 @@ fn test_registry_remove() {
install_dir: PathBuf::from("/home/user/.gitclaw/packages/user2/pkg2"),
asset_name: "pkg2.tar.gz".to_string(),
identifier: "pkg2".to_string(),
channel: None,
},
);

Expand All @@ -108,6 +112,7 @@ fn test_registry_get() {
install_dir: PathBuf::from("/home/user/.gitclaw/packages/test/pkg"),
asset_name: "pkg.tar.gz".to_string(),
identifier: "pkg".to_string(),
channel: None,
};

registry.packages.insert(pkg.name.clone(), pkg);
Expand All @@ -116,7 +121,7 @@ fn test_registry_get() {
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().version, "1.0.0");

assert!(registry.packages.get("nonexistent/pkg").is_none());
assert!(!registry.packages.contains_key("nonexistent/pkg"));
}

#[test]
Expand All @@ -137,6 +142,7 @@ fn test_registry_is_installed() {
install_dir: PathBuf::from("/home/user/.gitclaw/packages/test/package"),
asset_name: "package.tar.gz".to_string(),
identifier: "package".to_string(),
channel: None,
},
);

Expand All @@ -159,6 +165,7 @@ fn test_serialize_deserialize() {
install_dir: PathBuf::from("/home/user/.gitclaw/packages/test/pkg"),
asset_name: "pkg.tar.gz".to_string(),
identifier: "pkg".to_string(),
channel: None,
},
);

Expand Down Expand Up @@ -221,6 +228,7 @@ fn test_registry_crud() {
install_dir: PathBuf::from("/tmp/install"),
asset_name: "tool.tar.gz".to_string(),
identifier: "repo".to_string(),
channel: None,
};

assert!(!reg.is_installed("user/repo"));
Expand Down
Loading