kodegen_tools_git/operations/
remote.rs

1//! Git remote operations
2
3use crate::{GitError, GitResult, RepoHandle};
4use gix::bstr::ByteSlice;
5use gix::config::parse::section::ValueName;
6use std::borrow::Cow;
7
8/// Options for adding a remote
9#[derive(Debug, Clone)]
10pub struct RemoteAddOpts {
11    /// Remote name (e.g., "origin", "upstream")
12    pub name: String,
13    /// Remote URL (https, git, ssh, or file URL)
14    pub url: String,
15    /// Force add (overwrite if exists)
16    pub force: bool,
17}
18
19/// Add a new remote to repository configuration
20pub async fn add_remote(repo: RepoHandle, opts: RemoteAddOpts) -> GitResult<()> {
21    let mut repo_clone = repo.clone_inner();
22
23    tokio::task::spawn_blocking(move || {
24        // Validate URL format
25        if !is_valid_git_url(&opts.url) {
26            return Err(GitError::InvalidInput(format!(
27                "Invalid Git URL format: {}",
28                opts.url
29            )));
30        }
31
32        // Check if remote exists
33        if !opts.force
34            && repo_clone
35                .find_remote(opts.name.as_bytes().as_bstr())
36                .is_ok()
37        {
38            return Err(GitError::InvalidInput(format!(
39                "Remote '{}' already exists",
40                opts.name
41            )));
42        }
43
44        // Add remote via config
45        let mut config = repo_clone.config_snapshot_mut();
46
47        // Create remote section with remote name as subsection
48        let mut section = config
49            .new_section("remote", Some(Cow::Owned(opts.name.clone().into())))
50            .map_err(|e| GitError::Gix(Box::new(e)))?;
51
52        // Set remote.<name>.url = <url>
53        let url_key = ValueName::try_from("url").map_err(|e| GitError::Gix(Box::new(e)))?;
54        section.push(url_key, Some(opts.url.as_bytes().as_bstr()));
55
56        // Set remote.<name>.fetch = +refs/heads/*:refs/remotes/<name>/*
57        let fetch_key = ValueName::try_from("fetch").map_err(|e| GitError::Gix(Box::new(e)))?;
58        let refspec = format!("+refs/heads/*:refs/remotes/{}/*", opts.name);
59        section.push(fetch_key, Some(refspec.as_bytes().as_bstr()));
60
61        // Commit the config changes
62        drop(section);
63        config
64            .commit()
65            .map_err(|e| GitError::Gix(Box::new(e)))?;
66
67        Ok(())
68    })
69    .await
70    .map_err(|e| GitError::Gix(Box::new(e)))??;
71
72    Ok(())
73}
74
75/// Remove a remote from repository configuration
76pub async fn remove_remote(repo: RepoHandle, name: &str) -> GitResult<()> {
77    let mut repo_clone = repo.clone_inner();
78    let name = name.to_string();
79
80    tokio::task::spawn_blocking(move || {
81        // Check if remote exists
82        if repo_clone
83            .find_remote(name.as_bytes().as_bstr())
84            .is_err()
85        {
86            return Err(GitError::InvalidInput(format!(
87                "Remote '{}' does not exist",
88                name
89            )));
90        }
91
92        // Remove remote via config
93        let mut config = repo_clone.config_snapshot_mut();
94
95        let section_name = format!("remote.{}", name);
96
97        // Remove all keys under the remote section
98        if config.remove_section(&section_name, None).is_none() {
99            return Err(GitError::InvalidInput(format!(
100                "Remote '{}' not found in configuration",
101                name
102            )));
103        }
104
105        // Commit the config changes
106        config
107            .commit()
108            .map_err(|e| GitError::Gix(Box::new(e)))?;
109
110        Ok(())
111    })
112    .await
113    .map_err(|e| GitError::Gix(Box::new(e)))??;
114
115    Ok(())
116}
117
118/// Validate Git URL format
119fn is_valid_git_url(url: &str) -> bool {
120    url.starts_with("https://")
121        || url.starts_with("http://")
122        || url.starts_with("git://")
123        || url.starts_with("ssh://")
124        || url.starts_with("file://")
125        || (url.contains('@') && url.contains(':')) // SSH format like git@github.com:user/repo.git
126}