Skip to content

Commit fbce4e6

Browse files
authored
Error handling overhaul and adding "Open in..." button (#28)
* feat(errors): interpret git errors with useful advice/options to solve the issues * feat: improve interpreted Git error handling * fix(git): ensure that --force-with-lease is ran after a rebase * feat: add repository open-in menu * ci(releases): notify website metadata updates * fix: enable about website link
1 parent bbe9a86 commit fbce4e6

38 files changed

Lines changed: 2049 additions & 193 deletions

‎.github/workflows/build-bundles.yml‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,3 +1068,53 @@ jobs:
10681068
--header "x-ci-signature: sha256=$SIGNATURE" \
10691069
--data "$PAYLOAD" \
10701070
"${WORKER_URL%/}/webhook"
1071+
1072+
notify-website-release-metadata:
1073+
name: Notify website release metadata
1074+
if: ${{ github.event_name == 'release' }}
1075+
needs:
1076+
- linux-bundles
1077+
- windows-bundles
1078+
- update-updater-json
1079+
- windows-msix
1080+
- macos-bundles
1081+
- notify-r2-release-sync
1082+
runs-on: ubuntu-22.04
1083+
1084+
steps:
1085+
- name: Dispatch website metadata update
1086+
env:
1087+
DISPATCH_TOKEN: ${{ secrets.GITMUN_WEBSITE_DISPATCH_TOKEN }}
1088+
GITHUB_API_URL: ${{ github.api_url }}
1089+
RELEASE_TAG: ${{ github.event.release.tag_name }}
1090+
RELEASE_URL: ${{ github.event.release.html_url }}
1091+
shell: bash
1092+
run: |
1093+
set -euo pipefail
1094+
1095+
if [ -z "${DISPATCH_TOKEN:-}" ]; then
1096+
echo "GITMUN_WEBSITE_DISPATCH_TOKEN is not configured." >&2
1097+
exit 1
1098+
fi
1099+
1100+
payload="$(
1101+
jq -cn \
1102+
--arg event_type "gitmun-release-assets-ready" \
1103+
--arg tag "$RELEASE_TAG" \
1104+
--arg release_url "$RELEASE_URL" \
1105+
'{
1106+
event_type: $event_type,
1107+
client_payload: {
1108+
tag: $tag,
1109+
release_url: $release_url
1110+
}
1111+
}'
1112+
)"
1113+
1114+
curl --fail-with-body \
1115+
--request POST \
1116+
--header "Accept: application/vnd.github+json" \
1117+
--header "Authorization: Bearer ${DISPATCH_TOKEN}" \
1118+
--header "X-GitHub-Api-Version: 2022-11-28" \
1119+
--data "$payload" \
1120+
"${GITHUB_API_URL%/}/repos/cst8t/gitmun-org-website/dispatches"

‎src-tauri/capabilities/default.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
{
2626
"identifier": "opener:allow-open-path",
2727
"allow": [
28-
{ "path": "$APPCONFIG" }
28+
{ "path": "$APPCONFIG" },
29+
{ "path": "$HOME/**" },
30+
{ "path": "$DESKTOP/**" },
31+
{ "path": "$DOCUMENT/**" },
32+
{ "path": "$DOWNLOAD/**" }
2933
]
3034
}
3135
]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"default":{"identifier":"default","description":"Default capability for invoking app commands","local":true,"windows":["main","settings","clone-repository","result-log","about","attributions"],"permissions":["core:default","core:webview:allow-create-webview-window","core:window:allow-create","core:window:allow-set-focus","core:window:allow-set-title","core:window:allow-show","core:window:allow-close","dialog:allow-open","dialog:allow-ask","os:default","core:menu:allow-popup","core:menu:allow-new","core:menu:allow-append","core:menu:allow-remove","core:window:allow-cursor-position","core:window:allow-outer-position","updater:default","shell:allow-open",{"identifier":"opener:allow-open-path","allow":[{"path":"$APPCONFIG"}]}]}}
1+
{"default":{"identifier":"default","description":"Default capability for invoking app commands","local":true,"windows":["main","settings","clone-repository","result-log","about","attributions"],"permissions":["core:default","core:webview:allow-create-webview-window","core:window:allow-create","core:window:allow-set-focus","core:window:allow-set-title","core:window:allow-show","core:window:allow-close","dialog:allow-open","dialog:allow-ask","os:default","core:menu:allow-popup","core:menu:allow-new","core:menu:allow-append","core:menu:allow-remove","core:window:allow-cursor-position","core:window:allow-outer-position","updater:default","shell:allow-open",{"identifier":"opener:allow-open-path","allow":[{"path":"$APPCONFIG"},{"path":"$HOME/**"},{"path":"$DESKTOP/**"},{"path":"$DOCUMENT/**"},{"path":"$DOWNLOAD/**"}]}]}}

‎src-tauri/src/commands/repo.rs‎

