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", "type": "string",
"const": "yaak-git:allow-checkout" "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.", "description": "Enables the commit command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5442,6 +5447,11 @@
"type": "string", "type": "string",
"const": "yaak-git:allow-delete-branch" "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.", "description": "Enables the initialize command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5492,6 +5502,11 @@
"type": "string", "type": "string",
"const": "yaak-git:deny-checkout" "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.", "description": "Denies the commit command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5502,6 +5517,11 @@
"type": "string", "type": "string",
"const": "yaak-git:deny-delete-branch" "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.", "description": "Denies the initialize command without any pre-configured scope.",
"type": "string", "type": "string",

View File

@@ -5432,6 +5432,11 @@
"type": "string", "type": "string",
"const": "yaak-git:allow-checkout" "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.", "description": "Enables the commit command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5442,6 +5447,11 @@
"type": "string", "type": "string",
"const": "yaak-git:allow-delete-branch" "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.", "description": "Enables the initialize command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5492,6 +5502,11 @@
"type": "string", "type": "string",
"const": "yaak-git:deny-checkout" "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.", "description": "Denies the commit command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -5502,6 +5517,11 @@
"type": "string", "type": "string",
"const": "yaak-git:deny-delete-branch" "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.", "description": "Denies the initialize command without any pre-configured scope.",
"type": "string", "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 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, }; export type PullResult = { receivedBytes: number, receivedObjects: number, };

View File

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

View File

