Release 0.2.2 — browse submitted changelists, People & Roles, AI summaries
- History: GitHub-style split view of submitted changelists; preview file contents at their exact revision (code/image/3D/uasset) via `p4 print` - People & Roles modal: who works on the depot, activity indicators, group/role assignment (p4 users / groups / group -i) - AI commit summaries via OpenRouter (sparkle button); key/model in Settings - Commit-message draft persists until Submit (not cleared on local commit) - Resizable History file-list column (pointer-capture drag) - Fix UI-scale leaving a gap at the bottom (compensate zoom in .win height) - Author avatars on history rows; bump version to 0.2.2 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -951,7 +951,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "exbyte-depot"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "exbyte-depot"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@ -6,7 +6,7 @@ use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::Instant;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
|
||||
/// App handle stashed at startup so the low-level p4 helpers can emit a live
|
||||
/// command log to the frontend (a Perforce-style "Log" panel).
|
||||
@ -683,13 +683,17 @@ async fn open_in_vscode(state: State<'_, AppState>, depot: String) -> Result<(),
|
||||
.next()
|
||||
.and_then(|v| v.get("path").and_then(|p| p.as_str()).map(String::from))
|
||||
.ok_or_else(|| "Could not resolve the file path".to_string())?;
|
||||
if !std::path::Path::new(&local).exists() {
|
||||
return Err(format!("File is not on disk yet (not synced): {local}"));
|
||||
}
|
||||
let mut c = Command::new("cmd");
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
c.creation_flags(0x0800_0000);
|
||||
}
|
||||
c.arg("/C").arg(format!("code \"{local}\""));
|
||||
// pass args separately so Rust quotes only the path (no double-escaping through cmd)
|
||||
c.args(["/C", "code", &local]);
|
||||
c.spawn()
|
||||
.map_err(|e| format!("VS Code not found (is `code` in PATH?): {e}"))?;
|
||||
Ok(())
|
||||
@ -917,6 +921,26 @@ async fn read_depot(state: State<'_, AppState>, depot: String) -> Result<tauri::
|
||||
.map_err(|e| format!("Could not read file: {e}"))
|
||||
}
|
||||
|
||||
/// Read raw file bytes at an exact revision via `p4 print` (works for
|
||||
/// submitted / historical revisions that may not be synced locally).
|
||||
/// `spec` is a depot path optionally suffixed with a revision, e.g.
|
||||
/// `//depot/foo.cpp#7`. `-q` suppresses the leading file header line.
|
||||
#[tauri::command]
|
||||
async fn print_depot(state: State<'_, AppState>, spec: String) -> Result<tauri::ipc::Response, String> {
|
||||
let conn = current(&state)?;
|
||||
let out = p4_cmd(&conn, false)
|
||||
.args(["print", "-q", &spec])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to start p4: {e}"))?;
|
||||
if !out.status.success() && out.stdout.is_empty() {
|
||||
let e = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
log_p4(&["print", "-q", &spec], 0, false, Some(&e));
|
||||
return Err(if e.is_empty() { "p4 print failed".into() } else { e });
|
||||
}
|
||||
log_p4(&["print", "-q", &spec], out.stdout.len() as i64, true, None);
|
||||
Ok(tauri::ipc::Response::new(out.stdout))
|
||||
}
|
||||
|
||||
/// Switch the active workspace (client) without re-authenticating.
|
||||
#[tauri::command]
|
||||
async fn p4_switch_client(state: State<'_, AppState>, client: String) -> Result<Value, String> {
|
||||
@ -939,6 +963,113 @@ async fn p4_depots(state: State<'_, AppState>) -> Result<Vec<Value>, String> {
|
||||
run_json(&conn, &["depots"])
|
||||
}
|
||||
|
||||
/// All Perforce users (with last-access time → activity indicator).
|
||||
#[tauri::command]
|
||||
async fn p4_users(state: State<'_, AppState>) -> Result<Vec<Value>, String> {
|
||||
let conn = current(&state)?;
|
||||
run_json(&conn, &["users"])
|
||||
}
|
||||
|
||||
/// All groups on the server, or the groups a given user belongs to.
|
||||
#[tauri::command]
|
||||
async fn p4_groups(state: State<'_, AppState>, user: String) -> Result<Vec<Value>, String> {
|
||||
let conn = current(&state)?;
|
||||
if user.is_empty() {
|
||||
run_json(&conn, &["groups"])
|
||||
} else {
|
||||
run_json(&conn, &["groups", "-u", &user])
|
||||
}
|
||||
}
|
||||
|
||||
/// Add or remove a user from a group (role assignment). Requires admin/super.
|
||||
/// Edits the group spec's Users list and pipes it back via `group -i`.
|
||||
#[tauri::command]
|
||||
async fn p4_group_member(
|
||||
state: State<'_, AppState>,
|
||||
group: String,
|
||||
user: String,
|
||||
add: bool,
|
||||
) -> Result<String, String> {
|
||||
let conn = current(&state)?;
|
||||
let spec = run_text(&conn, &["group", "-o", &group])?;
|
||||
// collect existing Users: entries (indented under the "Users:" field)
|
||||
let mut users: Vec<String> = Vec::new();
|
||||
let mut in_users = false;
|
||||
let mut head = String::new();
|
||||
for line in spec.lines() {
|
||||
if line.starts_with("Users:") {
|
||||
in_users = true;
|
||||
continue;
|
||||
}
|
||||
if in_users {
|
||||
if line.starts_with('\t') || line.starts_with(' ') {
|
||||
let u = line.trim().to_string();
|
||||
if !u.is_empty() {
|
||||
users.push(u);
|
||||
}
|
||||
continue;
|
||||
} else if line.trim().is_empty() {
|
||||
continue;
|
||||
} else {
|
||||
in_users = false; // next field — stop collecting
|
||||
}
|
||||
}
|
||||
if !in_users {
|
||||
head.push_str(line);
|
||||
head.push('\n');
|
||||
}
|
||||
}
|
||||
users.retain(|u| !u.eq_ignore_ascii_case(&user));
|
||||
if add {
|
||||
users.push(user.clone());
|
||||
}
|
||||
// rebuild spec: head (everything except Users:) + fresh Users section
|
||||
let mut out = head.trim_end().to_string();
|
||||
out.push_str("\n\nUsers:\n");
|
||||
for u in &users {
|
||||
out.push('\t');
|
||||
out.push_str(u);
|
||||
out.push('\n');
|
||||
}
|
||||
let mut child = p4_cmd(&conn, false)
|
||||
.args(["group", "-i"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start p4: {e}"))?;
|
||||
if let Some(si) = child.stdin.as_mut() {
|
||||
si.write_all(out.as_bytes()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let res = child.wait_with_output().map_err(|e| e.to_string())?;
|
||||
if !res.status.success() {
|
||||
return Err(String::from_utf8_lossy(&res.stderr).trim().to_string());
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&res.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
/// Path to the (git-ignored) OpenRouter API key, stored in the app config dir.
|
||||
fn ai_key_path() -> Option<std::path::PathBuf> {
|
||||
let app = APP.get()?;
|
||||
let dir = app.path().app_config_dir().ok()?;
|
||||
Some(dir.join("openrouter.key"))
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn read_ai_key() -> Result<String, String> {
|
||||
Ok(ai_key_path()
|
||||
.and_then(|p| std::fs::read_to_string(p).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn save_ai_key(key: String) -> Result<(), String> {
|
||||
let p = ai_key_path().ok_or("No config dir")?;
|
||||
if let Some(dir) = p.parent() {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
}
|
||||
std::fs::write(&p, key.trim()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let mut builder = tauri::Builder::default()
|
||||
@ -989,8 +1120,14 @@ pub fn run() {
|
||||
p4_describe,
|
||||
read_file,
|
||||
read_depot,
|
||||
print_depot,
|
||||
p4_switch_client,
|
||||
p4_depots,
|
||||
p4_users,
|
||||
p4_groups,
|
||||
p4_group_member,
|
||||
read_ai_key,
|
||||
save_ai_key,
|
||||
p4_dirs,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "exbyte-depot",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"identifier": "com.bonchellon.exbyte-depot",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
Reference in New Issue
Block a user