Skip to repository content412 lines · 12.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:25:58.962Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
git.rs
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