Handle remote branches

This commit is contained in:
Gregory Schier
2025-02-07 13:21:30 -08:00
parent 2da898d2d4
commit a42bee098b
20 changed files with 272 additions and 32 deletions

File diff suppressed because one or more lines are too long

View File

@@ -5432,6 +5432,11 @@
"type": "string",
"const": "yaak-git:allow-checkout"
},
{
"description": "Enables the checkout_remote command without any pre-configured scope.",
"type": "string",
"const": "yaak-git:allow-checkout-remote"
},
{
"description": "Enables the commit command without any pre-configured scope.",
"type": "string",
@@ -5442,6 +5447,11 @@
"type": "string",
"const": "yaak-git:allow-delete-branch"
},
{
"description": "Enables the fetch_all command without any pre-configured scope.",
"type": "string",
"const": "yaak-git:allow-fetch-all"
},
{
"description": "Enables the initialize command without any pre-configured scope.",
"type": "string",
@@ -5492,6 +5502,11 @@
"type": "string",
"const": "yaak-git:deny-checkout"
},
{
"description": "Denies the checkout_remote command without any pre-configured scope.",
"type": "string",
"const": "yaak-git:deny-checkout-remote"
},
{
"description": "Denies the commit command without any pre-configured scope.",
"type": "string",
@@ -5502,6 +5517,11 @@
"type": "string",
"const": "yaak-git:deny-delete-branch"
},
{
"description": "Denies the fetch_all command without any pre-configured scope.",
"type": "string",
"const": "yaak-git:deny-fetch-all"
},
{
"description": "Denies the initialize command without any pre-configured scope.",
"type": "string",

View File

@@ -5432,6 +5432,11 @@
"type": "string",
"const": "yaak-git:allow-checkout"
},
{
"description": "Enables the checkout_remote command without any pre-configured scope.",
"type": "string",
"const": "yaak-git:allow-checkout-remote"
},
{
"description": "Enables the commit command without any pre-configured scope.",
"type": "string",
@@ -5442,6 +5447,11 @@
"type": "string",
"const": "yaak-git:allow-delete-branch"
},
{
"description": "Enables the fetch_all command without any pre-configured scope.",
"type": "string",
"const": "yaak-git:allow-fetch-all"
},
{
"description": "Enables the initialize command without any pre-configured scope.",
"type": "string",
@@ -5492,6 +5502,11 @@
"type": "string",
"const": "yaak-git:deny-checkout"
},
{
"description": "Denies the checkout_remote command without any pre-configured scope.",
"type": "string",
"const": "yaak-git:deny-checkout-remote"
},
{
"description": "Denies the commit command without any pre-configured scope.",
"type": "string",
@@ -5502,6 +5517,11 @@
"type": "string",
"const": "yaak-git:deny-delete-branch"
},
{
"description": "Denies the fetch_all command without any pre-configured scope.",
"type": "string",
"const": "yaak-git:deny-fetch-all"
},
{
"description": "Denies the initialize command without any pre-configured scope.",
"type": "string",

View File

@@ -9,7 +9,7 @@ export type GitStatus = "added" | "conflict" | "current" | "modified" | "removed
export type GitStatusEntry = { relaPath: string, status: GitStatus, staged: boolean, prev: SyncModel | null, next: SyncModel | null, };
export type GitStatusSummary = { path: string, headRef: string | null, headRefShorthand: string | null, entries: Array<GitStatusEntry>, origins: Array<string>, branches: Array<string>, };
export type GitStatusSummary = { path: string, headRef: string | null, headRefShorthand: string | null, entries: Array<GitStatusEntry>, origins: Array<string>, localBranches: Array<string>, remoteBranches: Array<string>, };
export type PullResult = { receivedBytes: number, receivedObjects: number, };

View File

@@ -4,6 +4,7 @@ const COMMANDS: &[&str] = &[
"checkout",
"commit",
"delete_branch",
"fetch_all",
"initialize",
"log",
"merge_branch",

View File

@@ -41,7 +41,7 @@ export function useGit(dir: string) {
mutationFn: (args) => invoke('plugin:yaak-git|delete_branch', { dir, ...args }),
onSuccess,
}),
checkout: useMutation<void, string, { branch: string; force: boolean }>({
checkout: useMutation<string, string, { branch: string; force: boolean }>({
mutationKey: ['git', 'checkout', dir],
mutationFn: (args) => invoke('plugin:yaak-git|checkout', { dir, ...args }),
onSuccess,
@@ -51,6 +51,11 @@ export function useGit(dir: string) {
mutationFn: (args) => invoke('plugin:yaak-git|commit', { dir, ...args }),
onSuccess,
}),
fetchAll: useMutation<string, string, void>({
mutationKey: ['git', 'checkout', dir],
mutationFn: () => invoke('plugin:yaak-git|fetch_all', { dir }),
onSuccess,
}),
push: useMutation<PushResult, string, void>({
mutationKey: ['git', 'push', dir],
mutationFn: () => invoke('plugin:yaak-git|push', { dir }),

View File

@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-checkout-remote"
description = "Enables the checkout_remote command without any pre-configured scope."
commands.allow = ["checkout_remote"]
[[permission]]
identifier = "deny-checkout-remote"
description = "Denies the checkout_remote command without any pre-configured scope."
commands.deny = ["checkout_remote"]

View File

@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-fetch-all"
description = "Enables the fetch_all command without any pre-configured scope."
commands.allow = ["fetch_all"]
[[permission]]
identifier = "deny-fetch-all"
description = "Denies the fetch_all command without any pre-configured scope."
commands.deny = ["fetch_all"]

View File

@@ -7,6 +7,7 @@ Default permissions for the plugin
- `allow-checkout`
- `allow-commit`
- `allow-delete-branch`
- `allow-fetch-all`
- `allow-initialize`
- `allow-log`
- `allow-merge-branch`
@@ -105,6 +106,32 @@ Denies the checkout command without any pre-configured scope.
<tr>
<td>
`yaak-git:allow-checkout-remote`
</td>
<td>
Enables the checkout_remote command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-checkout-remote`
</td>
<td>
Denies the checkout_remote command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-commit`
</td>
@@ -157,6 +184,32 @@ Denies the delete_branch command without any pre-configured scope.
<tr>
<td>
`yaak-git:allow-fetch-all`
</td>
<td>
Enables the fetch_all command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:deny-fetch-all`
</td>
<td>
Denies the fetch_all command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`yaak-git:allow-initialize`
</td>

View File

@@ -6,6 +6,7 @@ permissions = [
"allow-checkout",
"allow-commit",
"allow-delete-branch",
"allow-fetch-all",
"allow-initialize",
"allow-log",
"allow-merge-branch",

View File

@@ -324,6 +324,16 @@
"type": "string",
"const": "deny-checkout"
},
{
"description": "Enables the checkout_remote command without any pre-configured scope.",
"type": "string",
"const": "allow-checkout-remote"
},
{
"description": "Denies the checkout_remote command without any pre-configured scope.",
"type": "string",
"const": "deny-checkout-remote"
},
{
"description": "Enables the commit command without any pre-configured scope.",
"type": "string",
@@ -344,6 +354,16 @@
"type": "string",
"const": "deny-delete-branch"
},
{
"description": "Enables the fetch_all command without any pre-configured scope.",
"type": "string",
"const": "allow-fetch-all"
},
{
"description": "Denies the fetch_all command without any pre-configured scope.",
"type": "string",
"const": "deny-fetch-all"
},
{
"description": "Enables the initialize command without any pre-configured scope.",
"type": "string",

View File

@@ -22,9 +22,13 @@ pub(crate) fn branch_set_upstream_after_push(repo: &Repository, branch_name: &st
Ok(())
}
pub(crate) fn git_checkout_branch(dir: &Path, branch: &str, force: bool) -> Result<()> {
pub(crate) fn git_checkout_branch(dir: &Path, branch_name: &str, force: bool) -> Result<String> {
if branch_name.starts_with("origin/") {
return git_checkout_remote_branch(dir, branch_name, force);
}
let repo = open_repo(dir)?;
let branch = get_branch_by_name(&repo, branch)?;
let branch = get_branch_by_name(&repo, branch_name)?;
let branch_ref = branch.into_reference();
let branch_tree = branch_ref.peel_to_tree()?;
@@ -36,7 +40,22 @@ pub(crate) fn git_checkout_branch(dir: &Path, branch: &str, force: bool) -> Resu
repo.checkout_tree(branch_tree.as_object(), Some(&mut options))?;
repo.set_head(branch_ref.name().unwrap())?;
Ok(())
Ok(branch_name.to_string())
}
pub(crate) fn git_checkout_remote_branch(dir: &Path, branch_name: &str, force: bool) -> Result<String> {
let branch_name = branch_name.trim_start_matches("origin/");
let repo = open_repo(dir)?;
let refname = format!("refs/remotes/origin/{}", branch_name);
let remote_ref = repo.find_reference(&refname)?;
let commit = remote_ref.peel_to_commit()?;
let mut new_branch = repo.branch(branch_name, &commit, false)?;
let upstream_name = format!("origin/{}", branch_name);
new_branch.set_upstream(Some(&upstream_name))?;
return git_checkout_branch(dir, branch_name, force)
}
pub(crate) fn git_create_branch(dir: &Path, name: &str) -> Result<()> {

View File

@@ -1,5 +1,6 @@
use crate::branch::{git_checkout_branch, git_create_branch, git_delete_branch, git_merge_branch};
use crate::error::Result;
use crate::fetch::git_fetch_all;
use crate::git::{
git_add, git_commit, git_init, git_log, git_status, git_unstage, GitCommit, GitStatusSummary,
};
@@ -10,7 +11,7 @@ use tauri::command;
// NOTE: All of these commands are async to prevent blocking work from locking up the UI
#[command]
pub async fn checkout(dir: &Path, branch: &str, force: bool) -> Result<()> {
pub async fn checkout(dir: &Path, branch: &str, force: bool) -> Result<String> {
git_checkout_branch(dir, branch, force)
}
@@ -49,6 +50,11 @@ pub async fn commit(dir: &Path, message: &str) -> Result<()> {
git_commit(dir, message)
}
#[command]
pub async fn fetch_all(dir: &Path) -> Result<()> {
git_fetch_all(dir)
}
#[command]
pub async fn push(dir: &Path) -> Result<PushResult> {
git_push(dir)

View File

@@ -0,0 +1,37 @@
use crate::callbacks::default_callbacks;
use crate::error::Result;
use crate::repository::open_repo;
use git2::{FetchOptions, ProxyOptions, Repository};
use std::path::Path;
pub(crate) fn git_fetch_all(dir: &Path) -> Result<()> {
let repo = open_repo(dir)?;
let remotes = repo.remotes()?.iter().flatten().map(String::from).collect::<Vec<_>>();
for (_idx, remote) in remotes.into_iter().enumerate() {
fetch_from_remote(&repo, &remote)?;
}
Ok(())
}
fn fetch_from_remote(repo: &Repository, remote: &str) -> Result<()> {
let mut remote = repo.find_remote(remote)?;
let mut options = FetchOptions::new();
let callbacks = default_callbacks();
options.prune(git2::FetchPrune::On);
let mut proxy = ProxyOptions::new();
proxy.auto();
options.proxy_options(proxy);
options.download_tags(git2::AutotagOption::All);
options.remote_callbacks(callbacks);
remote.fetch(&[] as &[&str], Some(&mut options), None)?;
// fetch tags (also removing remotely deleted ones)
remote.fetch(&["refs/tags/*:refs/tags/*"], Some(&mut options), None)?;
Ok(())
}

View File

@@ -1,6 +1,6 @@
use crate::error::Result;
use crate::repository::open_repo;
use crate::util::list_branch_names;
use crate::util::{local_branch_names, remote_branch_names};
use chrono::{DateTime, Utc};
use git2::IndexAddOption;
use log::{info, warn};
@@ -19,7 +19,8 @@ pub struct GitStatusSummary {
pub head_ref_shorthand: Option<String>,
pub entries: Vec<GitStatusEntry>,
pub origins: Vec<String>,
pub branches: Vec<String>,
pub local_branches: Vec<String>,
pub remote_branches: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
@@ -288,7 +289,8 @@ pub fn git_status(dir: &Path) -> Result<GitStatusSummary> {
}
let origins = repo.remotes()?.into_iter().filter_map(|o| Some(o?.to_string())).collect();
let branches = list_branch_names(&repo)?;
let local_branches = local_branch_names(&repo)?;
let remote_branches = remote_branch_names(&repo)?;
Ok(GitStatusSummary {
entries,
@@ -296,7 +298,8 @@ pub fn git_status(dir: &Path) -> Result<GitStatusSummary> {
path: dir.to_string_lossy().to_string(),
head_ref,
head_ref_shorthand,
branches,
local_branches,
remote_branches,
})
}

View File

@@ -1,4 +1,4 @@
use crate::commands::{add, branch, checkout, commit, delete_branch, initialize, log, merge_branch, pull, push, status, unstage};
use crate::commands::{add, branch, checkout, commit, delete_branch, fetch_all, initialize, log, merge_branch, pull, push, status, unstage};
use tauri::{
generate_handler,
plugin::{Builder, TauriPlugin},
@@ -9,6 +9,7 @@ mod branch;
mod callbacks;
mod commands;
mod error;
mod fetch;
mod git;
mod merge;
mod pull;
@@ -24,6 +25,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
checkout,
commit,
delete_branch,
fetch_all,
initialize,
log,
merge_branch,

View File

@@ -34,7 +34,7 @@ pub(crate) fn get_current_branch(repo: &Repository) -> Result<Option<Branch>> {
Ok(None)
}
pub(crate) fn list_branch_names(repo: &Repository) -> Result<Vec<String>> {
pub(crate) fn local_branch_names(repo: &Repository) -> Result<Vec<String>> {
let mut branches = Vec::new();
for branch in repo.branches(Some(BranchType::Local))? {
let branch = branch?.0;
@@ -45,6 +45,17 @@ pub(crate) fn list_branch_names(repo: &Repository) -> Result<Vec<String>> {
Ok(branches)
}
pub(crate) fn remote_branch_names(repo: &Repository) -> Result<Vec<String>> {
let mut branches = Vec::new();
for branch in repo.branches(Some(BranchType::Remote))? {
let branch = branch?.0;
let name = branch.name_bytes()?;
let name = bytes_to_string(name)?;
branches.push(name);
}
Ok(branches)
}
pub(crate) fn get_branch_by_name<'s>(repo: &'s Repository, name: &str) -> Result<Branch<'s>> {
Ok(repo.find_branch(name, BranchType::Local)?)
}