Skip to content

Commit c674464

Browse files
aster-voidclaude
andcommitted
git: unify local and remote repo handling
Use git operations for both local and remote repositories: - Clone via `git clone` instead of rsync - Sync via `git pull --ff-only` - Job dirs via `git archive` (no rsync fallback) This removes the rsync dependency and ensures only committed changes are used for local repos. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent e3c3cfb commit c674464

File tree

2 files changed

+58
-133
lines changed

2 files changed

+58
-133
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
8686

8787
### Startup
8888
1. Parse CLI args (repo, interval)
89-
2. Clone if remote URL, otherwise use local path
89+
2. Clone repo to cache via `git clone` (both local and remote)
9090
3. Load config from `rollcron.yaml`
9191
4. Sync job directories via `git archive` (using job ID)
9292
5. Start pull task + scheduler

src/git.rs

Lines changed: 57 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -10,91 +10,49 @@ pub fn ensure_repo(source: &str) -> Result<PathBuf> {
1010
}
1111

1212
if cache_dir.exists() {
13-
sync_repo(source, &cache_dir)?;
13+
sync_repo(&cache_dir)?;
1414
} else {
1515
clone_repo(source, &cache_dir)?;
1616
}
1717

1818
Ok(cache_dir)
1919
}
2020

21-
fn is_remote(source: &str) -> bool {
22-
source.starts_with("https://")
23-
|| source.starts_with("git@")
24-
|| source.starts_with("ssh://")
25-
|| source.starts_with("git://")
26-
}
27-
2821
fn clone_repo(source: &str, dest: &Path) -> Result<()> {
29-
if is_remote(source) {
30-
let dest_str = dest
31-
.to_str()
32-
.context("Destination path contains invalid UTF-8")?;
33-
let output = Command::new("git")
34-
.args(["clone", source, dest_str])
35-
.output()?;
36-
37-
if !output.status.success() {
38-
let stderr = String::from_utf8_lossy(&output.stderr);
39-
anyhow::bail!("git clone failed: {}", stderr);
40-
}
41-
} else {
42-
// Local: rsync entire directory (including uncommitted changes)
43-
rsync_local(source, dest)?;
44-
}
45-
46-
Ok(())
47-
}
22+
let dest_str = dest
23+
.to_str()
24+
.context("Destination path contains invalid UTF-8")?;
25+
let output = Command::new("git")
26+
.args(["clone", source, dest_str])
27+
.output()?;
4828