Lines changed: 276 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,282 @@ use crate::git::types::{
77
SubmoduleActionRequest,
88
};
99
use crate::{AppState, CloneCancelFlag, configure_command};
10+
use serde::{Deserialize, Serialize};
1011
use std::io::Read;
11-
use std::process::Stdio;
12+
use std::path::{Path, PathBuf};
13+
use std::process::{Command, Stdio};
1214
use std::sync::atomic::Ordering;
1315
use tauri::Manager;
16+
use tauri_plugin_opener::OpenerExt;
17+
18+
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
19+
#[serde(rename_all = "camelCase")]
20+
pub enum RepoOpenLocationKind {
21+
FileExplorer,
22+
Terminal,
23+
GitBash,
24+
}
25+
26+
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
27+
#[serde(rename_all = "camelCase")]
28+
pub struct RepoOpenLocation {
29+
kind: RepoOpenLocationKind,
30+
label: String,
31+
fallback_label: String,
32+
icon_data_url: Option<String>,
33+
}
34+
35+
#[tauri::command]
36+
pub fn get_repo_open_locations() -> Vec<RepoOpenLocation> {
37+
let locations = vec![
38+
RepoOpenLocation {
39+
kind: RepoOpenLocationKind::FileExplorer,
40+
label: default_file_manager_label().to_string(),
41+
fallback_label: default_file_manager_label().to_string(),
42+
icon_data_url: None,
43+
},
44+
RepoOpenLocation {
45+
kind: RepoOpenLocationKind::Terminal,
46+
label: default_terminal_label().to_string(),
47+
fallback_label: "Terminal".to_string(),
48+
icon_data_url: None,
49+
},
50+
];
51+
52+
#[cfg(target_os = "windows")]
53+
let locations = {
54+
let mut locations = locations;
55+
if crate::resolve_system_git_bash_exe().is_some() {
56+
locations.push(RepoOpenLocation {
57+
kind: RepoOpenLocationKind::GitBash,
58+
label: "Git Bash".to_string(),
59+
fallback_label: "Git Bash".to_string(),
60+
icon_data_url: None,
61+
});
62+
}
63+
locations
64+
};
65+
66+
locations
67+
}
68+
69+
#[tauri::command]
70+
pub fn open_repo_location(
71+
repo_path: String,
72+
kind: RepoOpenLocationKind,
73+
app: tauri::AppHandle,
74+
) -> Result<OperationResult, String> {
75+
let path = validate_repo_open_path(&repo_path)?;
76+
77+
match kind {
78+
RepoOpenLocationKind::FileExplorer => {
79+
app.opener()
80+
.open_path(path.to_string_lossy().to_string(), None::<&str>)
81+
.map_err(|e| format!("Failed to open file manager: {e}"))?;
82+
Ok(repo_open_result(
83+
format!("Opened repository in {}", default_file_manager_label()),
84+
path,
85+
))
86+
}
87+
RepoOpenLocationKind::Terminal => {
88+
open_terminal_at(&path)?;
89+
Ok(repo_open_result(
90+
"Opened repository in Terminal".to_string(),
91+
path,
92+
))
93+
}
94+
RepoOpenLocationKind::GitBash => {
95+
open_git_bash_at(&path)?;
96+
Ok(repo_open_result(
97+
"Opened repository in Git Bash".to_string(),
98+
path,
99+
))
100+
}
101+
}
102+
}
103+
104+
fn validate_repo_open_path(repo_path: &str) -> Result<PathBuf, String> {
105+
let trimmed = repo_path.trim();
106+
if trimmed.is_empty() {
107+
return Err("Repository path cannot be empty".to_string());
108+
}
109+
110+
let path = PathBuf::from(trimmed);
111+
if !path.is_dir() {
112+
return Err("Repository path must be an existing directory".to_string());
113+
}
114+
Ok(path)
115+
}
116+
117+
fn repo_open_result(message: String, path: PathBuf) -> OperationResult {
118+
OperationResult {
119+
message,
120+
output: None,
121+
repo_path: Some(path.to_string_lossy().to_string()),
122+
backend_used: "git-cli".to_string(),
123+
interpreted_error: None,
124+
}
125+
}
126+
127+
fn default_file_manager_label() -> &'static str {
128+
#[cfg(target_os = "windows")]
129+
{
130+
"File Explorer"
131+
}
132+
#[cfg(target_os = "macos")]
133+
{
134+
"Finder"
135+
}
136+
#[cfg(target_os = "linux")]
137+
{
138+
"File Manager"
139+
}
140+
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
141+
{
142+
"File Manager"
143+
}
144+
}
145+
146+
fn default_terminal_label() -> &'static str {
147+
"Terminal"
148+
}
149+
150+
fn open_terminal_at(path: &Path) -> Result<(), String> {
151+
#[cfg(target_os = "windows")]
152+
{
153+
return open_terminal_at_windows(path);
154+
}
155+
156+
#[cfg(target_os = "macos")]
157+
{
158+
return open_terminal_at_macos(path);
159+
}
160+
161+
#[cfg(target_os = "linux")]
162+
{
163+
return open_terminal_at_linux(path);
164+
}
165+
166+
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
167+
{
168+
let _ = path;
169+
Err("Opening a terminal is not supported on this platform".to_string())
170+
}
171+
}
172+
173+
fn open_git_bash_at(path: &Path) -> Result<(), String> {
174+
#[cfg(target_os = "windows")]
175+
{
176+
return open_git_bash_at_windows(path);
177+
}
178+
179+
#[cfg(not(target_os = "windows"))]
180+
{
181+
let _ = path;
182+
Err("Git Bash is only available on Windows".to_string())
183+
}
184+
}
185+
186+
#[cfg(target_os = "windows")]
187+
fn open_git_bash_at_windows(path: &Path) -> Result<(), String> {
188+
let git_bash = crate::resolve_system_git_bash_exe()
189+
.ok_or_else(|| "Git Bash from Git for Windows was not found".to_string())?;
190+
Command::new(git_bash)
191+
.arg(format!("--cd={}", path.display()))
192+
.spawn()
193+
.map(|_| ())
194+
.map_err(|e| format!("Failed to open Git Bash: {e}"))
195+
}
196+
197+
#[cfg(target_os = "windows")]
198+
fn open_terminal_at_windows(path: &Path) -> Result<(), String> {
199+
let mut wt = Command::new("wt.exe");
200+
wt.arg("-d").arg(path);
201+
if wt.spawn().is_ok() {
202+
return Ok(());
203+
}
204+
205+
Command::new("cmd.exe")
206+
.arg("/C")
207+
.arg("start")
208+
.arg("")
209+
.arg("/D")
210+
.arg(path)
211+
.arg("cmd.exe")
212+
.spawn()
213+
.map(|_| ())
214+
.map_err(|e| format!("Failed to open terminal: {e}"))
215+
}
216+
217+
#[cfg(target_os = "macos")]
218+
fn open_terminal_at_macos(path: &Path) -> Result<(), String> {
219+
Command::new("open")
220+
.arg("-a")
221+
.arg("Terminal")
222+
.arg(path)
223+
.spawn()
224+
.map(|_| ())
225+
.map_err(|e| format!("Failed to open Terminal: {e}"))
226+
}
227+
228+
#[cfg(target_os = "linux")]
229+
fn open_terminal_at_linux(path: &Path) -> Result<(), String> {
230+
let mut errors = Vec::new();
231+
232+
if let Some(terminal) = std::env::var_os("TERMINAL").and_then(|value| value.into_string().ok())
233+
{
234+
if !terminal.trim().is_empty() && spawn_terminal_command(&terminal, path, &mut errors) {
235+
return Ok(());
236+
}
237+
}
238+
239+
for command in [
240+
"x-terminal-emulator",
241+
"gnome-terminal",
242+
"kgx",
243+
"konsole",
244+
"xfce4-terminal",
245+
"mate-terminal",
246+
"lxterminal",
247+
"alacritty",
248+
"kitty",
249+
"wezterm",
250+
"foot",
251+
"xterm",
252+
] {
253+
if spawn_terminal_command(command, path, &mut errors) {
254+
return Ok(());
255+
}
256+
}
257+
258+
Err(if errors.is_empty() {
259+
"No supported terminal emulator was found".to_string()
260+
} else {
261+
format!(
262+
"No supported terminal emulator was found ({})",
263+
errors.join("; ")
264+
)
265+
})
266+
}
267+
268+
#[cfg(target_os = "linux")]
269+
fn spawn_terminal_command(command_spec: &str, path: &Path, errors: &mut Vec<String>) -> bool {
270+
let mut parts = command_spec.split_whitespace();
271+
let Some(command_name) = parts.next() else {
272+
return false;
273+
};
274+
275+
let mut command = Command::new(command_name);
276+
command.args(parts).current_dir(path);
277+
278+
match command.spawn() {
279+
Ok(_) => true,
280+
Err(error) => {
281+
errors.push(format!("{command_name}: {error}"));
282+
false
283+
}
284+
}
285+
}
14286

15287
#[tauri::command]
16288
pub async fn get_commit_markers(
@@ -83,6 +355,7 @@ pub fn init_repo(repo_path: String) -> Result<OperationResult, String> {
83355
output: None,
84356
repo_path: Some(path.to_string_lossy().to_string()),
85357
backend_used: "git-cli".to_string(),
358+
interpreted_error: None,
86359
});
87360
}
88361

@@ -117,6 +390,7 @@ pub fn init_repo(repo_path: String) -> Result<OperationResult, String> {
117390
output: None,
118391
repo_path: Some(path.to_string_lossy().to_string()),
119392
backend_used: "git-cli".to_string(),
393+
interpreted_error: None,
120394
})
121395
}
122396

@@ -232,6 +506,7 @@ pub async fn clone_repo(
232506
output: None,
233507
repo_path: Some(final_dest_str),
234508
backend_used: "git-cli".to_string(),
509+
interpreted_error: None,
235510
})
236511
}
237512

0 commit comments

Comments
 (0)