Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:34:16.798Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

git.rs

412 lines · 12.2 KB · rust
1pub mod blame;
2pub mod commit;
3mod hosting_provider;
4mod remote;
5pub mod repository;
6pub mod stash;
7pub mod status;
8
9pub use crate::hosting_provider::*;
10pub use crate::remote::*;
11use anyhow::Result;
12use gpui::{Action, actions};
13pub use repository::RemoteCommandOutput;
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16use std::fmt::{self, Write as _};
17use std::str::FromStr;
18
19pub const DOT_GIT: &str = ".git";
20pub const GITIGNORE: &str = ".gitignore";
21pub const FSMONITOR_DAEMON: &str = "fsmonitor--daemon";
22pub const LFS_DIR: &str = "lfs";
23pub const OBJECTS_DIR: &str = "objects";
24pub const REFS_DIR: &str = "refs";
25pub const REFTABLE_DIR: &str = "reftable";
26pub const HOOKS_DIR: &str = "hooks";
27pub const LOGS_DIR: &str = "logs";
28pub const LOGS_REF_STASH: &str = "logs/refs/stash";
29pub const REBASE_MERGE_DIR: &str = "rebase-merge";
30pub const REBASE_APPLY_DIR: &str = "rebase-apply";
31pub const SEQUENCER_DIR: &str = "sequencer";
32pub const COMMIT_MESSAGE: &str = "COMMIT_EDITMSG";
33pub const FETCH_HEAD: &str = "FETCH_HEAD";
34pub const ORIG_HEAD: &str = "ORIG_HEAD";
35pub const BISECT_LOG: &str = "BISECT_LOG";
36pub const GC_PID: &str = "gc.pid";
37pub const INFO_DIR: &str = "info";
38pub const REPO_EXCLUDE: &str = "info/exclude";
39
40actions!(
41    git,
42    [
43        // per-hunk
44        /// Toggles the staged state of the hunk or status entry at cursor.
45        ToggleStaged,
46        /// Stage status entries between an anchor entry and the cursor.
47        StageRange,
48        /// Stages the current hunk and moves to the next one.
49        StageAndNext,
50        /// Unstages the current hunk and moves to the next one.
51        UnstageAndNext,
52        /// Restores the selected hunks to their original state.
53        #[action(deprecated_aliases = ["editor::RevertSelectedHunks"])]
54        Restore,
55        /// Restores the selected hunks to their original state and moves to the
56        /// next one.
57        RestoreAndNext,
58        // per-file
59        /// Shows git blame information for the current file.
60        #[action(deprecated_aliases = ["editor::ToggleGitBlame"])]
61        Blame,
62        /// Shows the git history for the selected file, folder, or project.
63        FileHistory,
64        /// Opens the selected file in the editor without a diff view.
65        ViewFile,
66        /// Stages the current file.
67        StageFile,
68        /// Unstages the current file.
69        UnstageFile,
70        // repo-wide
71        /// Stages all changes in the repository.
72        StageAll,
73        /// Unstages all changes in the repository.
74        UnstageAll,
75        /// Stashes all changes in the repository, including untracked files.
76        StashAll,
77        /// Pops the most recent stash.
78        StashPop,
79        /// Apply the most recent stash.
80        StashApply,
81        /// Restores all tracked files to their last committed state.
82        RestoreTrackedFiles,
83        /// Moves all untracked files to trash.
84        TrashUntrackedFiles,
85        /// Undoes the last commit, keeping changes in the working directory.
86        Uncommit,
87        /// Pushes commits to the remote repository.
88        Push,
89        /// Pushes commits to a specific remote branch.
90        PushTo,
91        /// Force pushes commits to the remote repository.
92        ForcePush,
93        /// Pulls changes from the remote repository.
94        Pull,
95        /// Pulls changes from the remote repository with rebase.
96        PullRebase,
97        /// Fetches changes from the remote repository.
98        Fetch,
99        /// Fetches changes from a specific remote.
100        FetchFrom,
101        /// Creates a new commit with staged changes.
102        Commit,
103        /// Runs the next commit with `git commit --no-verify`.
104        SkipHooks,
105        /// Amends the last commit with staged changes.
106        Amend,
107        /// Enable the --signoff option.
108        Signoff,
109        /// Cancels the current git operation.
110        Cancel,
111        /// Expands the commit message editor.
112        ExpandCommitEditor,
113        /// Toggles whether the commit message editor fills all the available
114        /// vertical space within the git panel.
115        ToggleFillCommitEditor,
116        /// Generates a commit message using AI.
117        GenerateCommitMessage,
118        /// Initializes a new git repository.
119        Init,
120        /// Opens all modified files in the editor.
121        OpenModifiedFiles,
122        /// Opens the current file in a solo diff view.
123        OpenFileDiff,
124        /// Clones a repository.
125        Clone,
126        ViewCommit,
127        /// Adds a file to .gitignore.
128        AddToGitignore,
129        /// Adds a file to the repository's .git/info/exclude.
130        AddToGitInfoExclude,
131        /// Copies the current branch name to the clipboard.
132        CopyBranchName,
133    ]
134);
135
136/// Renames a git branch.
137#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
138#[action(namespace = git)]
139#[serde(deny_unknown_fields)]
140pub struct RenameBranch {
141    /// The branch to rename.
142    ///
143    /// Default: the current branch.
144    #[serde(default)]
145    pub branch: Option<String>,
146}
147
148/// Restores a file to its last committed state, discarding local changes.
149#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
150#[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])]
151#[serde(deny_unknown_fields)]
152pub struct RestoreFile {
153    #[serde(default)]
154    pub skip_prompt: bool,
155}
156
157/// The length of a Git short SHA.
158pub const SHORT_SHA_LENGTH: usize = 7;
159
160const SHA1_BYTE_LENGTH: usize = 20;
161const SHA256_BYTE_LENGTH: usize = 32;
162const SHA1_HEX_LENGTH: usize = SHA1_BYTE_LENGTH * 2;
163const SHA256_HEX_LENGTH: usize = SHA256_BYTE_LENGTH * 2;
164const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
165
166#[derive(Clone, Copy, Eq, Hash, PartialEq)]
167pub struct Oid {
168    bytes: [u8; SHA256_BYTE_LENGTH],
169    format: OidFormat,
170}
171
172#[derive(Clone, Copy, Eq, Hash, PartialEq)]
173enum OidFormat {
174    Sha1,
175    Sha256,
176}
177
178impl OidFormat {
179    fn byte_len(self) -> usize {
180        match self {
181            Self::Sha1 => SHA1_BYTE_LENGTH,
182            Self::Sha256 => SHA256_BYTE_LENGTH,
183        }
184    }
185
186    fn hex_len(self) -> usize {
187        match self {
188            Self::Sha1 => SHA1_HEX_LENGTH,
189            Self::Sha256 => SHA256_HEX_LENGTH,
190        }
191    }
192}
193
194impl Oid {
195    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
196        let format = match bytes.len() {
197            SHA1_BYTE_LENGTH => OidFormat::Sha1,
198            SHA256_BYTE_LENGTH => OidFormat::Sha256,
199            len => {
200                anyhow::bail!(
201                    "invalid git oid byte length: expected {SHA1_BYTE_LENGTH} for SHA-1 or {SHA256_BYTE_LENGTH} for SHA-256, got {len}"
202                );
203            }
204        };
205
206        let mut oid_bytes = [0u8; SHA256_BYTE_LENGTH];
207        oid_bytes[..bytes.len()].copy_from_slice(bytes);
208        Ok(Self {
209            bytes: oid_bytes,
210            format,
211        })
212    }
213
214    #[cfg(any(test, feature = "test-support"))]
215    pub fn random(rng: &mut impl rand::Rng) -> Self {
216        let mut bytes = [0u8; SHA256_BYTE_LENGTH];
217        rng.fill(&mut bytes[..SHA1_BYTE_LENGTH]);
218        Self {
219            bytes,
220            format: OidFormat::Sha1,
221        }
222    }
223
224    pub fn as_bytes(&self) -> &[u8] {
225        &self.bytes[..self.format.byte_len()]
226    }
227
228    pub(crate) fn is_zero(&self) -> bool {
229        self.as_bytes().iter().all(|byte| *byte == 0)
230    }
231
232    /// Returns this [`Oid`] as a short SHA.
233    pub fn display_short(&self) -> String {
234        self.hex_string(SHORT_SHA_LENGTH)
235    }
236
237    fn hex_string(&self, len: usize) -> String {
238        let mut string = String::with_capacity(len);
239        for index in 0..len {
240            string.push(self.hex_digit(index));
241        }
242        string
243    }
244
245    fn write_hex(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        for index in 0..self.format.hex_len() {
247            f.write_char(self.hex_digit(index))?;
248        }
249        Ok(())
250    }
251
252    #[inline(always)]
253    fn hex_digit(&self, index: usize) -> char {
254        debug_assert!(index < self.format.hex_len());
255        let byte = self.as_bytes()[index / 2];
256        let nibble = if index & 1 == 0 {
257            byte >> 4
258        } else {
259            byte & 0x0f
260        };
261        char::from(HEX_DIGITS[nibble as usize])
262    }
263}
264
265impl TryFrom<&str> for Oid {
266    type Error = anyhow::Error;
267
268    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
269        Oid::from_str(value)
270    }
271}
272
273impl FromStr for Oid {
274    type Err = anyhow::Error;
275
276    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
277        let format = match s.len() {
278            1..=SHA1_HEX_LENGTH => OidFormat::Sha1,
279            SHA256_HEX_LENGTH => OidFormat::Sha256,
280            len => {
281                anyhow::bail!(
282                    "invalid git oid hex length: expected 1..={SHA1_HEX_LENGTH} for SHA-1 or {SHA256_HEX_LENGTH} for SHA-256, got {len}"
283                );
284            }
285        };
286
287        let mut bytes = [0u8; SHA256_BYTE_LENGTH];
288        for (index, byte) in s.bytes().enumerate() {
289            let digit = decode_hex_digit(byte)
290                .ok_or_else(|| anyhow::anyhow!("invalid hex digit at byte {index} for git oid"))?;
291            if index % 2 == 0 {
292                bytes[index / 2] = digit << 4;
293            } else {
294                bytes[index / 2] |= digit;
295            }
296        }
297
298        Ok(Self { bytes, format })
299    }
300}
301
302fn decode_hex_digit(byte: u8) -> Option<u8> {
303    match byte {
304        b'0'..=b'9' => Some(byte - b'0'),
305        b'a'..=b'f' => Some(byte - b'a' + 10),
306        b'A'..=b'F' => Some(byte - b'A' + 10),
307        _ => None,
308    }
309}
310
311impl fmt::Debug for Oid {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        fmt::Display::fmt(self, f)
314    }
315}
316
317impl fmt::Display for Oid {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        self.write_hex(f)
320    }
321}
322
323impl Serialize for Oid {
324    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
325    where
326        S: serde::Serializer,
327    {
328        serializer.serialize_str(&self.hex_string(self.format.hex_len()))
329    }
330}
331
332impl<'de> Deserialize<'de> for Oid {
333    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
334    where
335        D: serde::Deserializer<'de>,
336    {
337        let s = String::deserialize(deserializer)?;
338        s.parse::<Oid>().map_err(serde::de::Error::custom)
339    }
340}
341
342impl From<Oid> for u32 {
343    fn from(oid: Oid) -> Self {
344        let mut u32_bytes = [0u8; 4];
345        u32_bytes.copy_from_slice(&oid.as_bytes()[..4]);
346        u32::from_ne_bytes(u32_bytes)
347    }
348}
349
350impl From<Oid> for usize {
351    fn from(oid: Oid) -> Self {
352        let mut u64_bytes = [0u8; 8];
353        u64_bytes.copy_from_slice(&oid.as_bytes()[..8]);
354        u64::from_ne_bytes(u64_bytes) as usize
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn parses_short_sha1_oid_by_padding_trailing_nibbles() {
364        let oid = "abc1234".parse::<Oid>().expect("failed to parse oid");
365
366        assert_eq!(oid.as_bytes().len(), SHA1_BYTE_LENGTH);
367        assert_eq!(oid.display_short(), "abc1234");
368        assert_eq!(oid.to_string(), "abc1234000000000000000000000000000000000");
369    }
370
371    #[test]
372    fn parses_full_sha256_oid() {
373        let sha = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
374        let oid = sha.parse::<Oid>().expect("failed to parse oid");
375
376        assert_eq!(oid.as_bytes().len(), SHA256_BYTE_LENGTH);
377        assert_eq!(oid.to_string(), sha);
378    }
379
380    #[test]
381    fn rejects_invalid_oid_lengths() {
382        assert!("".parse::<Oid>().is_err());
383        assert!("a".repeat(SHA1_HEX_LENGTH + 1).parse::<Oid>().is_err());
384        assert!("a".repeat(SHA256_HEX_LENGTH - 1).parse::<Oid>().is_err());
385    }
386}
387
388#[repr(i32)]
389#[derive(Copy, Clone, Debug)]
390pub enum RunHook {
391    PreCommit,
392}
393
394impl RunHook {
395    pub fn as_str(&self) -> &str {
396        match self {
397            Self::PreCommit => "pre-commit",
398        }
399    }
400
401    pub fn to_proto(&self) -> i32 {
402        *self as i32
403    }
404
405    pub fn from_proto(value: i32) -> Option<Self> {
406        match value {
407            0 => Some(Self::PreCommit),
408            _ => None,
409        }
410    }
411}
412
Served at tenant.openagents/omega Member data and write actions are omitted.