From b7f3404f9980606726c0e17c647b7e78622ce43f Mon Sep 17 00:00:00 2001 From: Azalea Date: Fri, 8 May 2026 06:29:13 +0000 Subject: [PATCH] [+] Docker --- .dockerignore | 4 ++++ Dockerfile | 26 ++++++++++++++++++++++ README.md | 22 +++++++++++++++++- compose.yaml | 14 ++++++++++++ src/interactive.rs | 27 ++++++++++++++++++++-- src/main.rs | 9 +++++++- tests/unit/interactive.rs | 37 +++++++++++++++++++++++++++++++ tests/unit/interactive_test_io.rs | 16 +++++++++++++ 8 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 compose.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9762919 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +/target +/.git +/.github +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..454d2c5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM rust:1-slim-bookworm AS build + +WORKDIR /app +COPY Cargo.toml Cargo.lock ./ +COPY src ./src +RUN cargo build --release --locked + +FROM debian:bookworm-slim AS runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates git nano openssh-client vim \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --uid 10001 --create-home --home-dir /home/refray --shell /usr/sbin/nologin refray \ + && mkdir -p /data \ + && chown -R refray:refray /data + +COPY --from=build /app/target/release/refray /usr/local/bin/refray + +USER refray +WORKDIR /data +ENV XDG_CONFIG_HOME=/data/config \ + XDG_CACHE_HOME=/data/cache + +EXPOSE 8787 +ENTRYPOINT ["refray"] +CMD ["serve", "--listen", "0.0.0.0:8787"] diff --git a/README.md b/README.md index fe00514..635b7fe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # refray -A tool to keep ALL of your repos in sync across ALL git platforms, while being able to work from any one of them. +A tool to keep your repos in sync across all git platforms, while being able to work from everywhere all at once. Created becasue github is so unusable and unreliable and I want to leave, but I don't want to leave the community behind. @@ -22,6 +22,26 @@ Supported platforms: GitHub, GitLab, Gitea, Forgejo Go to the [releases page](https://github.com/MaigoLabs/refray/releases), find the latest release, and download the appropriate binary for your platform. +### Option 3. Docker Compose + +Run config wizard: + +```sh +docker compose run --rm refray config +``` + +Start the webhook receiver as a service: + +```sh +docker compose up -d --build +``` + +To edit config manually: + +```sh +docker compose run --rm --entrypoint nano refray /data/config/refray/config.toml +``` + ## Configure Run the interactive configuration wizard: diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..763e874 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,14 @@ +services: + refray: + build: . + image: refray:local + command: ["serve", "--listen", "0.0.0.0:8787"] + ports: + - "8787:8787" + volumes: + - refray-data:/data + restart: unless-stopped + +volumes: + refray-data: + name: refray-data diff --git a/src/interactive.rs b/src/interactive.rs index 8545f75..183b8b5 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -37,7 +37,7 @@ struct ParsedProfileUrl { namespace: String, } -pub fn run_config_wizard(path: &Path) -> Result<()> { +pub fn run_config_wizard(path: &Path) -> Result { let existing_config = path.exists(); let mut config = Config::load_or_default(path)?; let theme = ColorfulTheme::default(); @@ -85,7 +85,17 @@ pub fn run_config_wizard(path: &Path) -> Result<()> { style("saved").green().bold(), style(path.display()).cyan() ); - Ok(()) + let run_full_sync_now = prompt_run_full_sync_now_styled(&config, &theme)?; + Ok(ConfigWizardOutcome { + config, + run_full_sync_now, + }) +} + +#[derive(Clone, Debug)] +pub struct ConfigWizardOutcome { + pub config: Config, + pub run_full_sync_now: bool, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -389,6 +399,19 @@ fn prompt_wizard_action_styled(theme: &ColorfulTheme) -> Result { }) } +fn prompt_run_full_sync_now_styled(config: &Config, theme: &ColorfulTheme) -> Result { + if config.mirrors.is_empty() { + return Ok(false); + } + + println!(); + Confirm::with_theme(theme) + .with_prompt("Run full sync now?") + .default(false) + .interact() + .map_err(Into::into) +} + fn edit_sync_group_styled(config: &mut Config, theme: &ColorfulTheme) -> Result { if config.mirrors.is_empty() { println!("{}", style("No sync groups to edit.").yellow()); diff --git a/src/main.rs b/src/main.rs index c034f0a..90f219f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -146,7 +146,14 @@ fn main() -> Result<()> { let config_path = cli.config.unwrap_or_else(default_config_path); match cli.command { - Command::Config => interactive::run_config_wizard(&config_path), + Command::Config => { + let outcome = interactive::run_config_wizard(&config_path)?; + if outcome.run_full_sync_now { + sync_all(&outcome.config, SyncOptions::default()) + } else { + Ok(()) + } + } Command::Sync(command) => { let config = load_config(&config_path)?; sync_all( diff --git a/tests/unit/interactive.rs b/tests/unit/interactive.rs index a49eb5d..a8eeb84 100644 --- a/tests/unit/interactive.rs +++ b/tests/unit/interactive.rs @@ -219,6 +219,43 @@ fn wizard_starts_existing_config_at_sync_group_menu() { assert!(!output.contains("Profile/org URL:")); } +#[test] +fn wizard_can_ask_to_run_full_sync_after_config() { + let config = Config { + sites: Vec::new(), + mirrors: vec![MirrorConfig { + name: "sync-1".to_string(), + endpoints: Vec::new(), + create_missing: true, + visibility: Visibility::Private, + allow_force: false, + conflict_resolution: ConflictResolutionStrategy::Fail, + }], + webhook: None, + }; + let mut reader = Cursor::new(b"y\n".as_slice()); + let mut output = Vec::new(); + + let run_full_sync = + prompt_run_full_sync_now_with_io(&config, &mut reader, &mut output).unwrap(); + + assert!(run_full_sync); + let output = String::from_utf8(output).unwrap(); + assert!(output.contains("Run full sync now? [y/N]:")); +} + +#[test] +fn wizard_skips_full_sync_prompt_without_sync_groups() { + let mut reader = Cursor::new(b"".as_slice()); + let mut output = Vec::new(); + + let run_full_sync = + prompt_run_full_sync_now_with_io(&Config::default(), &mut reader, &mut output).unwrap(); + + assert!(!run_full_sync); + assert!(output.is_empty()); +} + #[test] fn wizard_edits_existing_sync_group_from_menu() { let config = Config { diff --git a/tests/unit/interactive_test_io.rs b/tests/unit/interactive_test_io.rs index 04f8d79..bbf564b 100644 --- a/tests/unit/interactive_test_io.rs +++ b/tests/unit/interactive_test_io.rs @@ -41,6 +41,22 @@ where Ok(config) } +pub fn prompt_run_full_sync_now_with_io( + config: &Config, + reader: &mut R, + writer: &mut W, +) -> Result +where + R: BufRead, + W: Write, +{ + if config.mirrors.is_empty() { + return Ok(false); + } + + prompt_bool(reader, writer, "Run full sync now?", false) +} + fn add_sync_group(reader: &mut R, writer: &mut W, config: &mut Config) -> Result<()> where R: BufRead,