Revert "git: Intercept signing prompt from GPG when committing" (#34306)
Reverts zed-industries/zed#34096 This introduced a regression, because the unlocked key can't benefit from caching. Release Notes: - N/A
This commit is contained in:
parent
12bc8907d9
commit
625ce12a3e
9 changed files with 52 additions and 193 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -608,7 +608,6 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"smol",
|
"smol",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"unindent",
|
|
||||||
"util",
|
"util",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
|
@ -19,6 +19,5 @@ net.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
unindent.workspace = true
|
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
|
@ -40,21 +40,11 @@ impl AskPassDelegate {
|
||||||
self.tx.send((prompt, tx)).await?;
|
self.tx.send((prompt, tx)).await?;
|
||||||
Ok(rx.await?)
|
Ok(rx.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_always_failing() -> Self {
|
|
||||||
let (tx, _rx) = mpsc::unbounded::<(String, oneshot::Sender<String>)>();
|
|
||||||
Self {
|
|
||||||
tx,
|
|
||||||
_task: Task::ready(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AskPassSession {
|
pub struct AskPassSession {
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
script_path: std::path::PathBuf,
|
script_path: std::path::PathBuf,
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
gpg_script_path: std::path::PathBuf,
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
askpass_helper: String,
|
askpass_helper: String,
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
@ -69,9 +59,6 @@ const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1";
|
const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1";
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
const GPG_SCRIPT_NAME: &str = "gpg.sh";
|
|
||||||
|
|
||||||
impl AskPassSession {
|
impl AskPassSession {
|
||||||
/// This will create a new AskPassSession.
|
/// This will create a new AskPassSession.
|
||||||
/// You must retain this session until the master process exits.
|
/// You must retain this session until the master process exits.
|
||||||
|
@ -85,8 +72,6 @@ impl AskPassSession {
|
||||||
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
|
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
|
||||||
let askpass_socket = temp_dir.path().join("askpass.sock");
|
let askpass_socket = temp_dir.path().join("askpass.sock");
|
||||||
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
|
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
let gpg_script_path = temp_dir.path().join(GPG_SCRIPT_NAME);
|
|
||||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||||
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
|
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
@ -150,20 +135,9 @@ impl AskPassSession {
|
||||||
askpass_script_path.display()
|
askpass_script_path.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
let gpg_script = generate_gpg_script();
|
|
||||||
fs::write(&gpg_script_path, gpg_script)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("creating gpg wrapper script at {gpg_script_path:?}"))?;
|
|
||||||
make_file_executable(&gpg_script_path).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
script_path: askpass_script_path,
|
script_path: askpass_script_path,
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
gpg_script_path,
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
secret,
|
secret,
|
||||||
|
@ -186,19 +160,6 @@ impl AskPassSession {
|
||||||
&self.askpass_helper
|
&self.askpass_helper
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub fn gpg_script_path(&self) -> Option<impl AsRef<OsStr>> {
|
|
||||||
Some(&self.gpg_script_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn gpg_script_path(&self) -> Option<impl AsRef<OsStr>> {
|
|
||||||
// TODO implement wrapping GPG on Windows. This is more difficult than on Unix
|
|
||||||
// because we can't use --passphrase-fd with a nonstandard FD, and both --passphrase
|
|
||||||
// and --passphrase-file are insecure.
|
|
||||||
None::<std::path::PathBuf>
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will run the askpass task forever, resolving as many authentication requests as needed.
|
// This will run the askpass task forever, resolving as many authentication requests as needed.
|
||||||
// The caller is responsible for examining the result of their own commands and cancelling this
|
// The caller is responsible for examining the result of their own commands and cancelling this
|
||||||
// future when this is no longer needed. Note that this can only be called once, but due to the
|
// future when this is no longer needed. Note that this can only be called once, but due to the
|
||||||
|
@ -302,23 +263,3 @@ fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::pat
|
||||||
askpass_socket = askpass_socket.display(),
|
askpass_socket = askpass_socket.display(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn generate_gpg_script() -> String {
|
|
||||||
use unindent::Unindent as _;
|
|
||||||
|
|
||||||
r#"
|
|
||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
unset GIT_CONFIG_PARAMETERS
|
|
||||||
GPG_PROGRAM=$(git config gpg.program || echo 'gpg')
|
|
||||||
PROMPT="Enter passphrase to unlock GPG key:"
|
|
||||||
PASSPHRASE=$(${GIT_ASKPASS} "${PROMPT}")
|
|
||||||
|
|
||||||
exec "${GPG_PROGRAM}" --batch --no-tty --yes --passphrase-fd 3 --pinentry-mode loopback "$@" 3<<EOF
|
|
||||||
${PASSPHRASE}
|
|
||||||
EOF
|
|
||||||
"#.unindent()
|
|
||||||
}
|
|
||||||
|
|
|
@ -375,10 +375,8 @@ impl GitRepository for FakeGitRepository {
|
||||||
_message: gpui::SharedString,
|
_message: gpui::SharedString,
|
||||||
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
|
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
|
||||||
_options: CommitOptions,
|
_options: CommitOptions,
|
||||||
_ask_pass: AskPassDelegate,
|
|
||||||
_env: Arc<HashMap<String, String>>,
|
_env: Arc<HashMap<String, String>>,
|
||||||
_cx: AsyncApp,
|
) -> BoxFuture<'_, Result<()>> {
|
||||||
) -> BoxFuture<'static, Result<()>> {
|
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,9 +41,9 @@ futures.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tempfile.workspace = true
|
|
||||||
text = { workspace = true, features = ["test-support"] }
|
text = { workspace = true, features = ["test-support"] }
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
tempfile.workspace = true
|
||||||
|
|
|
@ -391,12 +391,8 @@ pub trait GitRepository: Send + Sync {
|
||||||
message: SharedString,
|
message: SharedString,
|
||||||
name_and_email: Option<(SharedString, SharedString)>,
|
name_and_email: Option<(SharedString, SharedString)>,
|
||||||
options: CommitOptions,
|
options: CommitOptions,
|
||||||
askpass: AskPassDelegate,
|
|
||||||
env: Arc<HashMap<String, String>>,
|
env: Arc<HashMap<String, String>>,
|
||||||
// This method takes an AsyncApp to ensure it's invoked on the main thread,
|
) -> BoxFuture<'_, Result<()>>;
|
||||||
// otherwise git-credentials-manager won't work.
|
|
||||||
cx: AsyncApp,
|
|
||||||
) -> BoxFuture<'static, Result<()>>;
|
|
||||||
|
|
||||||
fn push(
|
fn push(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1197,68 +1193,36 @@ impl GitRepository for RealGitRepository {
|
||||||
message: SharedString,
|
message: SharedString,
|
||||||
name_and_email: Option<(SharedString, SharedString)>,
|
name_and_email: Option<(SharedString, SharedString)>,
|
||||||
options: CommitOptions,
|
options: CommitOptions,
|
||||||
ask_pass: AskPassDelegate,
|
|
||||||
env: Arc<HashMap<String, String>>,
|
env: Arc<HashMap<String, String>>,
|
||||||
cx: AsyncApp,
|
) -> BoxFuture<'_, Result<()>> {
|
||||||
) -> BoxFuture<'static, Result<()>> {
|
|
||||||
let working_directory = self.working_directory();
|
let working_directory = self.working_directory();
|
||||||
let executor = cx.background_executor().clone();
|
self.executor
|
||||||
async move {
|
.spawn(async move {
|
||||||
let working_directory = working_directory?;
|
let mut cmd = new_smol_command("git");
|
||||||
let have_user_git_askpass = env.contains_key("GIT_ASKPASS");
|
cmd.current_dir(&working_directory?)
|
||||||
let mut command = new_smol_command("git");
|
.envs(env.iter())
|
||||||
command.current_dir(&working_directory).envs(env.iter());
|
.args(["commit", "--quiet", "-m"])
|
||||||
|
.arg(&message.to_string())
|
||||||
|
.arg("--cleanup=strip");
|
||||||
|
|
||||||
let ask_pass = if have_user_git_askpass {
|
if options.amend {
|
||||||
None
|
cmd.arg("--amend");
|
||||||
} else {
|
}
|
||||||
Some(AskPassSession::new(&executor, ask_pass).await?)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(program) = ask_pass
|
if let Some((name, email)) = name_and_email {
|
||||||
.as_ref()
|
cmd.arg("--author").arg(&format!("{name} <{email}>"));
|
||||||
.and_then(|ask_pass| ask_pass.gpg_script_path())
|
}
|
||||||
{
|
|
||||||
command.arg("-c").arg(format!(
|
|
||||||
"gpg.program={}",
|
|
||||||
program.as_ref().to_string_lossy()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
command
|
let output = cmd.output().await?;
|
||||||
.args(["commit", "-m"])
|
|
||||||
.arg(message.to_string())
|
|
||||||
.arg("--cleanup=strip")
|
|
||||||
.stdin(smol::process::Stdio::null())
|
|
||||||
.stdout(smol::process::Stdio::piped())
|
|
||||||
.stderr(smol::process::Stdio::piped());
|
|
||||||
|
|
||||||
if options.amend {
|
|
||||||
command.arg("--amend");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((name, email)) = name_and_email {
|
|
||||||
command.arg("--author").arg(&format!("{name} <{email}>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ask_pass) = ask_pass {
|
|
||||||
command.env("GIT_ASKPASS", ask_pass.script_path());
|
|
||||||
let git_process = command.spawn()?;
|
|
||||||
|
|
||||||
run_askpass_command(ask_pass, git_process).await?;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
let git_process = command.spawn()?;
|
|
||||||
let output = git_process.output().await?;
|
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
output.status.success(),
|
output.status.success(),
|
||||||
"{}",
|
"Failed to commit:\n{}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
})
|
||||||
}
|
.boxed()
|
||||||
.boxed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(
|
fn push(
|
||||||
|
@ -2082,16 +2046,12 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx.spawn(|cx| {
|
repo.commit(
|
||||||
repo.commit(
|
"Initial commit".into(),
|
||||||
"Initial commit".into(),
|
None,
|
||||||
None,
|
CommitOptions::default(),
|
||||||
CommitOptions::default(),
|
Arc::new(checkpoint_author_envs()),
|
||||||
AskPassDelegate::new_always_failing(),
|
)
|
||||||
Arc::new(checkpoint_author_envs()),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -2115,16 +2075,12 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx.spawn(|cx| {
|
repo.commit(
|
||||||
repo.commit(
|
"Commit after checkpoint".into(),
|
||||||
"Commit after checkpoint".into(),
|
None,
|
||||||
None,
|
CommitOptions::default(),
|
||||||
CommitOptions::default(),
|
Arc::new(checkpoint_author_envs()),
|
||||||
AskPassDelegate::new_always_failing(),
|
)
|
||||||
Arc::new(checkpoint_author_envs()),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -2257,16 +2213,12 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx.spawn(|cx| {
|
repo.commit(
|
||||||
repo.commit(
|
"Initial commit".into(),
|
||||||
"Initial commit".into(),
|
None,
|
||||||
None,
|
CommitOptions::default(),
|
||||||
CommitOptions::default(),
|
Arc::new(checkpoint_author_envs()),
|
||||||
AskPassDelegate::new_always_failing(),
|
)
|
||||||
Arc::new(checkpoint_author_envs()),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -1574,15 +1574,10 @@ impl GitPanel {
|
||||||
|
|
||||||
let task = if self.has_staged_changes() {
|
let task = if self.has_staged_changes() {
|
||||||
// Repository serializes all git operations, so we can just send a commit immediately
|
// Repository serializes all git operations, so we can just send a commit immediately
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
let commit_task = active_repository.update(cx, |repo, cx| {
|
||||||
let askpass_delegate = this.update_in(cx, |this, window, cx| {
|
repo.commit(message.into(), None, options, cx)
|
||||||
this.askpass_delegate("git commit", window, cx)
|
});
|
||||||
})?;
|
cx.background_spawn(async move { commit_task.await? })
|
||||||
let commit_task = active_repository.update(cx, |repo, cx| {
|
|
||||||
repo.commit(message.into(), None, options, askpass_delegate, cx)
|
|
||||||
})?;
|
|
||||||
commit_task.await?
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
let changed_files = self
|
let changed_files = self
|
||||||
.entries
|
.entries
|
||||||
|
@ -1599,13 +1594,10 @@ impl GitPanel {
|
||||||
|
|
||||||
let stage_task =
|
let stage_task =
|
||||||
active_repository.update(cx, |repo, cx| repo.stage_entries(changed_files, cx));
|
active_repository.update(cx, |repo, cx| repo.stage_entries(changed_files, cx));
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn(async move |_, cx| {
|
||||||
stage_task.await?;
|
stage_task.await?;
|
||||||
let askpass_delegate = this.update_in(cx, |this, window, cx| {
|
|
||||||
this.askpass_delegate("git commit".to_string(), window, cx)
|
|
||||||
})?;
|
|
||||||
let commit_task = active_repository.update(cx, |repo, cx| {
|
let commit_task = active_repository.update(cx, |repo, cx| {
|
||||||
repo.commit(message.into(), None, options, askpass_delegate, cx)
|
repo.commit(message.into(), None, options, cx)
|
||||||
})?;
|
})?;
|
||||||
commit_task.await?
|
commit_task.await?
|
||||||
})
|
})
|
||||||
|
|
|
@ -1726,18 +1726,6 @@ impl GitStore {
|
||||||
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
||||||
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
||||||
|
|
||||||
let askpass = if let Some(askpass_id) = envelope.payload.askpass_id {
|
|
||||||
make_remote_delegate(
|
|
||||||
this,
|
|
||||||
envelope.payload.project_id,
|
|
||||||
repository_id,
|
|
||||||
askpass_id,
|
|
||||||
&mut cx,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AskPassDelegate::new_always_failing()
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = SharedString::from(envelope.payload.message);
|
let message = SharedString::from(envelope.payload.message);
|
||||||
let name = envelope.payload.name.map(SharedString::from);
|
let name = envelope.payload.name.map(SharedString::from);
|
||||||
let email = envelope.payload.email.map(SharedString::from);
|
let email = envelope.payload.email.map(SharedString::from);
|
||||||
|
@ -1751,7 +1739,6 @@ impl GitStore {
|
||||||
CommitOptions {
|
CommitOptions {
|
||||||
amend: options.amend,
|
amend: options.amend,
|
||||||
},
|
},
|
||||||
askpass,
|
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
|
@ -3475,14 +3462,11 @@ impl Repository {
|
||||||
message: SharedString,
|
message: SharedString,
|
||||||
name_and_email: Option<(SharedString, SharedString)>,
|
name_and_email: Option<(SharedString, SharedString)>,
|
||||||
options: CommitOptions,
|
options: CommitOptions,
|
||||||
askpass: AskPassDelegate,
|
|
||||||
_cx: &mut App,
|
_cx: &mut App,
|
||||||
) -> oneshot::Receiver<Result<()>> {
|
) -> oneshot::Receiver<Result<()>> {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let askpass_delegates = self.askpass_delegates.clone();
|
|
||||||
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
|
||||||
|
|
||||||
self.send_job(Some("git commit".into()), move |git_repo, cx| async move {
|
self.send_job(Some("git commit".into()), move |git_repo, _cx| async move {
|
||||||
match git_repo {
|
match git_repo {
|
||||||
RepositoryState::Local {
|
RepositoryState::Local {
|
||||||
backend,
|
backend,
|
||||||
|
@ -3490,16 +3474,10 @@ impl Repository {
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
backend
|
backend
|
||||||
.commit(message, name_and_email, options, askpass, environment, cx)
|
.commit(message, name_and_email, options, environment)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
RepositoryState::Remote { project_id, client } => {
|
RepositoryState::Remote { project_id, client } => {
|
||||||
askpass_delegates.lock().insert(askpass_id, askpass);
|
|
||||||
let _defer = util::defer(|| {
|
|
||||||
let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
|
|
||||||
debug_assert!(askpass_delegate.is_some());
|
|
||||||
});
|
|
||||||
|
|
||||||
let (name, email) = name_and_email.unzip();
|
let (name, email) = name_and_email.unzip();
|
||||||
client
|
client
|
||||||
.request(proto::Commit {
|
.request(proto::Commit {
|
||||||
|
@ -3511,9 +3489,9 @@ impl Repository {
|
||||||
options: Some(proto::commit::CommitOptions {
|
options: Some(proto::commit::CommitOptions {
|
||||||
amend: options.amend,
|
amend: options.amend,
|
||||||
}),
|
}),
|
||||||
askpass_id: Some(askpass_id),
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await
|
||||||
|
.context("sending commit request")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,7 +294,7 @@ message Commit {
|
||||||
optional string email = 5;
|
optional string email = 5;
|
||||||
string message = 6;
|
string message = 6;
|
||||||
optional CommitOptions options = 7;
|
optional CommitOptions options = 7;
|
||||||
optional uint64 askpass_id = 8;
|
reserved 8;
|
||||||
|
|
||||||
message CommitOptions {
|
message CommitOptions {
|
||||||
bool amend = 1;
|
bool amend = 1;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue