Skip to content

Commit

Permalink
Merge pull request #283 from siketyan/feat/dump-restore
Browse files Browse the repository at this point in the history
feat: Dump and Restore repositories through a TOML file
  • Loading branch information
siketyan authored Dec 31, 2023
2 parents a7486dc + 6f6d9db commit 47fcbae
Show file tree
Hide file tree
Showing 13 changed files with 445 additions and 18 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Commands:
path Prints the path to root, owner, or a repository
profile Manages profiles to use in repositories
shell Writes a shell script to extend ghr features
sync Sync repositories between your devices
version Prints the version of this application
help Print this message or the help of the given subcommand(s)
Expand Down Expand Up @@ -163,7 +164,7 @@ cmd = "code"
args = ["%p"]
```

> **Note**
> [!NOTE]
> `%p` will be replaced by the repository path.
### Finding path of the repository
Expand All @@ -177,6 +178,18 @@ ghr path --host=github.com # Host root
ghr path --host=github.com --owner=<owner> # Owner root of the specified host
```

### Syncing repositories and their state

> [!WARNING]
> This feature is experimental.
ghr supports dumping and restoring the current branch and remotes of managed repositories.

```shell
ghr sync dump > repositories.toml
ghr sync restore < repositories.toml
```

## 🛠 Customising
You can change the root of repositories managed by ghr by setting environment variable `GHR_ROOT` in your shell profile.

Expand Down
20 changes: 10 additions & 10 deletions src/cmd/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,42 @@ use crate::url::Url;
const CLONE_RETRY_COUNT: u32 = 3;
const CLONE_RETRY_DURATION: Duration = Duration::from_secs(2);

#[derive(Debug, Parser)]
#[derive(Debug, Default, Parser)]
pub struct Cmd {
/// URL or pattern of the repository to clone.
repo: Vec<String>,
pub(crate) repo: Vec<String>,

/// Forks the repository in the specified owner (organisation) and clones the forked repo.
#[clap(long)]
fork: Option<Option<String>>,
pub(crate) fork: Option<Option<String>>,

/// Clones multiple repositories in parallel.
#[clap(short, long)]
parallel: bool,
pub(crate) parallel: bool,

/// Clones their submodules recursively.
#[clap(short, long, alias = "recurse-submodules")]
recursive: Option<Option<String>>,
pub(crate) recursive: Option<Option<String>>,

/// Clones only the default branch.
#[clap(long)]
single_branch: bool,
pub(crate) single_branch: bool,

/// Uses this name instead of `origin` for the upstream remote.
#[clap(short, long)]
origin: Option<String>,
pub(crate) origin: Option<String>,

/// Points the specified branch instead of the default branch after cloned the repository.
#[clap(short, long)]
branch: Option<String>,
pub(crate) branch: Option<String>,

/// Change directory after cloning the repository (Shell extension required).
#[clap(long)]
cd: bool,
pub(crate) cd: bool,

/// Opens the directory after cloning the repository.
#[clap(long)]
open: Option<Option<String>>,
pub(crate) open: Option<Option<String>>,
}

