diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-07-25 23:53:40 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-07-25 23:53:40 +0100 |
| commit | ee50baf800f4cb46d17858ba87a3648bb084d8b9 (patch) | |
| tree | 469a3c1ba4681c209997e8f55dc5a7f7fd9baf74 /src/bin | |
| parent | e5cc566be82bbebb78f2c27ee13f3a5fafa4a0c8 (diff) | |
feat(sync): add cmd to sync git servers
with nostr state. optionally use 'force' flag
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/ngit/cli.rs | 2 | ||||
| -rw-r--r-- | src/bin/ngit/main.rs | 1 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/mod.rs | 1 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/sync.rs | 161 |
4 files changed, 164 insertions, 1 deletions
diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs index 1507f71..76874c3 100644 --- a/src/bin/ngit/cli.rs +++ b/src/bin/ngit/cli.rs | |||
| @@ -98,7 +98,7 @@ pub enum Commands { | |||
| 98 | List, | 98 | List, |
| 99 | /// update repo git servers to reflect nostr state (add, update or delete | 99 | /// update repo git servers to reflect nostr state (add, update or delete |
| 100 | /// remote refs) | 100 | /// remote refs) |
| 101 | Sync, | 101 | Sync(sub_commands::sync::SubCommandArgs), |
| 102 | /// login, logout or export keys | 102 | /// login, logout or export keys |
| 103 | Account(AccountSubCommandArgs), | 103 | Account(AccountSubCommandArgs), |
| 104 | } | 104 | } |
diff --git a/src/bin/ngit/main.rs b/src/bin/ngit/main.rs index f896e97..f07203a 100644 --- a/src/bin/ngit/main.rs +++ b/src/bin/ngit/main.rs | |||
| @@ -32,6 +32,7 @@ async fn main() -> Result<()> { | |||
| 32 | Commands::Init(args) => sub_commands::init::launch(&cli, args).await, | 32 | Commands::Init(args) => sub_commands::init::launch(&cli, args).await, |
| 33 | Commands::List => sub_commands::list::launch().await, | 33 | Commands::List => sub_commands::list::launch().await, |
| 34 | Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await, | 34 | Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await, |
| 35 | Commands::Sync(args) => sub_commands::sync::launch(args).await, | ||
| 35 | } | 36 | } |
| 36 | } else { | 37 | } else { |
| 37 | // Handle the case where no command is provided | 38 | // Handle the case where no command is provided |
diff --git a/src/bin/ngit/sub_commands/mod.rs b/src/bin/ngit/sub_commands/mod.rs index b59b88f..b2e7c9a 100644 --- a/src/bin/ngit/sub_commands/mod.rs +++ b/src/bin/ngit/sub_commands/mod.rs | |||
| @@ -4,3 +4,4 @@ pub mod list; | |||
| 4 | pub mod login; | 4 | pub mod login; |
| 5 | pub mod logout; | 5 | pub mod logout; |
| 6 | pub mod send; | 6 | pub mod send; |
| 7 | pub mod sync; | ||
diff --git a/src/bin/ngit/sub_commands/sync.rs b/src/bin/ngit/sub_commands/sync.rs new file mode 100644 index 0000000..c1a3484 --- /dev/null +++ b/src/bin/ngit/sub_commands/sync.rs | |||
| @@ -0,0 +1,161 @@ | |||
| 1 | use anyhow::{Context, Result}; | ||
| 2 | use ngit::{ | ||
| 3 | client::{ | ||
| 4 | Client, Connect, Params, fetching_with_report, get_repo_ref_from_cache, | ||
| 5 | get_state_from_cache, | ||
| 6 | }, | ||
| 7 | git::{Repo, RepoActions}, | ||
| 8 | list::{get_ahead_behind, list_from_remotes}, | ||
| 9 | push::push_to_remote, | ||
| 10 | repo_ref::get_repo_coordinates_when_remote_unknown, | ||
| 11 | utils::get_short_git_server_name, | ||
| 12 | }; | ||
| 13 | |||
| 14 | #[derive(Debug, clap::Args)] | ||
| 15 | pub struct SubCommandArgs { | ||
| 16 | /// force push updates and delete refs from non-grasp git servers | ||
| 17 | #[arg(long, action)] | ||
| 18 | force: bool, | ||
| 19 | } | ||
| 20 | |||
| 21 | #[allow(clippy::too_many_lines)] | ||
| 22 | pub async fn launch(args: &SubCommandArgs) -> Result<()> { | ||
| 23 | let git_repo = Repo::discover().context("failed to find a git repository")?; | ||
| 24 | let git_repo_path = git_repo.get_path()?; | ||
| 25 | |||
| 26 | let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo))); | ||
| 27 | |||
| 28 | let (nostr_remote_name, decoded_nostr_url) = git_repo | ||
| 29 | .get_first_nostr_remote_when_in_ngit_binary() | ||
| 30 | .await.context("failed to list git remotes")? | ||
| 31 | .context("no `nostr://` remote detected. `ngit sync` must be run from a repo with a nostr remote")?; | ||
| 32 | |||
| 33 | let repo_coordinate = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?; | ||
| 34 | |||
| 35 | let _ = fetching_with_report(git_repo_path, &client, &repo_coordinate).await?; | ||
| 36 | |||
| 37 | // TODO push announcement event, then state event to grasps | ||
| 38 | |||
| 39 | let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinate).await?; | ||
| 40 | |||
| 41 | let nostr_state = get_state_from_cache(Some(git_repo_path), &repo_ref).await?; | ||
| 42 | |||
| 43 | let term = console::Term::stderr(); | ||
| 44 | |||
| 45 | let remote_states = list_from_remotes( | ||
| 46 | &term, | ||
| 47 | &git_repo, | ||
| 48 | &repo_ref.git_server, | ||
| 49 | &repo_ref.to_nostr_git_url(&None), | ||
| 50 | &repo_ref.grasp_servers(), | ||
| 51 | ); | ||
| 52 | |||
| 53 | for (url, (remote_state, is_grasp_server)) in &remote_states { | ||
| 54 | let remote_name = get_short_git_server_name(&git_repo, url); | ||
| 55 | let mut refspecs = vec![]; | ||
| 56 | // delete ref from remote | ||
| 57 | let mut not_deleted = vec![]; | ||
| 58 | for remote_ref_name in remote_state.keys() { | ||
| 59 | if (!remote_ref_name.starts_with("refs/heads/pr/") | ||
| 60 | && (remote_ref_name.starts_with("refs/heads/") | ||
| 61 | || remote_ref_name.starts_with("refs/tags/"))) | ||
| 62 | && !nostr_state | ||
| 63 | .state | ||
| 64 | .keys() | ||
| 65 | .any(|nostr_ref| nostr_ref.eq(remote_ref_name)) | ||
| 66 | { | ||
| 67 | if *is_grasp_server || args.force { | ||
| 68 | // delete branches / tags not on nostr | ||
| 69 | refspecs.push(format!(":{remote_ref_name}")); | ||
| 70 | } else { | ||
| 71 | not_deleted.push(remote_ref_name); | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
| 75 | // add or update ref on remote | ||
| 76 | let mut not_updated = vec![]; | ||
| 77 | for nostr_ref_name in nostr_state.state.keys() { | ||
| 78 | if nostr_ref_name.starts_with("refs/heads/") | ||
| 79 | || nostr_ref_name.starts_with("refs/tags/") | ||
| 80 | || !nostr_ref_name.starts_with("refs/heads/pr/") | ||
| 81 | { | ||
| 82 | // ensure nostr_state only supports refs/heads and refs/tags/ | ||
| 83 | // and not refs/heads/prs/* | ||
| 84 | } else if let Some(remote_ref_value) = remote_state.get(nostr_ref_name) { | ||
| 85 | // update ref | ||
| 86 | let force_required = { | ||
| 87 | if let Ok((ahead, _)) = | ||
| 88 | get_ahead_behind(&git_repo, nostr_ref_name, remote_ref_value) | ||
| 89 | { | ||
| 90 | !ahead.is_empty() | ||
| 91 | } else { | ||
| 92 | true | ||
| 93 | } | ||
| 94 | }; | ||
| 95 | if nostr_state | ||
| 96 | .state | ||
| 97 | .get(nostr_ref_name) | ||
| 98 | .is_none_or(|nostr_ref_value| nostr_ref_value.eq(remote_ref_value)) | ||
| 99 | { | ||
| 100 | // no action if ref in sync | ||
| 101 | } else if remote_ref_value.starts_with("ref ") && !(args.force || *is_grasp_server) | ||
| 102 | { | ||
| 103 | // dont try and sync push symbolic refs | ||
| 104 | } else if !force_required { | ||
| 105 | refspecs.push(format!( | ||
| 106 | "refs/remotes/{nostr_remote_name}/{nostr_ref_name}:{nostr_ref_name}", | ||
| 107 | )); | ||
| 108 | } else if *is_grasp_server || args.force { | ||
| 109 | refspecs.push(format!( | ||
| 110 | "+refs/remotes/{nostr_remote_name}/{nostr_ref_name}:{nostr_ref_name}", | ||
| 111 | )); | ||
| 112 | } else { | ||
| 113 | not_updated.push(nostr_ref_name); | ||
| 114 | } | ||
| 115 | } else { | ||
| 116 | // add missing refs | ||
| 117 | refspecs.push(format!( | ||
| 118 | "refs/remotes/{nostr_remote_name}/{nostr_ref_name}:{nostr_ref_name}", | ||
| 119 | )); | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | if refspecs.is_empty() { | ||
| 124 | if !not_updated.is_empty() || !not_deleted.is_empty() { | ||
| 125 | term.write_line(&format!("{remote_name} in sync excluding"))?; | ||
| 126 | } else { | ||
| 127 | term.write_line(&format!("{remote_name} already in sync"))?; | ||
| 128 | } | ||
| 129 | // report already in sync | ||
| 130 | } else if let Err(error) = push_to_remote( | ||
| 131 | &git_repo, | ||
| 132 | url, | ||
| 133 | &decoded_nostr_url, | ||
| 134 | &refspecs, | ||
| 135 | &term, | ||
| 136 | *is_grasp_server, | ||
| 137 | ) { | ||
| 138 | term.write_line(&format!( | ||
| 139 | "error pushing updates to {remote_name}: error: {error}" | ||
| 140 | ))?; | ||
| 141 | } else if *is_grasp_server || args.force { | ||
| 142 | term.write_line(&format!("{remote_name} sync completed"))?; | ||
| 143 | // TODO we only know if there was an error but not if it | ||
| 144 | // rejected any updates | ||
| 145 | } else { | ||
| 146 | // we should report on refs not force pushed | ||
| 147 | term.write_line(&format!("{remote_name} sync completed"))?; | ||
| 148 | } | ||
| 149 | for name in ¬_deleted { | ||
| 150 | term.write_line(&format!(" - {name} not deleted"))?; | ||
| 151 | } | ||
| 152 | for name in ¬_updated { | ||
| 153 | term.write_line(&format!(" - {name} not updated due to conflicts"))?; | ||
| 154 | } | ||
| 155 | if !not_updated.is_empty() || !not_deleted.is_empty() { | ||
| 156 | term.write_line("run `ngit sync --force` to delete refs or overwrite conflicts and potentially lose work")?; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | Ok(()) | ||
| 161 | } | ||