Compare commits

...

5 Commits

106
Cargo.lock generated

@ -1,15 +1,72 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "hermit-abi"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
dependencies = [
"libc",
]
[[package]]
name = "itoa"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]]
name = "libc"
version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
[[package]]
name = "mira"
version = "0.1.0"
dependencies = [
"clap",
"serde",
"serde_json",
]
@ -69,6 +126,12 @@ dependencies = [
"serde",
]
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "syn"
version = "1.0.53"
@ -80,8 +143,51 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

@ -5,5 +5,6 @@ authors = ["dece <shgck@pistache.land>"]
edition = "2018"
[dependencies]
clap = "2.33"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

@ -1,5 +1,79 @@
Mira
====
Small utility to help with Git repository mirroring, using a single JSON config
file. It is standalone and can be easily integrated in cron or systemd.
Small standalone utility to help with Git repository mirroring, using a single
JSON config file. If you read this on Github, I did not push it there myself,
Mira did!
About
-----
Mira automates mirroring a bunch of repository. Some people do not wish to use
Github, Gitlab Cloud or Bitbucket as their primary repository for various
reasons and run their own private Git servers, but making your code available
on these platforms still has advantages (visibility, issues, PR, etc).
Mira is a quick fix to that, hopefully just copy an example below, put it in a
Systemd service and be done with it.
Usage
-----
Example configuration for the mirroring of this repository from a Gitea
instance to its Github mirror:
```json
{
"workspace": "/tmp/mira",
"configurations": [
{
"name": "Gitea to Github",
"mirrors": [
{
"name": "Mira",
"src": "ssh://git@gitea.example.com:12345/Dece/Mira.git",
"dest": "git@github.com:Dece/Mira.git"
}
]
}
]
}
```
The different values are:
- `workspace`: a place where Mira can clone, fetch and push from.
- `configurations`: a set of configurations using similar auth mechanisms.
For now it is quite useless as no such mechanisms are supported.
- `configurations.N.name`: name of a configuration, a directory in the workspace.
- `configurations.N.mirrors`: list of mirrors.
- `mirrors.N.name`: name of a mirror, used to determine the directory where to
clone and work from.
- `mirrors.N.src`: clone and fetch URL for the mirror, passed to `git clone`.
Usually copying the URL provided by your server for cloning should suffice
(HTTP or SSH).
- `mirrors.N.dest`: push URL for the mirror, set as a remote named "mirror".
It may require some testing to determine what are the appropriate URLs for
clone and push. Gitea with SSH uses the full "ssh://" syntax, whereas Github
uses the simplified scp-like syntax. See the official clone help
[page][git-clone] for details.
[git-clone]: https://git-scm.com/docs/git-clone#_git_urls
When the configuration above is used, the output is the following:
```
$ ./mira -c config.json
Processing config Gitea to Github.
Mira mirrored successfully.
```
It does not support any authentication mechanisms beside what we'll be available
at the shell, which means you should run it in an environment where an SSH agent
will take care of authenticating yourself against the various servers you will
be mirroring from and to.

