Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/git-fs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ rand = "0.9.2"
rustc-hash = "2.1.1"
scc = "3.4.16"
base64 = "0.22"
nix = "0.29.0"
130 changes: 130 additions & 0 deletions crates/git-fs/src/commit_worker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Background worker for processing commit requests.
//!
//! This module handles asynchronous commits to the remote repository via the Mesa API.

use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use mesa_dev::Mesa;
use mesa_dev::models::{Author, CommitEncoding, CommitFile, CommitFileAction, CreateCommitRequest};
use tokio::sync::mpsc;
use tracing::{error, info};

/// A request to create a commit, sent to the background worker.
pub enum CommitRequest {
/// Create a new file with the given content.
Create {
/// Path to the file to create.
path: String,
/// Content of the file.
content: Vec<u8>,
},
/// Update an existing file with new content.
Update {
/// Path to the file to update.
path: String,
/// New content of the file.
content: Vec<u8>,
},
/// Delete a file.
Delete {
/// Path to the file to delete.
path: String,
},
}

/// Configuration for the commit worker.
pub struct CommitWorkerConfig {
/// Mesa API client.
pub mesa: Mesa,
/// Repository organization/owner.
pub org: String,
/// Repository name.
pub repo: String,
/// Branch to commit to.
pub branch: String,
/// Author information for commits.
pub author: Author,
}

/// Spawns a background task that processes commit requests from the given receiver.
///
/// The task will run until the channel is closed (all senders are dropped).
pub fn spawn_commit_worker(
rt: &tokio::runtime::Runtime,
config: CommitWorkerConfig,
mut commit_rx: mpsc::UnboundedReceiver<CommitRequest>,
) {
let mesa = config.mesa;
let org = config.org;
let repo = config.repo;
let branch = config.branch;
let author = config.author;
rt.spawn(async move {
while let Some(request) = commit_rx.recv().await {
let (message, files) = match request {
CommitRequest::Create { path, content } => {
// Use "." for empty files to work around Mesa API bug with empty content
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium

This "workaround" replaces every empty file with a single '.' byte before base64 encoding (lines 65‑78 and 82‑95). That means any zero-length file we create or truncate is committed with one byte of content, so the file is no longer empty when it reaches Mesa/GitHub. Unless the server really does a special-case replacement on Lg==, this corrupts empty files.

If the Mesa API cannot accept an empty string today, we need a server-side fix or a different protocol knob (e.g., send content: Some(String::new()), or add an explicit flag) rather than mutating user data on the client.

Fix in Cursor • Fix in Claude

Prompt for Agent
Task: Address review feedback left on GitHub.
Repository: mesa-dot-dev/git-fs#3
File: crates/git-fs/src/commit_worker.rs#L65
Action: Open this file location in your editor, inspect the highlighted code, and resolve the issue described below.

Feedback:
This "workaround" replaces every empty file with a single `'.'` byte before base64 encoding (lines 65‑78 and 82‑95). That means any zero-length file we create or truncate is committed with one byte of content, so the file is no longer empty when it reaches Mesa/GitHub. Unless the server really does a special-case replacement on `Lg==`, this corrupts empty files.

If the Mesa API cannot accept an empty string today, we need a server-side fix or a different protocol knob (e.g., send `content: Some(String::new())`, or add an explicit flag) rather than mutating user data on the client.

let content_bytes = if content.is_empty() {
b".".as_slice()
} else {
&content
};
(
format!("Create {path}"),
vec![CommitFile {
action: CommitFileAction::Upsert,
path,
encoding: CommitEncoding::Base64,
content: Some(BASE64.encode(content_bytes)),
}],
)
}
CommitRequest::Update { path, content } => {
// Use "." for empty files to work around Mesa API bug with empty content
let content_bytes = if content.is_empty() {
b".".as_slice()
} else {
&content
};
(
format!("Update {path}"),
vec![CommitFile {
action: CommitFileAction::Upsert,
path,
encoding: CommitEncoding::Base64,
content: Some(BASE64.encode(content_bytes)),
}],
)
}
CommitRequest::Delete { path } => (
format!("Delete {path}"),
vec![CommitFile {
action: CommitFileAction::Delete,
path,
encoding: CommitEncoding::Base64,
content: None,
}],
),
};

let create_commit_request = CreateCommitRequest {
branch: branch.clone(),
message: message.clone(),
author: author.clone(),
files,
base_sha: None,
};

info!("about to commit the following: {:?}", create_commit_request);

let result = mesa
.commits(&org, &repo)
.create(&create_commit_request)
.await;

match result {
Ok(_) => info!(message = %message, "commit pushed"),
Err(e) => error!(message = %message, error = %e, "commit failed"),
}
}
});
}
61 changes: 58 additions & 3 deletions crates/git-fs/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//! Mount a GitHub repository as a filesystem, without ever cloning.
use std::path::PathBuf;
use std::process::Command;

use clap::Parser;
use fuser::MountOption;
use tracing::error;
use tracing_subscriber::{EnvFilter, fmt};

mod commit_worker;
mod domain;
mod mesafuse;
mod ssfs;
Expand All @@ -32,6 +34,43 @@ struct Args {
/// repository's default branch.
#[arg(long)]
r#ref: Option<String>,

/// Enable write mode. When enabled, file modifications are immediately committed to the
/// remote repository. Requires git config user.name and user.email to be set.
#[arg(long)]
writable: bool,
}

/// Author information for commits.
#[derive(Debug, Clone)]
pub struct Author {
/// Author name (from git config user.name).
pub name: String,
/// Author email (from git config user.email).
pub email: String,
}

/// Read a git config value.
fn git_config_get(key: &str) -> Option<String> {
let output = Command::new("git")
.args(["config", "--get", key])
.output()
.ok()?;

output
.status
.success()
.then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())
}

/// Read author from git config, returning an error message if not found.
fn get_author_from_git_config() -> Result<Author, String> {
let name = git_config_get("user.name").ok_or(
"git config user.name is not set. Please run: git config --global user.name \"Your Name\"",
)?;
let email = git_config_get("user.email")
.ok_or("git config user.email is not set. Please run: git config --global user.email \"your@email.com\"")?;
Ok(Author { name, email })
}

fn main() {
Expand All @@ -41,13 +80,29 @@ fn main() {
.with_span_events(fmt::format::FmtSpan::EXIT)
.init();

let options = vec![
MountOption::RO,
// Read author from git config if writable mode is enabled
let author = if args.writable {
match get_author_from_git_config() {
Ok(author) => Some(author),
Err(msg) => {
error!("{msg}");
std::process::exit(1);
}
}
} else {
None
};

let mut options = vec![
MountOption::AutoUnmount,
MountOption::FSName("mesafs".to_owned()),
];

let mesa_fs = MesaFS::new(&args.mesa_api_key, args.repo, args.r#ref.as_deref());
if !args.writable {
options.push(MountOption::RO);
}

let mesa_fs = MesaFS::new(&args.mesa_api_key, args.repo, args.r#ref.as_deref(), author);
if let Err(e) = fuser::mount2(mesa_fs, &args.mount_point, &options) {
error!("Failed to mount filesystem: {e}");
}
Expand Down
Loading