impl Cmd {
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod open;
mod path;
mod profile;
mod shell;
mod sync;
mod version;

use std::io::stderr;
Expand Down Expand Up @@ -42,6 +43,8 @@ pub enum Action {
Profile(profile::Cmd),
/// Writes a shell script to extend ghr features.
Shell(shell::Cmd),
/// Sync repositories between your devices.
Sync(sync::Cmd),
/// Prints the version of this application.
Version(version::Cmd),
}
Expand Down Expand Up @@ -91,6 +94,7 @@ impl Cli {
Path(cmd) => cmd.run(),
Profile(cmd) => cmd.run(),
Shell(cmd) => cmd.run(),
Sync(cmd) => cmd.run().await,
Version(cmd) => cmd.run(),
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/cmd/sync/dump.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use anyhow::Result;
use clap::Parser;
use itertools::Itertools;

use crate::repository::Repositories;
use crate::root::Root;
use crate::sync::File;

#[derive(Debug, Parser)]
pub struct Cmd {}

impl Cmd {
pub fn run(self) -> Result<()> {
let root = Root::find()?;
let file = Repositories::try_collect(&root)?
.into_iter()
.map(|(p, _)| p)
.sorted_by_key(|p| p.to_string())
.collect::<File>();

println!("{}", toml::to_string(&file)?);

Ok(())
}
}
29 changes: 29 additions & 0 deletions src/cmd/sync/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
mod dump;
mod restore;

use anyhow::Result;
use clap::{Parser, Subcommand};

#[derive(Debug, Subcommand)]
pub enum Action {
/// Dump remotes and the current ref of all repositories.
Dump(dump::Cmd),
/// Restore repositories from the dumped file.
Restore(restore::Cmd),
}

#[derive(Debug, Parser)]
pub struct Cmd {
#[clap(subcommand)]
action: Action,
}

impl Cmd {
pub async fn run(self) -> Result<()> {
use Action::*;
match self.action {
Dump(cmd) => cmd.run(),
Restore(cmd) => cmd.run().await,
}
}
}
97 changes: 97 additions & 0 deletions src/cmd/sync/restore.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use std::io::{read_to_string, stdin};
use std::path::PathBuf;

use anyhow::Result;
use clap::Parser;
use git2::{ErrorCode, Repository as GitRepository};
use tracing::info;

use crate::cmd::clone;
use crate::config::Config;
use crate::console::Spinner;
use crate::git::{CheckoutBranch, Fetch};
use crate::path::Path;
use crate::root::Root;
use crate::sync::{File, Ref, Repository};
use crate::url::Url;

#[derive(Debug, Parser)]
pub struct Cmd {}

impl Cmd {
pub async fn run(self) -> Result<()> {
let root = Root::find()?;
let config = Config::load_from(&root)?;
let file = toml::from_str::<File>(read_to_string(stdin())?.as_str())?;

for Repository {
host,
owner,
repo,
r#ref,
remotes,
} in file.repositories
{
let origin = remotes.iter().find(|r| {
Url::from_str(&r.url, &config.patterns, config.defaults.owner.as_deref())
.ok()
.map(|u| u.host.to_string() == host)
.unwrap_or_default()
});

clone::Cmd {
repo: vec![origin
.map(|r| r.url.to_string())
.unwrap_or_else(|| format!("{}:{}/{}", host, owner, repo))],
origin: origin.map(|r| r.name.to_string()),
..Default::default()
}
.run()
.await?;

let path = PathBuf::from(Path::new(&root, host, owner, repo));
let repo = GitRepository::open(&path)?;

for remote in remotes {
if let Err(e) = repo
.remote(&remote.name, &remote.url)
.and_then(|_| repo.remote_set_pushurl(&remote.name, remote.push_url.as_deref()))
{
match e.code() {
ErrorCode::Exists => (),
_ => return Err(e.into()),
}
}

Spinner::new("Fetching objects from remotes...")
.spin_while(|| async {
config.git.strategy.clone.fetch(&path, &remote.name)?;
Ok::<(), anyhow::Error>(())
})
.await?;

info!("Fetched from remote: {}", &remote.name);
}

match r#ref {
Some(Ref::Remote(r)) => {
repo.checkout_tree(&repo.revparse_single(&r)?, None)?;

info!("Successfully checked out a remote ref: {}", &r);
}
Some(Ref::Branch(b)) => {
config.git.strategy.checkout.checkout_branch(
&path,
&b.name,
Some(b.upstream.to_string()),
)?;

info!("Successfully checked out a branch: {}", &b.name);
}
_ => (),
}
}

Ok(())
}
}
4 changes: 4 additions & 0 deletions src/git/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use crate::git::strategy::Strategy;
pub struct StrategyConfig {
#[serde(default)]
pub clone: Strategy,
#[serde(default)]
pub fetch: Strategy,
#[serde(default)]
pub checkout: Strategy,
}

#[derive(Debug, Default, Deserialize)]
Expand Down
17 changes: 17 additions & 0 deletions src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,20 @@ pub trait CloneRepository {
U: ToString,
P: AsRef<Path>;
}

pub trait Fetch {
fn fetch<P>(&self, path: P, remote: impl Into<String>) -> Result<()>
where
P: AsRef<Path>;
}

pub trait CheckoutBranch {
fn checkout_branch<P>(
&self,
path: P,
branch: impl Into<String>,
track: impl Into<Option<String>>,
) -> Result<()>
where
P: AsRef<Path>;
}
49 changes: 48 additions & 1 deletion src/git/strategy/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::process::Command;
use anyhow::anyhow;
use tracing::debug;

use crate::git::{CloneOptions, CloneRepository};
use crate::git::{CheckoutBranch, CloneOptions, CloneRepository, Fetch};

pub struct Cli;

Expand Down Expand Up @@ -48,3 +48,50 @@ impl CloneRepository for Cli {
}
}
}

impl Fetch for Cli {
fn fetch<P>(&self, path: P, remote: impl Into<String>) -> anyhow::Result<()>
where
P: AsRef<Path>,
{
let output = Command::new("git")
.current_dir(path)
.args(["fetch".to_string(), remote.into()])
.output()?;

match output.status.success() {
true => Ok(()),
_ => Err(anyhow!(
"Error occurred while fetching the remote: {}",
String::from_utf8_lossy(output.stderr.as_slice()).trim(),
)),
}
}
}

impl CheckoutBranch for Cli {
fn checkout_branch<P>(
&self,
path: P,
branch: impl Into<String>,
track: impl Into<Option<String>>,
) -> anyhow::Result<()>
where
P: AsRef<Path>,
{
let mut args = Vec::from(["checkout".to_string(), "-b".to_string(), branch.into()]);
if let Some(t) = track.into() {
args.push("--track".to_string());
args.push(t);
}

let output = Command::new("git").current_dir(path).args(args).output()?;
match output.status.success() {
true => Ok(()),
_ => Err(anyhow!(
"Error occurred while fetching the remote: {}",
String::from_utf8_lossy(output.stderr.as_slice()).trim(),
)),
}
}
}
29 changes: 28 additions & 1 deletion src/git/strategy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::path::Path;

use serde::Deserialize;

use crate::git::{CloneOptions, CloneRepository};
use crate::git::{CheckoutBranch, CloneOptions, CloneRepository, Fetch};

#[derive(Debug, Default, Deserialize)]
pub enum Strategy {
Expand All @@ -25,3 +25,30 @@ impl CloneRepository for Strategy {
}
}
}

impl Fetch for Strategy {
fn fetch<P>(&self, path: P, remote: impl Into<String>) -> anyhow::Result<()>
where
P: AsRef<Path>,
{
match self {
Self::Cli => Cli.fetch(path, remote),
}
}
}

impl CheckoutBranch for Strategy {
fn checkout_branch<P>(
&self,
path: P,
branch: impl Into<String>,
track: impl Into<Option<String>>,
) -> anyhow::Result<()>
where
P: AsRef<Path>,
{
match self {
Self::Cli => Cli.checkout_branch(path, branch, track),
}
}
}
Loading

0 comments on commit 47fcbae

Please sign in to comment.