Skip to content

Support for but_ctx::ProjectHandle#12647

Draft
Byron wants to merge 3 commits intogitbutlerapp:masterfrom
Byron:next4
Draft

Support for but_ctx::ProjectHandle#12647
Byron wants to merge 3 commits intogitbutlerapp:masterfrom
Byron:next4

Conversation

@Byron
Copy link
Collaborator

@Byron Byron commented Mar 2, 2026

ProjectHandle is the evolution of ProjectId as a URL-safe way to point to a project and create a Context from it.
Let's have this ready and available in but-api.

This PR is part of the data-consistency plan.

Tasks

  • review
  • but-api-macro tests
  • but-api support to allow creating contexts from LegacyProjectId and ProjectHandle.
  • Use ProjectHandleOrLegacyProjectId everywhere and let add_project return a ProjectHandle equivalent
  • update plan to indicate how to migrate to it
Codex Plan

Implement ProjectHandle + Context::new_from_project_handle

Summary

Implement a fully lossless, URL-safe ProjectHandle in but-ctx that supports two wire forms in one type:

  • Readable form for non-release channels (debuggability)
  • Compact form for release/stable channel (short IDs)

Add conversion in both directions (Path <-> ProjectHandle) and a new Context constructor from ProjectHandle, mirroring the existing legacy ProjectId-based flow.

No AGENTS skill applies directly here (skill-creator / skill-installer are unrelated), so this will be implemented directly in-crate.

Public API changes

  1. Add new module:
  • crates/but-ctx/src/project_handle.rs
  • Export via pub mod project_handle; in crates/but-ctx/src/lib.rs
  1. Add representation enum (as requested):
  • but_ctx::project_handle::Representation
  • Variants:
    • Readable
    • Compact
  • Add helpers:
    • Representation::for_app_channel(channel: but_path::AppChannel) -> Self
    • Representation::for_current_channel() -> Self
  • Mapping:
    • Release => Compact
    • Nightly / Dev => Readable
  1. Implement ProjectHandle behavior in lib.rs (or module impl block):
  • Keep the type as pub struct ProjectHandle(String);
  • Add methods:
    • ProjectHandle::from_path(path: impl AsRef<Path>, representation: Representation) -> anyhow::Result<Self>
    • ProjectHandle::from_path_for_current_channel(path: impl AsRef<Path>) -> anyhow::Result<Self>
    • ProjectHandle::as_str(&self) -> &str
  • Add path conversion:
    • impl TryFrom<&ProjectHandle> for PathBuf
    • impl TryFrom<ProjectHandle> for PathBuf
  • Add string interop:
    • impl std::fmt::Display for ProjectHandle
    • impl std::str::FromStr for ProjectHandle (validate format on parse)
  1. Add constructor analogous to new_from_legacy_project_id:
  • Context::new_from_project_handle(project_handle: ProjectHandle) -> anyhow::Result<Self>
  • Internals:
    • decode handle -> PathBuf (gitdir)
    • call Context::new(gitdir, but_path::app_config_dir()?, but_path::app_cache_dir().ok())
  1. Add conversion impls analogous to legacy ID:
  • impl TryFrom<ProjectHandle> for Context
  • impl TryFrom<ProjectHandle> for ThreadSafeContext

Encoding/decoding spec (decision-complete)

Use self-describing prefixes to distinguish formats during decode:

  • Readable: r.
  • Compact: c.

Path-to-bytes:

  • Use gix::path::os_str_into_bstr(path.as_os_str()) for lossless path extraction.

Readable encoding (r.):

  • Byte-wise percent-encode everything except RFC3986 unreserved bytes: A-Z a-z 0-9 - . _ ~
  • Escape format: %HH uppercase hex
  • This keeps ASCII paths readable while remaining URL-safe and reversible.

Compact encoding (c.):

  • URL-safe base64 without padding over raw path bytes (URL_SAFE_NO_PAD).

Decode rules:

  • If prefix is r.: strict percent-decoder to raw bytes.
  • If prefix is c.: base64url decode to raw bytes.
  • Unknown prefix => error.
  • Decode bytes back to PathBuf with gix::path::try_from_bstring(...).

Dependency changes

In crates/but-ctx/Cargo.toml:

  • Add base64 = "0.22.1" (for compact encoding/decoding)

No additional dependency needed for readable form (manual percent codec).

Tests and scenarios

