mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
1 Commits
fix/image-
...
dh--add-gi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae003f6942 |
@@ -7,6 +7,7 @@ use std::sync::atomic::AtomicU64;
|
||||
use crate::AuthManager;
|
||||
use crate::client_common::REVIEW_PROMPT;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::git_info::collect_git_info;
|
||||
use crate::mcp::auth::McpAuthStatusEntry;
|
||||
use crate::mcp_connection_manager::DEFAULT_STARTUP_TIMEOUT;
|
||||
use crate::parse_command::parse_command;
|
||||
@@ -22,6 +23,7 @@ use codex_protocol::ConversationId;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::protocol::ConversationPathResponseEvent;
|
||||
use codex_protocol::protocol::ExitedReviewModeEvent;
|
||||
use codex_protocol::protocol::GitInfo;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
@@ -177,6 +179,7 @@ impl Codex {
|
||||
approval_policy: config.approval_policy,
|
||||
sandbox_policy: config.sandbox_policy.clone(),
|
||||
cwd: config.cwd.clone(),
|
||||
git_info: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
};
|
||||
|
||||
@@ -271,6 +274,7 @@ pub(crate) struct TurnContext {
|
||||
pub(crate) is_review_mode: bool,
|
||||
pub(crate) final_output_json_schema: Option<Value>,
|
||||
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub(crate) git_info: Option<GitInfo>,
|
||||
}
|
||||
|
||||
impl TurnContext {
|
||||
@@ -312,6 +316,8 @@ pub(crate) struct SessionConfiguration {
|
||||
/// operate deterministically.
|
||||
cwd: PathBuf,
|
||||
|
||||
pub(crate) git_info: Option<GitInfo>,
|
||||
|
||||
// TODO(pakrym): Remove config from here
|
||||
original_config_do_not_use: Arc<Config>,
|
||||
}
|
||||
@@ -406,11 +412,12 @@ impl Session {
|
||||
is_review_mode: false,
|
||||
final_output_json_schema: None,
|
||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||
git_info: session_configuration.git_info.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn new(
|
||||
session_configuration: SessionConfiguration,
|
||||
mut session_configuration: SessionConfiguration,
|
||||
config: Arc<Config>,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
tx_event: Sender<Event>,
|
||||
@@ -467,6 +474,7 @@ impl Session {
|
||||
config.mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
);
|
||||
let git_info_fut = collect_git_info(&session_configuration.cwd);
|
||||
|
||||
// Join all independent futures.
|
||||
let (
|
||||
@@ -475,14 +483,18 @@ impl Session {
|
||||
default_shell,
|
||||
(history_log_id, history_entry_count),
|
||||
auth_statuses,
|
||||
git_info,
|
||||
) = tokio::join!(
|
||||
rollout_fut,
|
||||
mcp_fut,
|
||||
default_shell_fut,
|
||||
history_meta_fut,
|
||||
auth_statuses_fut
|
||||
auth_statuses_fut,
|
||||
git_info_fut
|
||||
);
|
||||
|
||||
session_configuration.git_info = git_info;
|
||||
|
||||
let rollout_recorder = rollout_recorder.map_err(|e| {
|
||||
error!("failed to initialize rollout recorder: {e:#}");
|
||||
anyhow::anyhow!("failed to initialize rollout recorder: {e:#}")
|
||||
@@ -630,9 +642,7 @@ impl Session {
|
||||
}
|
||||
|
||||
pub(crate) async fn update_settings(&self, updates: SessionSettingsUpdate) {
|
||||
let mut state = self.state.lock().await;
|
||||
|
||||
state.session_configuration = state.session_configuration.apply(&updates);
|
||||
self.apply_session_updates(&updates).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn new_turn(&self, updates: SessionSettingsUpdate) -> Arc<TurnContext> {
|
||||
@@ -645,12 +655,7 @@ impl Session {
|
||||
sub_id: String,
|
||||
updates: SessionSettingsUpdate,
|
||||
) -> Arc<TurnContext> {
|
||||
let session_configuration = {
|
||||
let mut state = self.state.lock().await;
|
||||
let session_configuration = state.session_configuration.clone().apply(&updates);
|
||||
state.session_configuration = session_configuration.clone();
|
||||
session_configuration
|
||||
};
|
||||
let session_configuration = self.apply_session_updates(&updates).await;
|
||||
|
||||
let mut turn_context: TurnContext = Self::make_turn_context(
|
||||
Some(Arc::clone(&self.services.auth_manager)),
|
||||
@@ -666,6 +671,26 @@ impl Session {
|
||||
Arc::new(turn_context)
|
||||
}
|
||||
|
||||
async fn apply_session_updates(&self, updates: &SessionSettingsUpdate) -> SessionConfiguration {
|
||||
let needs_git_refresh = updates.cwd.is_some();
|
||||
let mut state = self.state.lock().await;
|
||||
let mut session_configuration = state.session_configuration.clone().apply(updates);
|
||||
if needs_git_refresh {
|
||||
session_configuration.git_info = None;
|
||||
}
|
||||
state.session_configuration = session_configuration.clone();
|
||||
drop(state);
|
||||
|
||||
if needs_git_refresh {
|
||||
let git_info = collect_git_info(&session_configuration.cwd).await;
|
||||
session_configuration.git_info = git_info;
|
||||
let mut state = self.state.lock().await;
|
||||
state.session_configuration = session_configuration.clone();
|
||||
}
|
||||
|
||||
session_configuration
|
||||
}
|
||||
|
||||
fn build_environment_update_item(
|
||||
&self,
|
||||
previous: Option<&Arc<TurnContext>>,
|
||||
@@ -919,6 +944,7 @@ impl Session {
|
||||
Some(turn_context.approval_policy),
|
||||
Some(turn_context.sandbox_policy.clone()),
|
||||
Some(self.user_shell().clone()),
|
||||
turn_context.git_info.clone(),
|
||||
)));
|
||||
items
|
||||
}
|
||||
@@ -1472,6 +1498,7 @@ async fn spawn_review_thread(
|
||||
is_review_mode: true,
|
||||
final_output_json_schema: None,
|
||||
codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(),
|
||||
git_info: parent_turn_context.git_info.clone(),
|
||||
};
|
||||
|
||||
// Seed the child task with the review prompt as the initial user message.
|
||||
@@ -2549,6 +2576,7 @@ mod tests {
|
||||
approval_policy: config.approval_policy,
|
||||
sandbox_policy: config.sandbox_policy.clone(),
|
||||
cwd: config.cwd.clone(),
|
||||
git_info: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
};
|
||||
|
||||
@@ -2617,6 +2645,7 @@ mod tests {
|
||||
approval_policy: config.approval_policy,
|
||||
sandbox_policy: config.sandbox_policy.clone(),
|
||||
cwd: config.cwd.clone(),
|
||||
git_info: None,
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
|
||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
|
||||
use codex_protocol::protocol::GitInfo;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, DeriveDisplay)]
|
||||
@@ -29,6 +30,7 @@ pub(crate) struct EnvironmentContext {
|
||||
pub network_access: Option<NetworkAccess>,
|
||||
pub writable_roots: Option<Vec<PathBuf>>,
|
||||
pub shell: Option<Shell>,
|
||||
pub git_info: Option<GitInfo>,
|
||||
}
|
||||
|
||||
impl EnvironmentContext {
|
||||
@@ -37,6 +39,7 @@ impl EnvironmentContext {
|
||||
approval_policy: Option<AskForApproval>,
|
||||
sandbox_policy: Option<SandboxPolicy>,
|
||||
shell: Option<Shell>,
|
||||
git_info: Option<GitInfo>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cwd,
|
||||
@@ -70,6 +73,7 @@ impl EnvironmentContext {
|
||||
_ => None,
|
||||
},
|
||||
shell,
|
||||
git_info,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +89,7 @@ impl EnvironmentContext {
|
||||
writable_roots,
|
||||
// should compare all fields except shell
|
||||
shell: _,
|
||||
git_info,
|
||||
} = other;
|
||||
|
||||
self.cwd == *cwd
|
||||
@@ -92,6 +97,7 @@ impl EnvironmentContext {
|
||||
&& self.sandbox_mode == *sandbox_mode
|
||||
&& self.network_access == *network_access
|
||||
&& self.writable_roots == *writable_roots
|
||||
&& self.git_info == *git_info
|
||||
}
|
||||
|
||||
pub fn diff(before: &TurnContext, after: &TurnContext) -> Self {
|
||||
@@ -110,7 +116,12 @@ impl EnvironmentContext {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
EnvironmentContext::new(cwd, approval_policy, sandbox_policy, None)
|
||||
let git_info = if before.git_info != after.git_info {
|
||||
after.git_info.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
EnvironmentContext::new(cwd, approval_policy, sandbox_policy, None, git_info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +133,7 @@ impl From<&TurnContext> for EnvironmentContext {
|
||||
Some(turn_context.sandbox_policy.clone()),
|
||||
// Shell is not configurable from turn to turn
|
||||
None,
|
||||
turn_context.git_info.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -139,6 +151,7 @@ impl EnvironmentContext {
|
||||
/// <writable_roots>...</writable_roots>
|
||||
/// <network_access>...</network_access>
|
||||
/// <shell>...</shell>
|
||||
/// <git_info>...</git_info>
|
||||
/// </environment_context>
|
||||
/// ```
|
||||
pub fn serialize_to_xml(self) -> String {
|
||||
@@ -174,6 +187,26 @@ impl EnvironmentContext {
|
||||
{
|
||||
lines.push(format!(" <shell>{shell_name}</shell>"));
|
||||
}
|
||||
if let Some(git_info) = self.git_info {
|
||||
let has_data = git_info.commit_hash.is_some()
|
||||
|| git_info.branch.is_some()
|
||||
|| git_info.repository_url.is_some();
|
||||
if has_data {
|
||||
lines.push(" <git_info>".to_string());
|
||||
if let Some(commit_hash) = git_info.commit_hash {
|
||||
lines.push(format!(" <commit_hash>{commit_hash}</commit_hash>"));
|
||||
}
|
||||
if let Some(branch) = git_info.branch {
|
||||
lines.push(format!(" <branch>{branch}</branch>"));
|
||||
}
|
||||
if let Some(repository_url) = git_info.repository_url {
|
||||
lines.push(format!(
|
||||
" <repository_url>{repository_url}</repository_url>"
|
||||
));
|
||||
}
|
||||
lines.push(" </git_info>".to_string());
|
||||
}
|
||||
}
|
||||
lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string());
|
||||
lines.join("\n")
|
||||
}
|
||||
@@ -215,6 +248,7 @@ mod tests {
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(workspace_write_policy(vec!["/repo", "/tmp"], false)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
@@ -238,6 +272,7 @@ mod tests {
|
||||
Some(AskForApproval::Never),
|
||||
Some(SandboxPolicy::ReadOnly),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
@@ -256,6 +291,7 @@ mod tests {
|
||||
Some(AskForApproval::OnFailure),
|
||||
Some(SandboxPolicy::DangerFullAccess),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
@@ -267,6 +303,31 @@ mod tests {
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_environment_context_with_git_info() {
|
||||
let context = EnvironmentContext::new(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(GitInfo {
|
||||
commit_hash: Some("abc123".to_string()),
|
||||
branch: Some("main".to_string()),
|
||||
repository_url: Some("git@example.com:repo.git".to_string()),
|
||||
}),
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
<git_info>
|
||||
<commit_hash>abc123</commit_hash>
|
||||
<branch>main</branch>
|
||||
<repository_url>git@example.com:repo.git</repository_url>
|
||||
</git_info>
|
||||
</environment_context>"#;
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_except_shell_compares_approval_policy() {
|
||||
// Approval policy
|
||||
@@ -275,12 +336,14 @@ mod tests {
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(workspace_write_policy(vec!["/repo"], false)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
Some(AskForApproval::Never),
|
||||
Some(workspace_write_policy(vec!["/repo"], true)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(!context1.equals_except_shell(&context2));
|
||||
}
|
||||
@@ -292,12 +355,14 @@ mod tests {
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(SandboxPolicy::new_read_only_policy()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(SandboxPolicy::new_workspace_write_policy()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!context1.equals_except_shell(&context2));
|
||||
@@ -310,12 +375,14 @@ mod tests {
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(workspace_write_policy(vec!["/repo", "/tmp", "/var"], false)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
Some(AskForApproval::OnRequest),
|
||||
Some(workspace_write_policy(vec!["/repo", "/tmp"], true)),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!context1.equals_except_shell(&context2));
|
||||
@@ -331,6 +398,7 @@ mod tests {
|
||||
shell_path: "/bin/bash".into(),
|
||||
bashrc_path: "/home/user/.bashrc".into(),
|
||||
})),
|
||||
None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
@@ -340,6 +408,7 @@ mod tests {
|
||||
shell_path: "/bin/zsh".into(),
|
||||
zshrc_path: "/home/user/.zshrc".into(),
|
||||
})),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(context1.equals_except_shell(&context2));
|
||||
|
||||
@@ -21,6 +21,9 @@ use core_test_support::load_sse_fixture_with_id;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::wait_for_event;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
@@ -52,6 +55,75 @@ fn default_env_context_str(cwd: &str, shell: &Shell) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn git_command(repo_path: &Path) -> Command {
|
||||
let mut command = Command::new("git");
|
||||
command.current_dir(repo_path);
|
||||
command.env("GIT_CONFIG_GLOBAL", "/dev/null");
|
||||
command.env("GIT_CONFIG_NOSYSTEM", "1");
|
||||
command
|
||||
}
|
||||
|
||||
fn run_git_command(repo_path: &Path, args: &[&str]) {
|
||||
let output = git_command(repo_path)
|
||||
.args(args)
|
||||
.output()
|
||||
.unwrap_or_else(|err| panic!("failed to run git command: {err}"));
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"git {} failed: {}",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn init_git_repo_with_remote() -> (TempDir, String) {
|
||||
let repo = TempDir::new().unwrap();
|
||||
let repo_path = repo.path();
|
||||
|
||||
run_git_command(repo_path, &["init"]);
|
||||
run_git_command(repo_path, &["config", "user.name", "Integration Test"]);
|
||||
run_git_command(repo_path, &["config", "user.email", "test@example.com"]);
|
||||
|
||||
fs::write(repo_path.join("tracked.txt"), "tracked file").unwrap();
|
||||
run_git_command(repo_path, &["add", "."]);
|
||||
run_git_command(repo_path, &["commit", "-m", "initial commit"]);
|
||||
run_git_command(repo_path, &["checkout", "-b", "integration-test-branch"]);
|
||||
run_git_command(
|
||||
repo_path,
|
||||
&[
|
||||
"remote",
|
||||
"add",
|
||||
"origin",
|
||||
"https://github.com/example/git-info-test.git",
|
||||
],
|
||||
);
|
||||
|
||||
let output = git_command(repo_path)
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.unwrap_or_else(|err| panic!("failed to read git commit hash: {err}"));
|
||||
assert!(output.status.success(), "git rev-parse failed");
|
||||
let commit_hash = String::from_utf8(output.stdout)
|
||||
.unwrap_or_else(|err| panic!("commit hash should be utf8: {err}"))
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
(repo, commit_hash)
|
||||
}
|
||||
|
||||
fn find_environment_context(body: &serde_json::Value) -> &str {
|
||||
body["input"]
|
||||
.as_array()
|
||||
.and_then(|items| {
|
||||
items.iter().find_map(|item| {
|
||||
let content = item.get("content")?.as_array()?;
|
||||
let text = content.first()?.get("text")?.as_str()?;
|
||||
text.starts_with("<environment_context>").then_some(text)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| panic!("environment_context message not found"))
|
||||
}
|
||||
|
||||
/// Build minimal SSE stream with completed marker using the JSON fixture.
|
||||
fn sse_completed(id: &str) -> String {
|
||||
load_sse_fixture_with_id("tests/fixtures/completed_template.json", id)
|
||||
@@ -883,3 +955,88 @@ async fn send_user_turn_with_changes_sends_environment_context() {
|
||||
]);
|
||||
assert_eq!(body2["input"], expected_input_2);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn environment_context_includes_git_info_when_available() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let sse = sse_completed("resp");
|
||||
let template = ResponseTemplate::new(200)
|
||||
.insert_header("content-type", "text/event-stream")
|
||||
.set_body_raw(sse, "text/event-stream");
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/responses"))
|
||||
.respond_with(template)
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let model_provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers()["openai"].clone()
|
||||
};
|
||||
|
||||
let (git_repo, commit_hash) = init_git_repo_with_remote();
|
||||
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let mut config = load_default_config_for_test(&codex_home);
|
||||
config.cwd = git_repo.path().to_path_buf();
|
||||
config.model_provider = model_provider;
|
||||
|
||||
let default_cwd = config.cwd.clone();
|
||||
let default_approval_policy = config.approval_policy;
|
||||
let default_sandbox_policy = config.sandbox_policy.clone();
|
||||
let default_model = config.model.clone();
|
||||
let default_effort = config.model_reasoning_effort;
|
||||
let default_summary = config.model_reasoning_summary;
|
||||
|
||||
let conversation_manager =
|
||||
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
||||
let codex = conversation_manager
|
||||
.new_conversation(config)
|
||||
.await
|
||||
.expect("create new conversation")
|
||||
.conversation;
|
||||
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![UserInput::Text {
|
||||
text: "git info turn".into(),
|
||||
}],
|
||||
cwd: default_cwd,
|
||||
approval_policy: default_approval_policy,
|
||||
sandbox_policy: default_sandbox_policy,
|
||||
model: default_model,
|
||||
effort: default_effort,
|
||||
summary: default_summary,
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
let requests = server.received_requests().await.unwrap();
|
||||
assert_eq!(requests.len(), 1, "expected a single POST request");
|
||||
|
||||
let body = requests[0].body_json::<serde_json::Value>().unwrap();
|
||||
let env_text = find_environment_context(&body);
|
||||
|
||||
assert!(env_text.contains("<git_info>"), "git info block missing");
|
||||
assert!(
|
||||
env_text.contains(&format!("<commit_hash>{commit_hash}</commit_hash>")),
|
||||
"commit hash missing from git info"
|
||||
);
|
||||
assert!(
|
||||
env_text.contains("<branch>integration-test-branch</branch>"),
|
||||
"branch missing from git info"
|
||||
);
|
||||
assert!(
|
||||
env_text.contains(
|
||||
"<repository_url>https://github.com/example/git-info-test.git</repository_url>"
|
||||
),
|
||||
"repository url missing from git info"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1002,7 +1002,7 @@ pub struct RolloutLine {
|
||||
pub item: RolloutItem,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
|
||||
pub struct GitInfo {
|
||||
/// Current commit hash (SHA)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
Reference in New Issue
Block a user