@ -1,12 +1,21 @@
use std::env;
//! Mira -- Git mirrors from a JSON config file.
use std::fs;
use std::io;
use std::io::prelude::*;
use std::path;
use std::process;
const MIRROR_REMOTE_NAME: &str = "mirror";
fn main() {
let config_text = match load_file(&path::Path::new("mira.json")) {
let matches = clap::App::new("Mira")
.setting(clap::AppSettings::ArgRequiredElseHelp)
.arg(clap::Arg::with_name("config")
.short("c").long("config").takes_value(true).required(true))
.get_matches();
let config_file = matches.value_of("config").unwrap();
let config_text = match load_file(&path::Path::new(config_file)) {
Ok(content) => content,
Err(e) => { eprintln!("{:?}", e); process::exit(1) }
};
@ -14,7 +23,7 @@ fn main() {
Ok(config) => config,
Err(e) => { eprintln!("{:?}", e); process::exit(1) }
};
process_root_config(&root_config);
process::exit(if process_root_config(&root_config) { 0 } else { 1 });
}
fn load_file(path: &path::Path) -> Result<String, io::Error> {
@ -24,88 +33,217 @@ fn load_file(path: &path::Path) -> Result<String, io::Error> {
Ok(contents)
}
/// Configuration file.
#[derive(Debug, serde::Deserialize)]
struct RootConfig {
workdir: Option<String>,
auth: serde_json::Value,
workspace: String,
configurations: Vec<Configuration>,
}
// #[derive(Debug, serde::Deserialize)]
// struct Authentication {
// key: Option<String>,
// }
fn process_root_config(root_config: &RootConfig) {
// Ensure working directory exists and move to it.
if let Some(workdir) = &root_config.workdir {
let workdir = path::Path::new(&workdir);
if !workdir.is_dir() {
if let Err(e) = fs::create_dir_all(&workdir) {
eprintln!("Failed to create working directory: {}.", e);
return
}
}
if let Err(e) = env::set_current_dir(&workdir) {
eprintln!("Failed to move to working directory: {}.", e);
return
}
}
// Process each configuration.
for config in &root_config.configurations {
process_config(config);
}
}
/// Server configuration.
#[derive(Debug, serde::Deserialize)]
struct Configuration {
name: String,
mirrors: Vec<Mirror>,
}
/// Mirror configuration.
#[derive(Debug, serde::Deserialize)]
struct Mirror {
name: String,
src: String,
dest: String,
}
fn process_config(config: &Configuration) {
/// Process the Mira configuration file, return true on complete success.
fn process_root_config(root_config: &RootConfig) -> bool {
// Ensure working directory exists and move to it.
let workspace = path::Path::new(&root_config.workspace);
if !workspace.is_dir() {
if let Err(e) = fs::create_dir_all(&workspace) {
eprintln!("Failed to create workspace directory: {}.", e);
return false
}
}
// Process each configuration, even if some of them fail.
let mut complete_success = true;
for config in &root_config.configurations {
if let Err(e) = process_config(config, workspace) {
eprintln!("An error occured with configuration {}: {}", config.name, e);
complete_success = false;
}
}
complete_success
}
/// Result of a mirror operation.
enum MirrorResult {
Success,
CloneFailed,
FetchFailed,
RemotesError,
PushFailed,
}
/// Process mirrors of this server configuration.
///
/// If an IO error is met when preparing for the mirroring, this function returns early with this
/// error. After that, all mirrors in `config` are processed, and the function returns true only if
/// every mirror completes succesfully.
fn process_config(config: &Configuration, workspace: &path::Path) -> Result<bool, io::Error> {
println!("Processing config {}.", config.name);
// Move into the configuration directory.
let mut config_path = match env::current_dir() {
Ok(pb) => pb,
Err(e) => { eprintln!("Current directory is not available: {}.", e); return }
};
let mut config_path = workspace.to_path_buf();
config_path.push(&config.name);
if !config_path.is_dir() {
if let Err(e) = fs::create_dir_all(&config_path) {
eprintln!("Failed to create working directory: {}.", e);
return
}
}
if let Err(e) = env::set_current_dir(&config_path) {
eprintln!("Failed to move to working directory: {}.", e);
return
fs::create_dir_all(&config_path)?;
}
// Mirror each repository in the configuration.
let mut complete_success = true;
for mirror in &config.mirrors {
mirror_repo(&mirror.src, &mirror.dest);
match mirror_repo(&mirror.name, &mirror.src, &mirror.dest, &config_path) {
Ok(MirrorResult::Success) => { println!("{} mirrored successfully.", mirror.name); },
Ok(MirrorResult::CloneFailed) => {
println!("Failed to clone {}.", mirror.name);
complete_success = false;
},
Ok(MirrorResult::FetchFailed) => {
println!("Failed to fetch changes for {}.", mirror.name);
complete_success = false;
},
Ok(MirrorResult::RemotesError) => {
println!("Failed to process remotes for {}.", mirror.name);
complete_success = false;
},
Ok(MirrorResult::PushFailed) => {
println!("Failed to push {}.", mirror.name);
complete_success = false;
},
Err(e) => {
eprintln!("An error occured during {} mirroring: {}", mirror.name, e);
complete_success = false;
}
}
}
Ok(complete_success)
}
/// Mirror a repository from `src_url` to `dest_url`.
///
/// This function assumes that the current work directory is the workspace,
/// so that a directory named `name` can be used to clone and/or push from.
fn mirror_repo(
name: &str,
src_url: &str,
dest_url: &str,
path: &path::Path
) -> Result<MirrorResult, io::Error> {
let mut repo_path = path.to_path_buf();
repo_path.push(name);
// Ensure the repository is cloned and up to date.
if !repo_path.exists() {
if let Some(e) = check_git_return(&clone(src_url, path, name), MirrorResult::CloneFailed) {
return Ok(e)
}
} else {
if let Some(e) = check_git_return(&fetch(&repo_path), MirrorResult::FetchFailed) {
return Ok(e)
}
}
// Ensure the mirror remote is available.
let remotes = match get_remotes(&repo_path) {
Some(remotes) => remotes,
None => return Ok(MirrorResult::RemotesError)
};
if !remotes.contains(&MIRROR_REMOTE_NAME.to_string()) {
if let Some(e) = check_git_return(
&add_mirror_remote(&repo_path, dest_url),
MirrorResult::RemotesError
) {
return Ok(e)
}
}
// Push to the mirror repo.
if let Some(e) = check_git_return(&push(&repo_path), MirrorResult::PushFailed) {
return Ok(e)
}
Ok(MirrorResult::Success)
}
fn mirror_repo(src_url: &str, dest_url: &str) -> bool {
clone(src_url)
/// Common type for wrappers around Git commands: success and optional stdout.
type GitCmdReturn = (bool, Option<String>);
/// Check a GitCmdReturn.
///
/// Print errors if the command failed and return `Some(on_error)`, or return None if the command
/// completed successfully.
fn check_git_return(cmd_return: &GitCmdReturn, on_error: MirrorResult) -> Option<MirrorResult> {
match cmd_return {
(false, output_opt) => {
if let Some(output) = output_opt {
eprintln!("Git output:\n{}", output);
}
Some(on_error)
}
_ => None
}
}
fn run_git_command(args: Vec<&str>) -> bool {
/// Run a git mirror clone command.
fn clone(url: &str, path: &path::Path, name: &str) -> GitCmdReturn {
let args = vec!("clone", "--mirror", url, name);
run_git_command_in(args, path)
}
/// Update a local repository.
fn fetch(path: &path::Path) -> GitCmdReturn {
run_git_command_in(vec!("fetch"), path)
}
/// Return a vector of remote names on success.
fn get_remotes(path: &path::Path) -> Option<Vec<String>> {
let (success, stdout) = run_git_command_in(vec!("remote"), path);
if !success {
return None
}
stdout.and_then(|s| Some(s.split_whitespace().map(|ss| ss.to_string()).collect()))
}
/// Set the mirror remote `url` in the repository at `path`.
fn add_mirror_remote(path: &path::Path, url: &str) -> GitCmdReturn {
let args = vec!("remote", "add", MIRROR_REMOTE_NAME, url);
run_git_command_in(args, path)
}
/// Run a git mirror push command.
fn push(path: &path::Path) -> GitCmdReturn {
let args = vec!("push", "--mirror", MIRROR_REMOTE_NAME);
run_git_command_in(args, path)
}
/// Run a git command with supplied arguments, return true on successful completion.
fn run_git_command(args: Vec<&str>) -> GitCmdReturn {
let mut command = process::Command::new("git");
command.args(&args);
match command.status() {
Ok(status) => status.success(),
Err(e) => { eprintln!("Failed to run Git: {}", e); false }
match command.output() {
Ok(output) => {
let success = output.status.success();
let text = String::from_utf8(
if success { output.stdout } else { output.stderr }
).ok();
(success, text)
}
Err(e) => { eprintln!("Failed to run Git: {}", e); (false, None) }
}
}
fn clone(url: &str) -> bool {
run_git_command(vec!("clone", "--mirror", url))
/// Call `run_git_command` but with a work directory specified.
fn run_git_command_in(args: Vec<&str>, path: &path::Path) -> GitCmdReturn {
let path = match path.to_str() {
Some(path) => path,
None => { eprintln!("Invalid path: {:?}", path); return (false, None) }
};
let mut full_args = vec!("-C", path);
full_args.extend(args.clone());
run_git_command(full_args)
}

Loading…
Cancel
Save