Skip to content

Commit 8bc3929

Browse files
committed
Implement pull
1 parent 9c16584 commit 8bc3929

File tree

8 files changed

+468
-35
lines changed

8 files changed

+468
-35
lines changed

Cargo.lock

Lines changed: 92 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ name = "josh-sync"
33
version = "0.1.0"
44
edition = "2024"
55

6+
[[bin]]
7+
path = "src/bin/josh_sync.rs"
8+
name = "josh-sync"
9+
610
[dependencies]
711
anyhow = "1"
812
clap = { version = "4", features = ["derive"] }
13+
directories = "6"
914
toml = "0.8"
1015
serde = { version = "1", features = ["derive"] }
1116
which = "8"

src/bin/josh_sync.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use anyhow::Context;
22
use clap::Parser;
3-
use josh_sync::JoshConfig;
3+
use josh_sync::config::{JoshConfig, load_config};
44
use josh_sync::josh::{JoshProxy, try_install_josh};
5+
use josh_sync::sync::{GitSync, RustcPullError};
56
use std::path::{Path, PathBuf};
67

78
const DEFAULT_CONFIG_PATH: &str = "josh-sync.toml";
@@ -31,16 +32,31 @@ fn main() -> anyhow::Result<()> {
3132
let config = JoshConfig {
3233
org: "rust-lang".to_string(),
3334
repo: "<repository-name>".to_string(),
34-
upstream_sha: None,
35+
path: "<relative-subtree-path>".to_string(),
36+
last_upstream_sha: None,
3537
};
36-
let config = toml::to_string_pretty(&config).context("cannot serialize config")?;
37-
std::fs::write(DEFAULT_CONFIG_PATH, config).context("cannot write config")?;
38+
config
39+
.write(Path::new(DEFAULT_CONFIG_PATH))
40+
.context("cannot write config")?;
3841
println!("Created config file at {DEFAULT_CONFIG_PATH}");
3942
}
4043
Command::Pull { config } => {
4144
let config = load_config(&config)
4245
.context("cannot load config. Run the `init` command to initialize it.")?;
4346
let josh = get_josh_proxy()?;
47+
let sync = GitSync::new(config, josh);
48+
if let Err(error) = sync.rustc_pull() {
49+
match error {
50+
RustcPullError::NothingToPull => {
51+
eprintln!("Nothing to pull");
52+
std::process::exit(2);
53+
}
54+
RustcPullError::PullFailed(error) => {
55+
eprintln!("Pull failure: {error:?}");
56+
std::process::exit(1);
57+
}
58+
}
59+
}
4460
}
4561
}
4662

@@ -65,10 +81,3 @@ fn get_josh_proxy() -> anyhow::Result<JoshProxy> {
6581
}
6682
}
6783
}
68-
69-
fn load_config(path: &Path) -> anyhow::Result<JoshConfig> {
70-
let data = std::fs::read_to_string(path)
71-
.with_context(|| format!("cannot load config file from {}", path.display()))?;
72-
let config: JoshConfig = toml::from_str(&data).context("cannot load config as TOML")?;
73-
Ok(config)
74-
}

src/config.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use anyhow::Context;
2+
use std::path::{Path, PathBuf};
3+
4+
#[derive(serde::Serialize, serde::Deserialize, Clone)]
5+
pub struct JoshConfig {
6+
#[serde(default = "default_org")]
7+
pub org: String,
8+
pub repo: String,
9+
/// Relative path where the subtree is located in rust-lang/rust.
10+
/// For example `src/doc/rustc-dev-guide`.
11+
pub path: String,
12+
/// Last SHA of rust-lang/rust that was pulled into this subtree.
13+
#[serde(default)]
14+
pub last_upstream_sha: Option<String>,
15+
}
16+
17+
impl JoshConfig {
18+
pub fn full_repo_name(&self) -> String {
19+
format!("{}/{}", self.org, self.repo)
20+
}
21+
22+
pub fn write(&self, path: &Path) -> anyhow::Result<()> {
23+
let config = toml::to_string_pretty(self).context("cannot serialize config")?;
24+
std::fs::write(path, config).context("cannot write config")?;
25+
Ok(())
26+
}
27+
}
28+
29+
pub struct JoshConfigWithPath {
30+
pub config: JoshConfig,
31+
pub path: PathBuf,
32+
}
33+
34+
fn default_org() -> String {
35+
String::from("rust-lang")
36+
}
37+
38+
pub fn load_config(path: &Path) -> anyhow::Result<JoshConfigWithPath> {
39+
let data = std::fs::read_to_string(path)
40+
.with_context(|| format!("cannot load config file from {}", path.display()))?;
41+
let config: JoshConfig = toml::from_str(&data).context("cannot load config as TOML")?;
42+
Ok(JoshConfigWithPath {
43+
config,
44+
path: path.to_path_buf(),
45+
})
46+
}

