upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/bin/ngit
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-07-28 14:10:03 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2025-07-28 14:10:03 +0100
commitcaed1d751eb29d329119c1372c99a651980f42a4 (patch)
tree469a3c1ba4681c209997e8f55dc5a7f7fd9baf74 /src/bin/ngit
parente89dbc142f5a0a517f197562f5f228681d9aed47 (diff)
parentee50baf800f4cb46d17858ba87a3648bb084d8b9 (diff)
Merge branch 'add-ngit-sync-cmd'
Diffstat (limited to 'src/bin/ngit')
-rw-r--r--src/bin/ngit/cli.rs3
-rw-r--r--src/bin/ngit/main.rs1
-rw-r--r--src/bin/ngit/sub_commands/mod.rs1
-rw-r--r--src/bin/ngit/sub_commands/sync.rs161
4 files changed, 166 insertions, 0 deletions
diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs
index 843c904..76874c3 100644
--- a/src/bin/ngit/cli.rs
+++ b/src/bin/ngit/cli.rs
@@ -96,6 +96,9 @@ pub enum Commands {
96 Send(sub_commands::send::SubCommandArgs), 96 Send(sub_commands::send::SubCommandArgs),
97 /// list PRs; checkout, apply or download selected 97 /// list PRs; checkout, apply or download selected
98 List, 98 List,
99 /// update repo git servers to reflect nostr state (add, update or delete
100 /// remote refs)
101 Sync(sub_commands::sync::SubCommandArgs),
99 /// login, logout or export keys 102 /// login, logout or export keys
100 Account(AccountSubCommandArgs), 103 Account(AccountSubCommandArgs),
101} 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;
4pub mod login; 4pub mod login;
5pub mod logout; 5pub mod logout;
6pub mod send; 6pub mod send;
7pub 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 @@
1use anyhow::{Context, Result};
2use 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)]
15pub 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)]
22pub 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 &not_deleted {
150 term.write_line(&format!(" - {name} not deleted"))?;
151 }
152 for name in &not_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}