[READ-ONLY] Mirror of https://github.com/SantaClaas/gigit. git client
Something went wrong. Try again.
Rust
at ref-filter
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224//! The list of repositories opened recently.//!//! An actor of its own, so the list and the file it lives in have exactly one//! owner and need no lock. It is deliberately not part of the workspace actor://! the workspace owns *open* repositories, this owns a memory of ones that were//! open once, and the two have different lifetimes.
use std::future::Future;use std::path::{Path, PathBuf};use std::time::{SystemTime, UNIX_EPOCH};
use gigit_git::RepositorySummary;use serde::{Deserialize, Serialize};use tokio::sync::{mpsc, oneshot};
use crate::error::{Error, Result};use crate::workspace::RepositoryId;
const INBOX_CAPACITY: usize = 32;
/// How many repositories to remember. Long enough to cover what anyone is/// actually moving between, short enough that the list stays scannable.const CAPACITY: usize = 20;
/// A repository that was open at some point.#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]pub struct RecentRepository { pub id: RepositoryId, /// The working tree, or the git directory for a bare repository — whichever /// a person would recognise as "where the repository is". pub path: PathBuf, /// The last path segment, which is what people call a repository. pub name: String, /// Seconds since the epoch. pub last_opened: i64, /// Whether the path is still there. /// /// Recomputed on every read and never trusted from disk: a repository can /// be moved or deleted while the app is not looking. It is written out with /// the rest only because keeping it out would mean a second type for no /// real gain. #[serde(default)] pub exists: bool,}
impl RecentRepository { pub(crate) fn new(id: RepositoryId, summary: &RepositorySummary) -> Self { let path = summary .workdir .clone() .unwrap_or_else(|| summary.git_dir.clone());
Self { name: name_of(&path), id, path, last_opened: now(), exists: true, } }}
fn name_of(path: &Path) -> String { path.file_name() .map(|name| name.to_string_lossy().into_owned()) // A path ending in a separator, or the filesystem root. Rare, but // showing an empty name would be worse than showing the whole path. .unwrap_or_else(|| path.to_string_lossy().into_owned())}
fn now() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|elapsed| i64::try_from(elapsed.as_secs()).unwrap_or(i64::MAX)) .unwrap_or_default()}
enum Message { List { respond: oneshot::Sender<Vec<RecentRepository>>, }, Record { entry: Box<RecentRepository>, respond: oneshot::Sender<()>, }, Forget { id: RepositoryId, respond: oneshot::Sender<bool>, },}
/// A cheap, clonable way to talk to the recents actor.#[derive(Clone, Debug)]pub struct RecentsHandle { outbox: mpsc::Sender<Message>,}
impl RecentsHandle { /// Build the handle and the actor's future. /// /// As with the workspace, the future is returned rather than spawned so the /// caller picks the runtime — Tauri's in the app, the test runtime in tests. pub fn new(file: PathBuf) -> (Self, impl Future<Output = ()> + Send + 'static) { let (outbox, inbox) = mpsc::channel(INBOX_CAPACITY);
(Self { outbox }, run(file, inbox)) }
/// Most recently opened first. pub async fn list(&self) -> Result<Vec<RecentRepository>> { self.request(|respond| Message::List { respond }).await }
/// Remember a repository, moving it to the front if it is already known. pub async fn record(&self, entry: RecentRepository) -> Result<()> { self.request(|respond| Message::Record { entry: Box::new(entry), respond, }) .await }
/// Drop a repository from the list. Returns whether it was there. pub async fn forget(&self, id: RepositoryId) -> Result<bool> { self.request(|respond| Message::Forget { id, respond }) .await }
async fn request<T>(&self, build: impl FnOnce(oneshot::Sender<T>) -> Message) -> Result<T> { let (respond, answer) = oneshot::channel();
self.outbox .send(build(respond)) .await .map_err(|_| Error::WorkerStopped)?;
answer.await.map_err(|_| Error::WorkerStopped) }}
async fn run(file: PathBuf, mut inbox: mpsc::Receiver<Message>) { let mut entries = load(&file).await;
while let Some(message) = inbox.recv().await { match message { Message::List { respond } => { for entry in &mut entries { entry.exists = entry.path.exists(); }
let _ = respond.send(entries.clone()); } Message::Record { entry, respond } => { // Same repository opened again: it moves to the front rather // than appearing twice. entries.retain(|known| known.id != entry.id); entries.insert(0, *entry); entries.truncate(CAPACITY);
save(&file, &entries).await; let _ = respond.send(()); } Message::Forget { id, respond } => { let before = entries.len(); entries.retain(|known| known.id != id); let forgotten = entries.len() != before;
if forgotten { save(&file, &entries).await; }
let _ = respond.send(forgotten); } } }}
/// Read the list, treating anything unreadable as an empty list.////// A missing file is the normal first run. A corrupt one is not worth refusing/// to start over — the list is a convenience, and the worst case is that a user/// has to open a repository the long way once.async fn load(file: &Path) -> Vec<RecentRepository> { let Ok(contents) = tokio::fs::read_to_string(file).await else { return Vec::new(); };
match serde_json::from_str::<Vec<RecentRepository>>(&contents) { Ok(entries) => entries, Err(error) => { eprintln!( "ignoring unreadable recent repositories at {}: {error}", file.display() ); Vec::new() } }}
/// Write the list back.////// Failures are reported but not propagated: not being able to remember a/// repository is a far smaller problem than refusing to open one because of it.async fn save(file: &Path, entries: &[RecentRepository]) { if let Some(directory) = file.parent() { if let Err(error) = tokio::fs::create_dir_all(directory).await { eprintln!("could not create {}: {error}", directory.display()); return; } }
let Ok(contents) = serde_json::to_string_pretty(entries) else { return; };
if let Err(error) = tokio::fs::write(file, contents).await { eprintln!( "could not save recent repositories to {}: {error}", file.display() ); }}