@@ -41,7 +41,7 @@ export function useGit(dir: string) {
mutationFn: (args) => invoke('plugin:yaak-git|delete_branch', { dir, ...args }), mutationFn: (args) => invoke('plugin:yaak-git|delete_branch', { dir, ...args }),
onSuccess, onSuccess,
}), }),
checkout: useMutation<void, string, { branch: string; force: boolean }>({ checkout: useMutation<string, string, { branch: string; force: boolean }>({
mutationKey: ['git', 'checkout', dir], mutationKey: ['git', 'checkout', dir],
mutationFn: (args) => invoke('plugin:yaak-git|checkout', { dir, ...args }), mutationFn: (args) => invoke('plugin:yaak-git|checkout', { dir, ...args }),
onSuccess, onSuccess,
@@ -51,6 +51,11 @@ export function useGit(dir: string) {
mutationFn: (args) => invoke('plugin:yaak-git|commit', { dir, ...args }), mutationFn: (args) => invoke('plugin:yaak-git|commit', { dir, ...args }),
onSuccess, onSuccess,
}), }),
fetchAll: useMutation<string, string, void>({
mutationKey: ['git', 'checkout', dir],
mutationFn: () => invoke('plugin:yaak-git|fetch_all', { dir }),
onSuccess,
}),
push: useMutation<PushResult, string, void>({ push: useMutation<PushResult, string, void>({
mutationKey: ['git', 'push', dir], mutationKey: ['git', 'push', dir],
mutationFn: () => invoke('plugin:yaak-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-checkout`
- `allow-commit` - `allow-commit`
- `allow-delete-branch` - `allow-delete-branch`
- `allow-fetch-all`
- `allow-initialize` - `allow-initialize`
- `allow-log` - `allow-log`
- `allow-merge-branch` - `allow-merge-branch`
@@ -105,6 +106,32 @@ Denies the checkout command without any pre-configured scope.
<tr> <tr>
<td> <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` `yaak-git:allow-commit`
</td> </td>
@@ -157,6 +184,32 @@ Denies the delete_branch command without any pre-configured scope.
<tr> <tr>
<td> <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` `yaak-git:allow-initialize`
</td> </td>

View File

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

View File

@@ -324,6 +324,16 @@
"type": "string", "type": "string",
"const": "deny-checkout" "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.", "description": "Enables the commit command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -344,6 +354,16 @@
"type": "string", "type": "string",
"const": "deny-delete-branch" "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.", "description": "Enables the initialize command without any pre-configured scope.",
"type": "string", "type": "string",

View File

@@ -22,9 +22,13 @@ pub(crate) fn branch_set_upstream_after_push(repo: &Repository, branch_name: &st
Ok(()) 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 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_ref = branch.into_reference();
let branch_tree = branch_ref.peel_to_tree()?; 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.checkout_tree(branch_tree.as_object(), Some(&mut options))?;
repo.set_head(branch_ref.name().unwrap())?; 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<()> { 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::branch::{git_checkout_branch, git_create_branch, git_delete_branch, git_merge_branch};
use crate::error::Result; use crate::error::Result;
use crate::fetch::git_fetch_all;
use crate::git::{ use crate::git::{
git_add, git_commit, git_init, git_log, git_status, git_unstage, GitCommit, GitStatusSummary, 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 // NOTE: All of these commands are async to prevent blocking work from locking up the UI
#[command] #[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) git_checkout_branch(dir, branch, force)
} }
@@ -49,6 +50,11 @@ pub async fn commit(dir: &Path, message: &str) -> Result<()> {
git_commit(dir, message) git_commit(dir, message)
} }
#[command]
pub async fn fetch_all(dir: &Path) -> Result<()> {
git_fetch_all(dir)
}
#[command] #[command]
pub async fn push(dir: &Path) -> Result<PushResult> { pub async fn push(dir: &Path) -> Result<PushResult> {
git_push(dir) 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::error::Result;
use crate::repository::open_repo; 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 chrono::{DateTime, Utc};
use git2::IndexAddOption; use git2::IndexAddOption;
use log::{info, warn}; use log::{info, warn};
@@ -19,7 +19,8 @@ pub struct GitStatusSummary {
pub head_ref_shorthand: Option<String>, pub head_ref_shorthand: Option<String>,
pub entries: Vec<GitStatusEntry>, pub entries: Vec<GitStatusEntry>,
pub origins: Vec<String>, 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)] #[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 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 { Ok(GitStatusSummary {
entries, entries,
@@ -296,7 +298,8 @@ pub fn git_status(dir: &Path) -> Result<GitStatusSummary> {
path: dir.to_string_lossy().to_string(), path: dir.to_string_lossy().to_string(),
head_ref, head_ref,
head_ref_shorthand, 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::{ use tauri::{
generate_handler, generate_handler,
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
@@ -9,6 +9,7 @@ mod branch;
mod callbacks; mod callbacks;
mod commands; mod commands;
mod error; mod error;
mod fetch;
mod git; mod git;
mod merge; mod merge;
mod pull; mod pull;
@@ -24,6 +25,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
checkout, checkout,
commit, commit,
delete_branch, delete_branch,
fetch_all,
initialize, initialize,
log, log,
merge_branch, merge_branch,

View File

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

View File

@@ -12,6 +12,7 @@ import classNames from 'classnames';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { fallbackRequestName } from '../lib/fallbackRequestName';
import { showToast } from '../lib/toast';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import type { CheckboxProps } from './core/Checkbox'; import type { CheckboxProps } from './core/Checkbox';
@@ -46,8 +47,9 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
}; };
const handleCreateCommitAndPush = async () => { const handleCreateCommitAndPush = async () => {
await handleCreateCommit(); await commit.mutateAsync({ message });
await push.mutateAsync(); await push.mutateAsync();
showToast({ id: 'git-push-success', message: 'Pushed changes', color: 'success' });
onDone(); onDone();
}; };

View File

@@ -34,9 +34,16 @@ export function GitDropdown() {
function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
const workspace = useActiveWorkspace(); const workspace = useActiveWorkspace();
const [{ status, log }, { branch, deleteBranch, mergeBranch, push, pull, checkout }] = const [{ status, log }, { branch, deleteBranch, fetchAll, mergeBranch, push, pull, checkout }] =
useGit(syncDir); useGit(syncDir);
const localBranches = status.data?.localBranches ?? [];
const remoteBranches = status.data?.remoteBranches ?? [];
const remoteOnlyBranches = remoteBranches.filter(
(b) => !localBranches.includes(b.replace(/^origin\//, '')),
);
const currentBranch = status.data?.headRefShorthand ?? 'UNKNOWN';
if (workspace == null) { if (workspace == null) {
return null; return null;
} }
@@ -69,12 +76,12 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
showErrorToast('git-checkout-error', String(err)); showErrorToast('git-checkout-error', String(err));
} }
}, },
async onSuccess() { async onSuccess(branchName) {
showToast({ showToast({
id: 'git-checkout-success', id: 'git-checkout-success',
message: ( message: (
<> <>
Switched branch <InlineCode>{branch}</InlineCode> Switched branch <InlineCode>{branchName}</InlineCode>
</> </>
), ),
color: 'success', color: 'success',
@@ -124,7 +131,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
{ {
label: 'Merge Branch', label: 'Merge Branch',
leftSlot: <Icon icon="merge" />, leftSlot: <Icon icon="merge" />,
hidden: (status.data?.branches ?? []).length <= 1, hidden: localBranches.length <= 1,
async onSelect() { async onSelect() {
showDialog({ showDialog({
id: 'git-merge', id: 'git-merge',
@@ -132,15 +139,13 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
size: 'sm', size: 'sm',
description: ( description: (
<> <>
Select a branch to merge into <InlineCode>{status.data?.headRefShorthand}</InlineCode> Select a branch to merge into <InlineCode>{currentBranch}</InlineCode>
</> </>
), ),
render: ({ hide }) => ( render: ({ hide }) => (
<BranchSelectionDialog <BranchSelectionDialog
selectText="Merge" selectText="Merge"
branches={(status.data?.branches ?? []).filter( branches={localBranches.filter((b) => b !== currentBranch)}
(b) => b !== status.data?.headRefShorthand,
)}
onCancel={hide} onCancel={hide}
onSelect={async (branch) => { onSelect={async (branch) => {
await mergeBranch.mutateAsync( await mergeBranch.mutateAsync(
@@ -153,7 +158,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
message: ( message: (
<> <>
Merged <InlineCode>{branch}</InlineCode> into{' '} Merged <InlineCode>{branch}</InlineCode> into{' '}
<InlineCode>{status.data?.headRefShorthand}</InlineCode> <InlineCode>{currentBranch}</InlineCode>
</> </>
), ),
}); });
@@ -173,10 +178,9 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
{ {
label: 'Delete Branch', label: 'Delete Branch',
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
hidden: (status.data?.branches ?? []).length <= 1, hidden: localBranches.length <= 1,
color: 'danger', color: 'danger',
async onSelect() { async onSelect() {
const currentBranch = status.data?.headRefShorthand;
if (currentBranch == null) return; if (currentBranch == null) return;
const confirmed = await showConfirmDelete({ const confirmed = await showConfirmDelete({
@@ -256,9 +260,17 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
}); });
}, },
}, },
{ type: 'separator', label: 'Branches', hidden: (status.data?.branches ?? []).length < 1 }, { type: 'separator', label: 'Branches', hidden: localBranches.length < 1 },
...(status.data?.branches ?? []).map((branch) => { ...localBranches.map((branch) => {
const isCurrent = status.data?.headRefShorthand === branch; const isCurrent = currentBranch === branch;
return {
label: branch,
leftSlot: <Icon icon={isCurrent ? 'check' : 'empty'} />,
onSelect: isCurrent ? undefined : () => tryCheckout(branch, false),
};
}),
...remoteOnlyBranches.map((branch) => {
const isCurrent = currentBranch === branch;
return { return {
label: branch, label: branch,
leftSlot: <Icon icon={isCurrent ? 'check' : 'empty'} />, leftSlot: <Icon icon={isCurrent ? 'check' : 'empty'} />,
@@ -268,9 +280,9 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
]; ];
return ( return (
<Dropdown fullWidth items={items}> <Dropdown fullWidth items={items} onOpen={fetchAll.mutate}>
<GitMenuButton> <GitMenuButton>
{noRepo ? 'Configure Git' : <InlineCode>{status.data?.headRefShorthand}</InlineCode>} <InlineCode>{currentBranch}</InlineCode>
<Icon icon="git_branch" size="sm" /> <Icon icon="git_branch" size="sm" />
</GitMenuButton> </GitMenuButton>
</Dropdown> </Dropdown>

View File

@@ -71,6 +71,7 @@ export interface DropdownProps {
items: DropdownItem[]; items: DropdownItem[];
fullWidth?: boolean; fullWidth?: boolean;
hotKeyAction?: HotkeyAction; hotKeyAction?: HotkeyAction;
onOpen?: () => void;
} }
export interface DropdownRef { export interface DropdownRef {
@@ -89,7 +90,7 @@ export interface DropdownRef {
const openAtom = atom<string | null>(null); const openAtom = atom<string | null>(null);
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown( export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
{ children, items, hotKeyAction, fullWidth }: DropdownProps, { children, items, hotKeyAction, fullWidth, onOpen }: DropdownProps,
ref, ref,
) { ) {
const id = useRef(generateId()); const id = useRef(generateId());
@@ -116,13 +117,14 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o; const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o;
// Persist background color of button until we close the dropdown // Persist background color of button until we close the dropdown
if (newIsOpen) { if (newIsOpen) {
onOpen?.();
buttonRef.current!.style.backgroundColor = window buttonRef.current!.style.backgroundColor = window
.getComputedStyle(buttonRef.current!) .getComputedStyle(buttonRef.current!)
.getPropertyValue('background-color'); .getPropertyValue('background-color');
} }
return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state
}); });
}, []); }, [onOpen]);
// Because a different dropdown can cause ours to close, a useEffect([isOpen]) is the only method // Because a different dropdown can cause ours to close, a useEffect([isOpen]) is the only method
// we have of detecting the dropdown closed, to do cleanup. // we have of detecting the dropdown closed, to do cleanup.