49-
fn sync_repo(source: &str, dest: &Path) -> Result<()> {
50-
if is_remote(source) {
51-
// Remote: git pull
52-
let has_upstream = Command::new("git")
53-
.args(["rev-parse", "--abbrev-ref", "@{upstream}"])
54-
.current_dir(dest)
55-
.output()
56-
.map(|o| o.status.success())
57-
.unwrap_or(false);
58-
59-
if has_upstream {
60-
let output = Command::new("git")
61-
.args(["pull", "--ff-only"])
62-
.current_dir(dest)
63-
.output()?;
64-
65-
if !output.status.success() {
66-
let stderr = String::from_utf8_lossy(&output.stderr);
67-
anyhow::bail!("git pull failed: {}", stderr);
68-
}
69-
}
70-
} else {
71-
// Local: rsync (syncs uncommitted changes too)
72-
rsync_local(source, dest)?;
29+
if !output.status.success() {
30+
let stderr = String::from_utf8_lossy(&output.stderr);
31+
anyhow::bail!("git clone failed: {}", stderr);
7332
}
7433

7534
Ok(())
7635
}
7736

78-
fn rsync_local(source: &str, dest: &Path) -> Result<()> {
79-
std::fs::create_dir_all(dest)?;
37+
fn sync_repo(dest: &Path) -> Result<()> {
38+
// git clone sets up tracking branches for both local and remote repos
39+
let has_upstream = Command::new("git")
40+
.args(["rev-parse", "--abbrev-ref", "@{upstream}"])
41+
.current_dir(dest)
42+
.output()
43+
.map(|o| o.status.success())
44+
.unwrap_or(false);
8045

81-
let dest_str = dest
82-
.to_str()
83-
.context("Destination path contains invalid UTF-8")?;
84-
let output = Command::new("rsync")
85-
.args([
86-
"-a",
87-
"--delete",
88-
"--exclude",
89-
".git",
90-
&format!("{}/", source),
91-
dest_str,
92-
])
93-
.output()?;
46+
if has_upstream {
47+
let output = Command::new("git")
48+
.args(["pull", "--ff-only"])
49+
.current_dir(dest)
50+
.output()?;
9451

95-
if !output.status.success() {
96-
let stderr = String::from_utf8_lossy(&output.stderr);
97-
anyhow::bail!("rsync failed: {}", stderr);
52+
if !output.status.success() {
53+
let stderr = String::from_utf8_lossy(&output.stderr);
54+
anyhow::bail!("git pull failed: {}", stderr);
55+
}
9856
}
9957

10058
Ok(())
@@ -139,9 +97,6 @@ pub fn get_job_dir(sot_path: &Path, job_id: &str) -> PathBuf {
13997
}
14098

14199
pub fn sync_to_job_dir(sot_path: &Path, job_dir: &Path) -> Result<()> {
142-
let sot_str = sot_path
143-
.to_str()
144-
.context("Source path contains invalid UTF-8")?;
145100
let job_dir_str = job_dir
146101
.to_str()
147102
.context("Job directory path contains invalid UTF-8")?;
@@ -158,68 +113,46 @@ pub fn sync_to_job_dir(sot_path: &Path, job_dir: &Path) -> Result<()> {
158113
}
159114
std::fs::create_dir_all(&temp_dir)?;
160115

161-
// Check if .git exists (remote repos have it, local rsync'd repos don't)
162-
if sot_path.join(".git").exists() {
163-
// Use git archive for git repos
164-
let archive = Command::new("git")
165-
.args(["archive", "HEAD"])
166-
.current_dir(sot_path)
167-
.output()?;
168-
169-
if !archive.status.success() {
170-
std::fs::remove_dir_all(&temp_dir)?;
171-
let stderr = String::from_utf8_lossy(&archive.stderr);
172-
anyhow::bail!("git archive failed: {}", stderr);
173-
}
116+
// Use git archive for all repos (both local and remote use git clone now)
117+
let archive = Command::new("git")
118+
.args(["archive", "HEAD"])
119+
.current_dir(sot_path)
120+
.output()?;
174121

175-
// Extract with security flags to prevent path traversal
176-
let mut extract = Command::new("tar")
177-
.args(["--no-absolute-file-names", "-x"])
178-
.current_dir(&temp_dir)
179-
.stdin(std::process::Stdio::piped())
180-
.spawn()?;
181-
182-
{
183-
use std::io::Write;
184-
let stdin = extract
185-
.stdin
186-
.as_mut()
187-
.context("Failed to open tar stdin")?;
188-
stdin.write_all(&archive.stdout)?;
189-
}
122+
if !archive.status.success() {
123+
std::fs::remove_dir_all(&temp_dir)?;
124+
let stderr = String::from_utf8_lossy(&archive.stderr);
125+
anyhow::bail!("git archive failed: {}", stderr);
126+
}
190127

191-
let status = extract.wait()?;
192-
if !status.success() {
193-
std::fs::remove_dir_all(&temp_dir)?;
194-
anyhow::bail!("tar extraction failed with exit code: {:?}", status.code());
195-
}
196-
} else {
197-
// For non-git dirs (rsync'd local repos), use rsync
198-
let output = Command::new("rsync")
199-
.args([
200-
"-a",
201-
"--delete",
202-
&format!("{}/", sot_str),
203-
temp_dir_str,
204-
])
205-
.output()?;
128+
// Extract with security flags to prevent path traversal
129+
let mut extract = Command::new("tar")
130+
.args(["--no-absolute-file-names", "-x"])
131+
.current_dir(&temp_dir)
132+
.stdin(std::process::Stdio::piped())
133+
.spawn()?;
134+
135+
{
136+
use std::io::Write;
137+
let stdin = extract
138+
.stdin
139+
.as_mut()
140+
.context("Failed to open tar stdin")?;
141+
stdin.write_all(&archive.stdout)?;
142+
}
206143

207-
if !output.status.success() {
208-
std::fs::remove_dir_all(&temp_dir)?;
209-
let stderr = String::from_utf8_lossy(&output.stderr);
210-
anyhow::bail!("rsync failed: {}", stderr);
211-
}
144+
let status = extract.wait()?;
145+
if !status.success() {
146+
std::fs::remove_dir_all(&temp_dir)?;
147+
anyhow::bail!("tar extraction failed with exit code: {:?}", status.code());
212148
}
213149

214150
// Atomic swap: remove old, rename temp to target
215151
if job_dir.exists() {
216152
std::fs::remove_dir_all(job_dir)?;
217153
}
218154
std::fs::rename(&temp_dir, job_dir).with_context(|| {
219-
format!(
220-
"Failed to rename {} to {}",
221-
temp_dir_str, job_dir_str
222-
)
155+
format!("Failed to rename {} to {}", temp_dir_str, job_dir_str)
223156
})?;
224157

225158
Ok(())
@@ -229,14 +162,6 @@ pub fn sync_to_job_dir(sot_path: &Path, job_dir: &Path) -> Result<()> {
229162
mod tests {
230163
use super::*;
231164

232-
#[test]
233-
fn detect_remote_urls() {
234-
assert!(is_remote("https://github.com/user/repo"));
235-
assert!(is_remote("[email protected]:user/repo.git"));
236-
assert!(!is_remote("/home/user/repo"));
237-
assert!(!is_remote("."));
238-
}
239-
240165
#[test]
241166
fn cache_dir_from_url() {
242167
let dir = get_cache_dir("https://github.com/user/myrepo.git").unwrap();

0 commit comments

Comments
 (0)