src/josh.rs

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
use crate::config::JoshConfig;
12
use crate::utils::check_output;
3+
use anyhow::Context;
4+
use std::net::{SocketAddr, TcpStream};
25
use std::path::PathBuf;
6+
use std::process::{Command, Stdio};
7+
use std::time::Duration;
8+
9+
const JOSH_PORT: u16 = 42042;
310

411
pub struct JoshProxy {
512
path: PathBuf,
@@ -10,6 +17,47 @@ impl JoshProxy {
1017
pub fn lookup() -> Option<Self> {
1118
which::which("josh-proxy").ok().map(|path| Self { path })
1219
}
20+
21+
pub fn start(&self, config: &JoshConfig) -> anyhow::Result<RunningJoshProxy> {
22+
// Determine cache directory.
23+
let user_dirs =
24+
directories::ProjectDirs::from("org", &config.full_repo_name(), "rustc-josh")
25+
.context("cannot determine cache directory for Josh")?;
26+
let local_dir = user_dirs.cache_dir().to_owned();
27+
28+
// Start josh, silencing its output.
29+
let josh = std::process::Command::new(&self.path)
30+
.arg("--local")
31+
.arg(local_dir)
32+
.args([
33+
"--remote=https://github.com",
34+
&format!("--port={JOSH_PORT}"),
35+
"--no-background",
36+
])
37+
.stdout(Stdio::null())
38+
.stderr(Stdio::null())
39+
.spawn()
40+
.context("failed to start josh-proxy, make sure it is installed")?;
41+
42+
// Wait until the port is open. We try every 10ms until 1s passed.
43+
for _ in 0..100 {
44+
// This will generally fail immediately when the port is still closed.
45+
let addr = SocketAddr::from(([127, 0, 0, 1], JOSH_PORT));
46+
let josh_ready = TcpStream::connect_timeout(&addr, Duration::from_millis(1));
47+
48+
if josh_ready.is_ok() {
49+
println!("josh up and running");
50+
return Ok(RunningJoshProxy {
51+
process: josh,
52+
port: JOSH_PORT,
53+
});
54+
}
55+
56+
// Not ready yet.
57+
std::thread::sleep(Duration::from_millis(10));
58+
}
59+
panic!("Even after waiting for 1s, josh-proxy is still not available.");
60+
}
1361
}
1462

1563
pub fn try_install_josh() -> Option<JoshProxy> {
@@ -22,6 +70,51 @@ pub fn try_install_josh() -> Option<JoshProxy> {
2270
"--tag",
2371
"r24.10.04",
2472
"josh-proxy",
25-
]);
73+
])
74+
.expect("cannot install josh-proxy");
2675
JoshProxy::lookup()
2776
}
77+
78+
/// Create a wrapper that represents a running instance of `josh-proxy` and stops it on drop.
79+
pub struct RunningJoshProxy {
80+
process: std::process::Child,
81+
port: u16,
82+
}
83+
84+
impl RunningJoshProxy {
85+
pub fn git_url(&self, upstream_repo: &str, commit: &str, filter: &str) -> String {
86+
format!(
87+
"http://localhost:{}/{upstream_repo}.git@{commit}{filter}.git",
88+
self.port
89+
)
90+
}
91+
}
92+
93+
impl Drop for RunningJoshProxy {
94+
fn drop(&mut self) {
95+
if cfg!(unix) {
96+
// Try to gracefully shut it down.
97+
Command::new("kill")
98+
.args(["-s", "INT", &self.process.id().to_string()])
99+
.output()
100+
.expect("failed to SIGINT josh-proxy");
101+
// Sadly there is no "wait with timeout"... so we just give it some time to finish.
102+
std::thread::sleep(Duration::from_millis(100));
103+
// Now hopefully it is gone.
104+
if self
105+
.process
106+
.try_wait()
107+
.expect("failed to wait for josh-proxy")
108+
.is_some()
109+
{
110+
return;
111+
}
112+
}
113+
// If that didn't work (or we're not on Unix), kill it hard.
114+
eprintln!(
115+
"I have to kill josh-proxy the hard way, let's hope this does not \
116+
break anything."
117+
);
118+
self.process.kill().expect("failed to SIGKILL josh-proxy");
119+
}
120+
}

0 commit comments

Comments
 (0)