upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/fetch_pull_push.rs
blob: 637c0403beaa6f7f65fb36ad30462c3af90279ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
use std::{env::current_dir, path::PathBuf, vec};

use dialoguer::{Confirm, theme::ColorfulTheme};
use git2::{BranchType};
use nostr::{Keys};
use nostr_sdk::blocking::Client;

use crate::{groups::groups::Groups, repos::repo::Repo, utils::{create_client, get_stored_keys, get_or_generate_keys}, config::load_config, repo_config::RepoConfig, funcs::{find_commits_ahead::find_commits_ahead, apply_patches::apply_patches, get_updates_of_patches::get_updates_of_patches, create_patches::create_and_broadcast_patches_from_oid, create_branch_and_pr::create_branch_and_pr, get_branch_event_from_user_input::{get_unmapped_branch_event_from_user_input,get_branch_event_from_user_input}, create_local_branch_from_user_input::create_local_branch_from_user_input, checkout_branch::{checkout_branch_from_name}}, branch_refs::{get_branch_refs, BranchRefs}, ngit_tag::{tag_is_commit_parent, tag_extract_value}};

/// will only pull if no rebase required or push if no downstream conflicts detected
pub fn fetch_pull_push(
    keys: Option<&Keys>,
    pull: bool,
    push: bool,
    proposed_branch_to_pull: Option<String>,
    clone: bool,
    repo_dir_path: Option<PathBuf>,
    client: Option<Client>,
) -> BranchRefs {

    let repo_dir_path = match repo_dir_path {
        None => current_dir().unwrap(),
        Some(p) => p,
    };

    let git_repo = git2::Repository::open(&repo_dir_path)
        .expect("git repo not initialized. run ngit init first");

    if !repo_dir_path.join(".ngit").is_dir() {
        panic!("ngit not initialised. Run 'ngit init' first...");
    }

    let repo_has_no_commits = git_repo.branches(Some(BranchType::Local))
        .expect("git_repo.branches to not error even if its a blank repo")
        .count() == 0;

    // check whether we already have a local branch mapped to proposed_branch_to_pull
    let proposed_branch_to_pull = match proposed_branch_to_pull {
        // it was never specified
        None => None,
        Some(id) => {
            let branch_id = get_branch_event_from_user_input(
                &Some(id),
                &BranchRefs::new(
                    vec![],
                    repo_dir_path.clone(),
                ),
                &repo_dir_path,
            ).id.to_string();
            match RepoConfig::open(&repo_dir_path).branch_name_from_id(&branch_id) {
                // we dont have a mapped local branch
                None => Some(branch_id),
                // we have a branch mapping. check it out and do a normal pull
                Some(name) => {
                    checkout_branch_from_name(
                        &git_repo,
                        name,
                    );
                    None
                }
            }
        }
    };

    let branch_name = if clone || repo_has_no_commits || proposed_branch_to_pull.is_some() {
        None
    } else {
        // no commits
        if git_repo.branches(Some(BranchType::Local))
            .expect("git_repo.branches to not error even if its a blank repo")
            .count() == 0 {
            panic!("There are no branches. you should use clone instead")
        }
        let head = git_repo.head()
            .expect("git_repo returns head");
        if !head.is_branch() {
            // TODO: fetch should still work here?
            panic!("check out a branch to continue. you have an another object checked out such as a tag or commit.");
        }
        Some(
            head.shorthand()
                .expect("head is branch so head.shortand() should retunr branch name")
                .to_string()
        )
    };

    let repo = Repo::open(&repo_dir_path);

    let new_commits_to_push =
        if clone || repo_has_no_commits || proposed_branch_to_pull.is_some() { vec![] }
        else {
            find_commits_ahead(
                &git_repo,
                &repo_dir_path,
                &branch_name.clone()
                    .expect("branch_name to always be defined (clone and pull_new_branch do not reach here)"),
            )
        }
    ;

    let mut cfg = load_config();

    let keys = match keys {
        None => {
            match get_stored_keys(&mut cfg) {
                None => {
                    if push { get_or_generate_keys(&mut cfg) }
                    else {Keys::generate() }
                }
                Some(k) => k.clone(),
            }
        },
        Some(k) => k.clone(),
    };

    let client = match client {
        None => create_client(&keys, repo.relays.clone())
            .expect("create_client returns client"),
        Some(client) => client,
    };

    let mut branch_refs = get_branch_refs(&repo, &client, &repo_dir_path);

    let branch_id:String = 
        if clone || repo_has_no_commits { repo.id.to_string() }
        else if proposed_branch_to_pull.is_some() {
            get_unmapped_branch_event_from_user_input(
                &proposed_branch_to_pull,
                &branch_refs,
                &repo_dir_path,
            ).id.to_string()
        }
        else {
            let name = &branch_name.clone()
                .expect("branch_name to always be defined (clone and pull_new_branch do not reach here)");

            match RepoConfig::open(&repo_dir_path)
                .branch_id_from_name(
                    &name,
                ) {
                    None => {
                        if push {
                            create_branch_and_pr(
                                &name,
                                new_commits_to_push.len(),
                                &repo_dir_path,
                                &repo,
                                &mut branch_refs,
                                &keys,
                                &mut cfg,
                                &client,
                            )
                        } else {
                            println!("branch '{}' hasn't been pushed!",&name);
                            return branch_refs;
                        }
                    },
                    Some(id) => id.clone(),
            }
        }
    ;

    let mut patches = get_updates_of_patches(
        &client,
        &mut branch_refs,
        &git_repo,
        &repo_dir_path,
        &branch_id,
        &branch_name,
        proposed_branch_to_pull.is_some(),
    );

    let mut confirmed_branch_name = 
        if clone || repo_has_no_commits { "master/main".to_string() }
        else if proposed_branch_to_pull.is_some() {
            match patches.get(0) {
                None => {
                    match RepoConfig::open(&repo_dir_path)
                    .branch_name_from_id(
                        &branch_id,
                    ) {
                        None => {
                            // TODO you should still be able to check it out -  find the easliest commit, check it out as branch, find the latest commit and set the branch to that commit.
                            println!("No new commits were found. Not pulling the branch.");
                        },
                        Some(name) => {
                            println!("exists as a local branch named '{}'. check it out and then fetch /pull.",name);
                        },
                    }
                    return branch_refs;
                }
                Some(earliest_patch_event) => {
                    // create local branch from off of parent commit
                    create_local_branch_from_user_input(
                        &repo_dir_path,
                        &git_repo,
                        &branch_refs.branch_as_repo(Some(&branch_id)).name,
                        &tag_extract_value(
                            earliest_patch_event.tags.iter()
                            .find(|t|tag_is_commit_parent(t))
                            .expect("patch event to have parent commit"),
                        ),
                        &branch_id
                    )
                }
            }
        }
        else {
            branch_name.clone()
            .expect("branch_name to exists. clone and pull_new_branch don't reach here")
        }
    ;

    // no patches or new commits
    if patches.is_empty()
        && new_commits_to_push.is_empty()
    {
        println!(
            "branch '{}' is up-to-date{}",
            &confirmed_branch_name,
            if push { ". no changes to push." }
            else { "!"}
        );
    }
    // patches with no new commits
    else if new_commits_to_push.is_empty()
    {
        println!(
            "branch '{}' {} behind{}",
            &confirmed_branch_name,
            &patches.len(),
            if push { ". no changes to push." }
            else { "!"}
        );
        if pull || proposed_branch_to_pull.is_some() {
            // apply patches
            apply_patches(
                &git_repo,
                &repo_dir_path,
                &mut patches,
            );

            // update repo_config
            let mut repo_config = RepoConfig::open(&repo_dir_path);
            // update branch mapping
            if clone || repo_has_no_commits {
                confirmed_branch_name = git_repo.head()
                    .expect("we have just cloned and therefore commited to main branch so git_repo.head should not error")
                    .shorthand()
                    .expect("shorthand to be moast / main")
                    .to_string();
                repo_config.set_mapping(&confirmed_branch_name, &repo.id.to_string());
            }
            // update branch update timestamp
            match patches.last() {
                Some(p) => {
                    repo_config.set_last_patch_update_time(
                        branch_id.clone(),
                        p.created_at.clone(),
                    );
                }
                None => (),
            };

            println!(
                "branch '{}' is up-to-date!",
                &confirmed_branch_name
            );
        }
    }
    else {
        let update = format!(
            "branch '{}' {} behind and {} ahead",
            &confirmed_branch_name,
            &patches.len(),
            &new_commits_to_push.len(),
        );
        // new commits and new patches
        if !patches.is_empty() {
            if pull { println!("{update}. TODO enable rebase option... pull to branch?"); }
            else if push { println!("{update} TODO enable for push option. TODO enable rebase option... pull to branch?"); }
            else { println!("{update}"); }
            // there have been 3 more commits on the main branch. would you like to rebase before pushing your new branch?
        // there has been 1 commit(s) the branch you are pushing 'feat:add-stuff'. how would you like to proceed?
        // [ ] rebase my commits
        // [ ] ignore commit(s) 

        }
        // new commits with no patches
        else {
            println!("{update}");
            if push {
                if Confirm::with_theme(&ColorfulTheme::default())
                .with_prompt(format!(
                    "push {} commits on the '{}' branch?",
                    &new_commits_to_push.len(),
                    &confirmed_branch_name
                ))
                .default(true)
                .interact()
                .unwrap()
                {
                    // get keys
                    let mut cfg = load_config();
                    let keys = get_or_generate_keys(&mut cfg);
            
                    // check permission
                    let groups = Groups::new();
                    let maintainers = groups.by_event_id(
                        repo.maintainers_group.get_first_active_group()
                            .expect("maintainers_group will never be null")
                    )
                        .expect("always will have the maintainers_group initialisaiton event cached")
                        .members();
                    if maintainers.iter().any(|k| keys.public_key() == **k) {
                        println!(
                            "you are a repo maintainer and have the permission to push to '{}'!",
                            &confirmed_branch_name,
                        )
                    } if match branch_refs.is_authorized(Some(&branch_id), &keys.public_key()) {
                        None => false,
                        Some(authorized) => authorized,
                    } {
                        println!(
                            "you have the permission to push to '{}'!",
                            &confirmed_branch_name,
                        )
                    } 
                    else {                        
                        panic!(
                            "You are not a repo maintainer so you  don't have permission to push to '{}' branch :(",
                            &confirmed_branch_name,
                        );
                    }
                    create_and_broadcast_patches_from_oid(
                        new_commits_to_push,
                        &git_repo,
                        &repo_dir_path,
                        &repo,
                        &branch_id,
                        &keys,
                    );
                }
            }
        }
    }

    // there have been 3 more commits on the main branch. would you like to rebase before pushing your new branch?
    // there has been 1 commit(s) the branch you are pushing 'feat:add-stuff'. how would you like to proceed?
    // [ ] rebase my commits
    // [ ] ignore commit(s) 


        // let ngit_path = repo_dir_path.join(".ngit");
    // // CURRENTLY UNUSED identify new merges 
    // let new_merge_ids: Vec<&String> = branch_refs.merged_branches_ids
    //     .iter()
    //     .filter(|id|
    //         ngit_path.join(format!("merges/{}.json",id)).exists()
    //     )
    //     .collect();
    // // TODO: identify new PullRequests to report
    
    // Non closed PRs and branches
    // TODO add a status-update custom tag for so PRs can be marked as closed or reopened.
        // then we can gather status updates and filter out closed branches and build open one.
        // merge - commit, from-branch, to-branch
    
    // find patches
    // get latest chain of patches on main
    
    // identify merged branches
        // will there always be a pull request for a branch?

    // get patches from maitainers or branches merged by maintainers and permission groups for these branches

    branch_refs
}