Add unit tests in project_handle.rs (or lib.rs test module) covering:

  1. Readable round-trip
  • UTF-8 path with spaces/special chars
  • Path -> ProjectHandle(Readabe) -> Path equality
  1. Compact round-trip
  • Same path, compact mode
  • exact round-trip equality
  1. Prefix-driven decode
  • Handles with r. and c. both decode correctly
  1. Strict malformed input handling
  • Unknown prefix errors
  • Bad % escape errors in readable
  • Invalid base64 errors in compact
  1. Channel default mapping
  • for_app_channel(Release) => Compact
  • Nightly/Dev => Readable
  1. Non-UTF8 path bytes on Unix (#[cfg(unix)])
  • Construct path from raw bytes
  • round-trip in both representations
  1. Context constructor smoke test
  • init temp repo/gitdir
  • create handle from gitdir
  • Context::new_from_project_handle(...) returns context with expected gitdir

Validation commands:

  • cargo test -p but-ctx
  • cargo check -p but-ctx --all-features

Assumptions and defaults

  • “Stable channel” means but_path::AppChannel::Release.
  • Self-describing prefixes (r. / c.) are the canonical format from first implementation.
  • Unknown/unprefixed handles are treated as invalid (no legacy fallback needed because ProjectHandle was not previously implemented).
  • Context::new_from_project_handle accepts a handle pointing at gitdir (same semantic as existing Context::new).

Copilot AI review requested due to automatic review settings March 2, 2026 07:53
@vercel
Copy link

vercel bot commented Mar 2, 2026

@chatgpt-codex-connector[bot] is attempting to deploy a commit to the GitButler Team on Vercel.

A member of the Team first needs to authorize it.

@Byron Byron marked this pull request as draft March 2, 2026 07:53
Copy link
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee2e76bf7d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements ProjectHandle in the but-ctx crate, which is the evolution of ProjectId as a self-describing, URL-safe way to identify a project by its on-disk path. It replaces the legacy UUID-based approach and is part of a broader data-consistency plan.

Changes:

  • Adds a new project_handle module with Representation enum and encoding/decoding functions (percent-encoding for Readable, base64url for Compact), with comprehensive unit tests.
  • Completes the ProjectHandle struct implementation in lib.rs: adds from_path, from_path_for_current_channel, as_str, Display, FromStr, TryFrom<&ProjectHandle> and TryFrom<ProjectHandle> for PathBuf, plus TryFrom<ProjectHandle> for both Context and ThreadSafeContext, and Context::new_from_project_handle.
  • Adds base64 = "0.22.1" as a direct (per-crate) dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.

File Description
crates/but-ctx/src/project_handle.rs New module implementing the encoding/decoding logic for both Readable and Compact representations, plus comprehensive tests
crates/but-ctx/src/lib.rs Completes the ProjectHandle struct with all required conversions and Context::new_from_project_handle constructor
crates/but-ctx/Cargo.toml Adds base64 = "0.22.1" as a direct dependency for compact encoding
Cargo.lock Lock file update reflecting the new base64 dependency in but-ctx

Add first-class `ProjectHandle` support to `but-ctx`, including:
- bidirectional `Path <-> ProjectHandle` conversion
- `Context::new_from_project_handle(...)`
- `TryFrom<ProjectHandle>` for `Context` and `ThreadSafeContext`
- comprehensive round-trip and malformed-input tests

Benefits vs `LegacyProjectId`:
- self-describing: handle directly encodes project path; no `projects.json` lookup
- lossless: preserves raw filesystem bytes (including non-UTF8 Unix paths)
- URL-safe by design for transport/deeplink/API usage
- channel-aware UX: compact IDs on release, readable IDs on dev/nightly
- reduces coupling to legacy project metadata and global ID indirection

Co-authored-by: Sebastian Thiel <sebastian.thiel@icloud.com>
The `#[but_api]` proc macro has multiple expansion paths (`legacy`, `tauri`, `napi`)
but had no dedicated compile-time coverage. Regressions were easy to introduce and
only showed up downstream in `but-api` consumers.

Add a separate `but-api-macros/tests` crate that mirrors `but-api` feature flags and
uses `trybuild` to verify pass/fail behavior for each macro capability. This makes
feature-specific expansion changes explicit and catches breakage at the macro boundary.

Co-authored-by: Sebastian Thiel <sebastian.thiel@icloud.com>
@Byron Byron force-pushed the next4 branch 2 times, most recently from a422132 to 69cbe39 Compare March 3, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants