From 516c45c8312598f80840c75807635e3b16c5af36 Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Sun, 31 Dec 2023 22:05:36 +0900 Subject: [PATCH 1/8] feat: Dump repositories into a TOML file --- README.md | 1 + src/cmd/mod.rs | 4 ++ src/cmd/sync/dump.rs | 23 ++++++++ src/cmd/sync/mod.rs | 25 +++++++++ src/main.rs | 1 + src/path.rs | 11 ++-- src/sync.rs | 131 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 src/cmd/sync/dump.rs create mode 100644 src/cmd/sync/mod.rs create mode 100644 src/sync.rs diff --git a/README.md b/README.md index 6514414..7ab21bf 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 241b128..0fe3f26 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -9,6 +9,7 @@ mod open; mod path; mod profile; mod shell; +mod sync; mod version; use std::io::stderr; @@ -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), } @@ -91,6 +94,7 @@ impl Cli { Path(cmd) => cmd.run(), Profile(cmd) => cmd.run(), Shell(cmd) => cmd.run(), + Sync(cmd) => cmd.run(), Version(cmd) => cmd.run(), } } diff --git a/src/cmd/sync/dump.rs b/src/cmd/sync/dump.rs new file mode 100644 index 0000000..4b260bf --- /dev/null +++ b/src/cmd/sync/dump.rs @@ -0,0 +1,23 @@ +use anyhow::Result; +use clap::Parser; + +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) + .collect::(); + + println!("{}", toml::to_string(&file)?); + + Ok(()) + } +} diff --git a/src/cmd/sync/mod.rs b/src/cmd/sync/mod.rs new file mode 100644 index 0000000..743eb3c --- /dev/null +++ b/src/cmd/sync/mod.rs @@ -0,0 +1,25 @@ +mod dump; + +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), +} + +#[derive(Debug, Parser)] +pub struct Cmd { + #[clap(subcommand)] + action: Action, +} + +impl Cmd { + pub fn run(self) -> Result<()> { + use Action::*; + match self.action { + Dump(cmd) => cmd.run(), + } + } +} diff --git a/src/main.rs b/src/main.rs index 5d7e264..21a714b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod profile; mod repository; mod root; mod rule; +mod sync; mod url; use std::process::exit; diff --git a/src/path.rs b/src/path.rs index 1f64529..2efe312 100644 --- a/src/path.rs +++ b/src/path.rs @@ -1,14 +1,15 @@ -use crate::root::Root; -use crate::url::Url; use std::fmt::{Display, Formatter}; use std::path::PathBuf; +use crate::root::Root; +use crate::url::Url; + #[derive(Debug, Eq, PartialEq, Hash)] pub struct Path<'a> { root: &'a Root, - host: String, - owner: String, - repo: String, + pub host: String, + pub owner: String, + pub repo: String, } impl<'a> Path<'a> { diff --git a/src/sync.rs b/src/sync.rs new file mode 100644 index 0000000..439bee4 --- /dev/null +++ b/src/sync.rs @@ -0,0 +1,131 @@ +use std::path::PathBuf; + +use anyhow::{bail, Result}; +use git2::{BranchType, ErrorCode, Reference, Repository as GitRepository}; +use serde::{Deserialize, Serialize}; +use tracing::warn; + +use crate::path::Path; + +#[derive(Deserialize, Serialize)] +pub enum Ref {} + +#[derive(Deserialize, Serialize)] +pub struct Remote { + pub name: String, + pub url: String, + pub push_url: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct Repository { + pub host: String, + pub owner: String, + pub repo: String, + pub r#ref: String, + #[serde(default)] + pub remotes: Vec, +} + +impl Repository { + pub fn save(path: &Path) -> Result { + let repo = match GitRepository::open(PathBuf::from(path)) { + Ok(r) => r, + Err(e) => match e.code() { + ErrorCode::NotFound => bail!("Not a Git repository"), + _ => return Err(e.into()), + }, + }; + + let head = match repo.head() { + Ok(r) => r, + Err(e) => match e.code() { + ErrorCode::UnbornBranch => bail!("HEAD is an unborn branch"), + ErrorCode::NotFound => bail!("Cannot find the HEAD"), + _ => return Err(e.into()), + }, + }; + + if let Err(e) = Self::ensure_synced(&repo, &head) { + warn!("Repository {} is not synced to remote: {}", path, e); + } + + let r#ref = head.name().unwrap_or_default().to_string(); + + Ok(Self { + host: path.host.to_string(), + owner: path.owner.to_string(), + repo: path.repo.to_string(), + r#ref, + remotes: repo + .remotes()? + .iter() + .flatten() + .map(|name| { + let remote = repo.find_remote(name)?; + + Ok(Remote { + name: name.to_string(), + url: remote.url().unwrap_or_default().to_string(), + push_url: remote.pushurl().map(|u| u.to_string()), + }) + }) + .collect::>>()?, + }) + } + + fn ensure_synced(repo: &GitRepository, head: &Reference) -> Result<()> { + if head.is_remote() { + return Ok(()); + } + + if head.is_branch() { + let upstream = match repo + .find_branch(head.shorthand().unwrap(), BranchType::Local)? + .upstream() + { + Ok(b) => b, + Err(e) => match e.code() { + ErrorCode::NotFound => bail!("Branch has never pushed to remote"), + _ => return Err(e.into()), + }, + }; + + if head != &upstream.into_reference() { + bail!("Branch is not synced"); + } + } else if head.is_tag() { + bail!("HEAD is a tag"); + } else { + bail!("Detached HEAD"); + } + + return Ok(()); + } +} + +#[derive(Deserialize, Serialize)] +pub struct File { + #[serde(default)] + pub repositories: Vec, +} + +impl<'a> FromIterator> for File { + fn from_iter(iter: T) -> Self + where + T: IntoIterator>, + { + Self { + repositories: iter + .into_iter() + .flat_map(|path| match Repository::save(&path) { + Ok(r) => Some(r), + Err(e) => { + warn!("Skipped repository {}: {}", &path, e); + None + } + }) + .collect::>(), + } + } +} From 4db6936c1b871e8dddbdc2ac5aaee3a56eedbd4a Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Sun, 31 Dec 2023 23:04:48 +0900 Subject: [PATCH 2/8] feat: Restore repos from the dumped file --- src/cmd/clone.rs | 20 +++++----- src/cmd/mod.rs | 2 +- src/cmd/sync/mod.rs | 6 ++- src/cmd/sync/restore.rs | 86 +++++++++++++++++++++++++++++++++++++++++ src/sync.rs | 65 +++++++++++++++++++++++-------- 5 files changed, 150 insertions(+), 29 deletions(-) create mode 100644 src/cmd/sync/restore.rs diff --git a/src/cmd/clone.rs b/src/cmd/clone.rs index f56a83e..5aa0a91 100644 --- a/src/cmd/clone.rs +++ b/src/cmd/clone.rs @@ -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, + pub(crate) repo: Vec, /// Forks the repository in the specified owner (organisation) and clones the forked repo. #[clap(long)] - fork: Option>, + pub(crate) fork: Option>, /// 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>, + pub(crate) recursive: Option>, /// 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, + pub(crate) origin: Option, /// Points the specified branch instead of the default branch after cloned the repository. #[clap(short, long)] - branch: Option, + pub(crate) branch: Option, /// 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>, + pub(crate) open: Option>, } impl Cmd { diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 0fe3f26..febb66d 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -94,7 +94,7 @@ impl Cli { Path(cmd) => cmd.run(), Profile(cmd) => cmd.run(), Shell(cmd) => cmd.run(), - Sync(cmd) => cmd.run(), + Sync(cmd) => cmd.run().await, Version(cmd) => cmd.run(), } } diff --git a/src/cmd/sync/mod.rs b/src/cmd/sync/mod.rs index 743eb3c..902f149 100644 --- a/src/cmd/sync/mod.rs +++ b/src/cmd/sync/mod.rs @@ -1,4 +1,5 @@ mod dump; +mod restore; use anyhow::Result; use clap::{Parser, Subcommand}; @@ -7,6 +8,8 @@ use clap::{Parser, 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)] @@ -16,10 +19,11 @@ pub struct Cmd { } impl Cmd { - pub fn run(self) -> Result<()> { + pub async fn run(self) -> Result<()> { use Action::*; match self.action { Dump(cmd) => cmd.run(), + Restore(cmd) => cmd.run().await, } } } diff --git a/src/cmd/sync/restore.rs b/src/cmd/sync/restore.rs new file mode 100644 index 0000000..29aae33 --- /dev/null +++ b/src/cmd/sync/restore.rs @@ -0,0 +1,86 @@ +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::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::(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 = Path::new(&root, host, owner, repo); + let repo = GitRepository::open(PathBuf::from(&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()), + } + } + } + + 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)) => { + let (object, reference) = repo.revparse_ext(&b.name)?; + let ref_name = reference.unwrap().name().unwrap().to_string(); + + repo.checkout_tree(&object, None)?; + repo.set_head(&ref_name)?; + + info!("Successfully checked out a branch: {}", &ref_name); + } + _ => (), + } + } + + Ok(()) + } +} diff --git a/src/sync.rs b/src/sync.rs index 439bee4..56892e6 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -8,7 +8,18 @@ use tracing::warn; use crate::path::Path; #[derive(Deserialize, Serialize)] -pub enum Ref {} +pub struct BranchRef { + pub name: String, + pub remote: String, +} + +#[derive(Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum Ref { + Remote(String), + Branch(BranchRef), + // tag is not supported ... yet. +} #[derive(Deserialize, Serialize)] pub struct Remote { @@ -22,7 +33,7 @@ pub struct Repository { pub host: String, pub owner: String, pub repo: String, - pub r#ref: String, + pub r#ref: Option, #[serde(default)] pub remotes: Vec, } @@ -46,19 +57,25 @@ impl Repository { }, }; - if let Err(e) = Self::ensure_synced(&repo, &head) { - warn!("Repository {} is not synced to remote: {}", path, e); - } + let r#ref = match Self::synced_ref(&repo, &head) { + Ok(r) => Some(r), + Err(e) => { + warn!("Repository {} is not synced to remote: {}", path, e); + None + } + }; - let r#ref = head.name().unwrap_or_default().to_string(); + let remotes = repo.remotes()?; + if remotes.is_empty() { + bail!("No remotes defined"); + } Ok(Self { host: path.host.to_string(), owner: path.owner.to_string(), repo: path.repo.to_string(), r#ref, - remotes: repo - .remotes()? + remotes: remotes .iter() .flatten() .map(|name| { @@ -74,16 +91,14 @@ impl Repository { }) } - fn ensure_synced(repo: &GitRepository, head: &Reference) -> Result<()> { + fn synced_ref(repo: &GitRepository, head: &Reference) -> Result { if head.is_remote() { - return Ok(()); + return Ok(Ref::Remote(head.name().unwrap().to_string())); } if head.is_branch() { - let upstream = match repo - .find_branch(head.shorthand().unwrap(), BranchType::Local)? - .upstream() - { + let name = head.shorthand().unwrap(); + let upstream = match repo.find_branch(name, BranchType::Local)?.upstream() { Ok(b) => b, Err(e) => match e.code() { ErrorCode::NotFound => bail!("Branch has never pushed to remote"), @@ -91,21 +106,36 @@ impl Repository { }, }; - if head != &upstream.into_reference() { + let reference = upstream.into_reference(); + if head != &reference { bail!("Branch is not synced"); } + + Ok(Ref::Branch(BranchRef { + name: name.to_string(), + remote: repo + .branch_remote_name(reference.name().unwrap())? + .as_str() + .unwrap() + .to_string(), + })) } else if head.is_tag() { bail!("HEAD is a tag"); } else { bail!("Detached HEAD"); } - - return Ok(()); } } +#[derive(Deserialize, Serialize)] +pub enum Version { + V1, +} + #[derive(Deserialize, Serialize)] pub struct File { + pub version: Version, + #[serde(default)] pub repositories: Vec, } @@ -116,6 +146,7 @@ impl<'a> FromIterator> for File { T: IntoIterator>, { Self { + version: Version::V1, repositories: iter .into_iter() .flat_map(|path| match Repository::save(&path) { From a277a958777349eec08069fe9988d10fd57e7436 Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Sun, 31 Dec 2023 23:08:23 +0900 Subject: [PATCH 3/8] feat: Rename some properties on the dump --- src/sync.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sync.rs b/src/sync.rs index 56892e6..eff08eb 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -16,7 +16,9 @@ pub struct BranchRef { #[derive(Deserialize, Serialize)] #[serde(tag = "type")] pub enum Ref { + #[serde(rename = "remote")] Remote(String), + #[serde(rename = "branch")] Branch(BranchRef), // tag is not supported ... yet. } @@ -129,6 +131,7 @@ impl Repository { #[derive(Deserialize, Serialize)] pub enum Version { + #[serde(rename = "1")] V1, } From 538bc986d8dd64aa803af5b284400b8a75488e83 Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Sun, 31 Dec 2023 23:10:03 +0900 Subject: [PATCH 4/8] feat: Sort repositories before dumping --- src/cmd/sync/dump.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cmd/sync/dump.rs b/src/cmd/sync/dump.rs index 4b260bf..23da30d 100644 --- a/src/cmd/sync/dump.rs +++ b/src/cmd/sync/dump.rs @@ -1,5 +1,6 @@ use anyhow::Result; use clap::Parser; +use itertools::Itertools; use crate::repository::Repositories; use crate::root::Root; @@ -14,6 +15,7 @@ impl Cmd { let file = Repositories::try_collect(&root)? .into_iter() .map(|(p, _)| p) + .sorted_by_key(|p| p.to_string()) .collect::(); println!("{}", toml::to_string(&file)?); From 266e03926bae0e96737a2d4be88e498e1754eb57 Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Sun, 31 Dec 2023 23:10:49 +0900 Subject: [PATCH 5/8] fix: Check any remotes are defined before checking HEAD is synced --- src/sync.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sync.rs b/src/sync.rs index eff08eb..9bed7e7 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -59,6 +59,11 @@ impl Repository { }, }; + let remotes = repo.remotes()?; + if remotes.is_empty() { + bail!("No remotes defined"); + } + let r#ref = match Self::synced_ref(&repo, &head) { Ok(r) => Some(r), Err(e) => { @@ -67,11 +72,6 @@ impl Repository { } }; - let remotes = repo.remotes()?; - if remotes.is_empty() { - bail!("No remotes defined"); - } - Ok(Self { host: path.host.to_string(), owner: path.owner.to_string(), From f12c2b8acba01d32a65f887d463eb543f49dea3e Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Sun, 31 Dec 2023 23:45:34 +0900 Subject: [PATCH 6/8] feat: Use Git CLI to checkout branch --- src/cmd/sync/restore.rs | 27 ++++++++++++++++------- src/git/config.rs | 4 ++++ src/git/mod.rs | 17 ++++++++++++++ src/git/strategy/cli.rs | 49 ++++++++++++++++++++++++++++++++++++++++- src/git/strategy/mod.rs | 29 +++++++++++++++++++++++- 5 files changed, 116 insertions(+), 10 deletions(-) diff --git a/src/cmd/sync/restore.rs b/src/cmd/sync/restore.rs index 29aae33..1667b30 100644 --- a/src/cmd/sync/restore.rs +++ b/src/cmd/sync/restore.rs @@ -8,6 +8,8 @@ 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}; @@ -47,8 +49,8 @@ impl Cmd { .run() .await?; - let path = Path::new(&root, host, owner, repo); - let repo = GitRepository::open(PathBuf::from(&path))?; + let path = PathBuf::from(Path::new(&root, host, owner, repo)); + let repo = GitRepository::open(&path)?; for remote in remotes { if let Err(e) = repo @@ -60,6 +62,15 @@ impl Cmd { _ => 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 { @@ -69,13 +80,13 @@ impl Cmd { info!("Successfully checked out a remote ref: {}", &r); } Some(Ref::Branch(b)) => { - let (object, reference) = repo.revparse_ext(&b.name)?; - let ref_name = reference.unwrap().name().unwrap().to_string(); - - repo.checkout_tree(&object, None)?; - repo.set_head(&ref_name)?; + config.git.strategy.checkout.checkout_branch( + &path, + &b.name, + format!("{}/{}", &b.remote, &b.name), + )?; - info!("Successfully checked out a branch: {}", &ref_name); + info!("Successfully checked out a branch: {}", &b.name); } _ => (), } diff --git a/src/git/config.rs b/src/git/config.rs index 64570d4..493ff1f 100644 --- a/src/git/config.rs +++ b/src/git/config.rs @@ -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)] diff --git a/src/git/mod.rs b/src/git/mod.rs index af401ee..40a2417 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -21,3 +21,20 @@ pub trait CloneRepository { U: ToString, P: AsRef; } + +pub trait Fetch { + fn fetch

(&self, path: P, remote: impl Into) -> Result<()> + where + P: AsRef; +} + +pub trait CheckoutBranch { + fn checkout_branch

( + &self, + path: P, + branch: impl Into, + track: impl Into>, + ) -> Result<()> + where + P: AsRef; +} diff --git a/src/git/strategy/cli.rs b/src/git/strategy/cli.rs index ac60a1b..d8ea132 100644 --- a/src/git/strategy/cli.rs +++ b/src/git/strategy/cli.rs @@ -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; @@ -48,3 +48,50 @@ impl CloneRepository for Cli { } } } + +impl Fetch for Cli { + fn fetch

(&self, path: P, remote: impl Into) -> anyhow::Result<()> + where + P: AsRef, + { + 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

( + &self, + path: P, + branch: impl Into, + track: impl Into>, + ) -> anyhow::Result<()> + where + P: AsRef, + { + 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(), + )), + } + } +} diff --git a/src/git/strategy/mod.rs b/src/git/strategy/mod.rs index 580c612..83a13cc 100644 --- a/src/git/strategy/mod.rs +++ b/src/git/strategy/mod.rs @@ -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 { @@ -25,3 +25,30 @@ impl CloneRepository for Strategy { } } } + +impl Fetch for Strategy { + fn fetch

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

( + &self, + path: P, + branch: impl Into, + track: impl Into>, + ) -> anyhow::Result<()> + where + P: AsRef, + { + match self { + Self::Cli => Cli.checkout_branch(path, branch, track), + } + } +} From ccadabd72c0150aee44d7e40e411d349b3c5798e Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Sun, 31 Dec 2023 23:48:47 +0900 Subject: [PATCH 7/8] fix: Dump full upstream name for a branch --- src/cmd/sync/restore.rs | 2 +- src/sync.rs | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/cmd/sync/restore.rs b/src/cmd/sync/restore.rs index 1667b30..e261279 100644 --- a/src/cmd/sync/restore.rs +++ b/src/cmd/sync/restore.rs @@ -83,7 +83,7 @@ impl Cmd { config.git.strategy.checkout.checkout_branch( &path, &b.name, - format!("{}/{}", &b.remote, &b.name), + Some(b.upstream.to_string()), )?; info!("Successfully checked out a branch: {}", &b.name); diff --git a/src/sync.rs b/src/sync.rs index 9bed7e7..2dfe53d 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -10,7 +10,7 @@ use crate::path::Path; #[derive(Deserialize, Serialize)] pub struct BranchRef { pub name: String, - pub remote: String, + pub upstream: String, } #[derive(Deserialize, Serialize)] @@ -108,6 +108,7 @@ impl Repository { }, }; + let upstream_name = upstream.name()?.unwrap().to_string(); let reference = upstream.into_reference(); if head != &reference { bail!("Branch is not synced"); @@ -115,11 +116,7 @@ impl Repository { Ok(Ref::Branch(BranchRef { name: name.to_string(), - remote: repo - .branch_remote_name(reference.name().unwrap())? - .as_str() - .unwrap() - .to_string(), + upstream: upstream_name, })) } else if head.is_tag() { bail!("HEAD is a tag"); From 6f6d9dbf666ce8e1e05c6e18a5ca02ce67d2e278 Mon Sep 17 00:00:00 2001 From: Natsuki Ikeguchi Date: Sun, 31 Dec 2023 23:51:42 +0900 Subject: [PATCH 8/8] docs: Update README --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ab21bf..48eeba6 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ cmd = "code" args = ["%p"] ``` -> **Note** +> [!NOTE] > `%p` will be replaced by the repository path. ### Finding path of the repository @@ -178,6 +178,18 @@ ghr path --host=github.com # Host root ghr path --host=github.com --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.