Skip to main content

jj_lib/
git_subprocess.rs

1// Copyright 2025 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::io;
16use std::io::BufReader;
17use std::io::Read;
18use std::num::NonZeroU32;
19use std::path::PathBuf;
20use std::process::Child;
21use std::process::Command;
22use std::process::Output;
23use std::process::Stdio;
24use std::thread;
25
26use bstr::BStr;
27use bstr::ByteSlice as _;
28use itertools::Itertools as _;
29use thiserror::Error;
30
31use crate::git::GitPushOptions;
32use crate::git::GitPushStats;
33use crate::git::GitSubprocessOptions;
34use crate::git::NegativeRefSpec;
35use crate::git::RefSpec;
36use crate::git::RefToPush;
37use crate::git_backend::GitBackend;
38use crate::merge::Diff;
39use crate::ref_name::GitRefNameBuf;
40use crate::ref_name::RefNameBuf;
41use crate::ref_name::RemoteName;
42
43// * 2.29.0 introduced `git fetch --no-write-fetch-head`
44// * 2.40 still receives security patches (latest one was in Jan/2025)
45// * 2.41.0 introduced `git fetch --porcelain`
46// If bumped, please update ../../docs/install-and-setup.md
47const MINIMUM_GIT_VERSION: &str = "2.41.0";
48
49/// Error originating by a Git subprocess
50#[derive(Error, Debug)]
51pub enum GitSubprocessError {
52    #[error("Could not find repository at '{0}'")]
53    NoSuchRepository(String),
54    #[error("Could not execute the git process, found in the OS path '{path}'")]
55    SpawnInPath {
56        path: PathBuf,
57        #[source]
58        error: std::io::Error,
59    },
60    #[error("Could not execute git process at specified path '{path}'")]
61    Spawn {
62        path: PathBuf,
63        #[source]
64        error: std::io::Error,
65    },
66    #[error("Failed to wait for the git process")]
67    Wait(std::io::Error),
68    #[error(
69        "Git does not recognize required option: {0} (note: Jujutsu requires git >= \
70         {MINIMUM_GIT_VERSION})"
71    )]
72    UnsupportedGitOption(String),
73    #[error("Git process failed: {0}")]
74    External(String),
75}
76
77/// Context for creating Git subprocesses
78pub(crate) struct GitSubprocessContext {
79    git_dir: PathBuf,
80    options: GitSubprocessOptions,
81}
82
83impl GitSubprocessContext {
84    pub(crate) fn new(git_dir: impl Into<PathBuf>, options: GitSubprocessOptions) -> Self {
85        Self {
86            git_dir: git_dir.into(),
87            options,
88        }
89    }
90
91    pub(crate) fn from_git_backend(
92        git_backend: &GitBackend,
93        options: GitSubprocessOptions,
94    ) -> Self {
95        Self::new(git_backend.git_repo_path(), options)
96    }
97
98    /// Create the Git command
99    fn create_command(&self) -> Command {
100        let mut git_cmd = Command::new(&self.options.executable_path);
101        // Hide console window on Windows (https://stackoverflow.com/a/60958956)
102        #[cfg(windows)]
103        {
104            use std::os::windows::process::CommandExt as _;
105            const CREATE_NO_WINDOW: u32 = 0x08000000;
106            git_cmd.creation_flags(CREATE_NO_WINDOW);
107        }
108
109        // TODO: here we are passing the full path to the git_dir, which can lead to UNC
110        // bugs in Windows. The ideal way to do this is to pass the workspace
111        // root to Command::current_dir and then pass a relative path to the git
112        // dir
113        git_cmd
114            // The gitconfig-controlled automated spawning of the macOS `fsmonitor--daemon`
115            // can cause strange behavior with certain subprocess operations.
116            // For example: https://github.com/jj-vcs/jj/issues/6440.
117            //
118            // Nothing we're doing in `jj` interacts with this daemon, so we force the
119            // config to be false for subprocess operations in order to avoid these
120            // interactions.
121            //
122            // In a colocated workspace, the daemon will still get started the first
123            // time a `git` command is run manually if the gitconfigs are set up that way.
124            .args(["-c", "core.fsmonitor=false"])
125            // Avoids an error message when fetching repos with submodules if
126            // user has `submodule.recurse` configured to true in their Git
127            // config (#7565).
128            .args(["-c", "submodule.recurse=false"])
129            .arg("--git-dir")
130            .arg(&self.git_dir)
131            // Disable translation so we can parse the output. We don't set
132            // LC_ALL=C because it would change the encoding. Also note that
133            // "C.UTF-8" locale isn't always available.
134            .env_remove("LC_ALL")
135            .env_remove("LANGUAGE")
136            .env("LC_MESSAGES", "C")
137            .stdin(Stdio::null())
138            .stderr(Stdio::piped());
139
140        git_cmd.envs(&self.options.environment);
141
142        git_cmd
143    }
144
145    /// Spawn the git command
146    fn spawn_cmd(&self, mut git_cmd: Command) -> Result<Child, GitSubprocessError> {
147        tracing::debug!(cmd = ?git_cmd, "spawning a git subprocess");
148
149        git_cmd.spawn().map_err(|error| {
150            if self.options.executable_path.is_absolute() {
151                GitSubprocessError::Spawn {
152                    path: self.options.executable_path.clone(),
153                    error,
154                }
155            } else {
156                GitSubprocessError::SpawnInPath {
157                    path: self.options.executable_path.clone(),
158                    error,
159                }
160            }
161        })
162    }
163
164    /// Perform a git fetch
165    ///
166    /// [`GitFetchStatus::NoRemoteRef`] is returned if ref doesn't exist. Note
167    /// that `git` only returns one failed ref at a time.
168    pub(crate) fn spawn_fetch(
169        &self,
170        remote_name: &RemoteName,
171        refspecs: &[RefSpec],
172        negative_refspecs: &[NegativeRefSpec],
173        callback: &mut dyn GitSubprocessCallback,
174        depth: Option<NonZeroU32>,
175    ) -> Result<GitFetchStatus, GitSubprocessError> {
176        if refspecs.is_empty() {
177            return Ok(GitFetchStatus::Updates(GitRefUpdates::default()));
178        }
179        let mut command = self.create_command();
180        command.stdout(Stdio::piped());
181        // attempt to prune stale refs with --prune
182        // --no-write-fetch-head ensures our request is invisible to other parties
183        command.args(["fetch", "--porcelain", "--prune", "--no-write-fetch-head"]);
184        if callback.needs_progress() {
185            command.arg("--progress");
186        }
187        if let Some(d) = depth {
188            command.arg(format!("--depth={d}"));
189        }
190        // Tags should be fetched explicitly by the refspecs
191        command.arg("--no-tags");
192        command.arg("--").arg(remote_name.as_str());
193        command.args(
194            refspecs
195                .iter()
196                .map(|x| x.to_git_format())
197                .chain(negative_refspecs.iter().map(|x| x.to_git_format())),
198        );
199
200        let output = wait_with_progress(self.spawn_cmd(command)?, callback)?;
201
202        parse_git_fetch_output(&output)
203    }
204
205    /// Prune particular branches
206    pub(crate) fn spawn_branch_prune(
207        &self,
208        branches_to_prune: &[String],
209    ) -> Result<(), GitSubprocessError> {
210        if branches_to_prune.is_empty() {
211            return Ok(());
212        }
213        tracing::debug!(?branches_to_prune, "pruning branches");
214        let mut command = self.create_command();
215        command.stdout(Stdio::null());
216        command.args(["branch", "--remotes", "--delete", "--"]);
217        command.args(branches_to_prune);
218
219        let output = wait_with_output(self.spawn_cmd(command)?)?;
220
221        // we name the type to make sure that it is not meant to be used
222        let () = parse_git_branch_prune_output(output)?;
223
224        Ok(())
225    }
226
227    /// How we retrieve the remote's default branch:
228    ///
229    /// `git remote show <remote_name>`
230    ///
231    /// dumps a lot of information about the remote, with a line such as:
232    /// `  HEAD branch: <default_branch>`
233    pub(crate) fn spawn_remote_show(
234        &self,
235        remote_name: &RemoteName,
236    ) -> Result<Option<RefNameBuf>, GitSubprocessError> {
237        let mut command = self.create_command();
238        command.stdout(Stdio::piped());
239        command.args(["remote", "show", "--", remote_name.as_str()]);
240        let output = wait_with_output(self.spawn_cmd(command)?)?;
241
242        let output = parse_git_remote_show_output(output)?;
243
244        // find the HEAD branch line in the output
245        let maybe_branch = parse_git_remote_show_default_branch(&output.stdout)?;
246        Ok(maybe_branch.map(Into::into))
247    }
248
249    /// Push references to git
250    ///
251    /// All pushes are forced, using --force-with-lease to perform a test&set
252    /// operation on the remote repository
253    ///
254    /// Return tuple with
255    ///     1. refs that failed to push
256    ///     2. refs that succeeded to push
257    pub(crate) fn spawn_push(
258        &self,
259        remote_name: &RemoteName,
260        references: &[RefToPush],
261        callback: &mut dyn GitSubprocessCallback,
262        options: &GitPushOptions,
263    ) -> Result<GitPushStats, GitSubprocessError> {
264        let mut command = self.create_command();
265        command.stdout(Stdio::piped());
266        // Currently jj does not support commit hooks, so we prevent git from running
267        // them
268        //
269        // https://github.com/jj-vcs/jj/issues/3577 and https://github.com/jj-vcs/jj/issues/405
270        // offer more context
271        command.args(["push", "--porcelain", "--no-verify"]);
272        if callback.needs_progress() {
273            command.arg("--progress");
274        }
275        command.args(
276            options
277                .remote_push_options
278                .iter()
279                .map(|option| format!("--push-option={option}")),
280        );
281        command.args(
282            references
283                .iter()
284                .map(|reference| format!("--force-with-lease={}", reference.to_git_lease())),
285        );
286        command.args(["--", remote_name.as_str()]);
287        // with --force-with-lease we cannot have the forced refspec,
288        // as it ignores the lease
289        command.args(
290            references
291                .iter()
292                .map(|r| r.refspec.to_git_format_not_forced()),
293        );
294
295        let output = wait_with_progress(self.spawn_cmd(command)?, callback)?;
296
297        parse_git_push_output(output)
298    }
299}
300
301/// Generate a GitSubprocessError::ExternalGitError if the stderr output was not
302/// recognizable
303fn external_git_error(stderr: &[u8]) -> GitSubprocessError {
304    GitSubprocessError::External(format!(
305        "External git program failed:\n{}",
306        stderr.to_str_lossy()
307    ))
308}
309
310const ERROR_PREFIXES: &[&[u8]] = &[
311    // error_builtin() in usage.c
312    b"error: ",
313    // die_message_builtin() in usage.c
314    b"fatal: ",
315    // usage_builtin() in usage.c
316    b"usage: ",
317    // handle_option() in git.c
318    b"unknown option: ",
319];
320
321/// Parse no such remote errors output from git
322///
323/// Returns the remote that wasn't found
324///
325/// To say this, git prints out a lot of things, but the first line is of the
326/// form:
327/// `fatal: '<remote>' does not appear to be a git repository`
328/// or
329/// `fatal: '<remote>': Could not resolve host: invalid-remote`
330fn parse_no_such_remote(stderr: &[u8]) -> Option<String> {
331    let first_line = stderr.lines().next()?;
332    let suffix = first_line
333        .strip_prefix(b"fatal: '")
334        .or_else(|| first_line.strip_prefix(b"fatal: unable to access '"))?;
335
336    suffix
337        .strip_suffix(b"' does not appear to be a git repository")
338        .or_else(|| suffix.strip_suffix(b"': Could not resolve host: invalid-remote"))
339        .map(|remote| remote.to_str_lossy().into_owned())
340}
341
342/// Parse error from refspec not present on the remote
343///
344/// This returns
345///     Some(local_ref) that wasn't found by the remote
346///     None if this wasn't the error
347///
348/// On git fetch even though --prune is specified, if a particular
349/// refspec is asked for but not present in the remote, git will error out.
350///
351/// Git only reports one of these errors at a time, so we only look at the first
352/// line
353///
354/// The first line is of the form:
355/// `fatal: couldn't find remote ref refs/heads/<ref>`
356fn parse_no_remote_ref(stderr: &[u8]) -> Option<String> {
357    let first_line = stderr.lines().next()?;
358    first_line
359        .strip_prefix(b"fatal: couldn't find remote ref ")
360        .map(|refname| refname.to_str_lossy().into_owned())
361}
362
363/// Parse remote tracking branch not found
364///
365/// This returns true if the error was detected
366///
367/// if a branch is asked for but is not present, jj will detect it post-hoc
368/// so, we want to ignore these particular errors with git
369///
370/// The first line is of the form:
371/// `error: remote-tracking branch '<branch>' not found`
372fn parse_no_remote_tracking_branch(stderr: &[u8]) -> Option<String> {
373    let first_line = stderr.lines().next()?;
374
375    let suffix = first_line.strip_prefix(b"error: remote-tracking branch '")?;
376
377    suffix
378        .strip_suffix(b"' not found.")
379        .or_else(|| suffix.strip_suffix(b"' not found"))
380        .map(|branch| branch.to_str_lossy().into_owned())
381}
382
383/// Parse unknown options
384///
385/// Return the unknown option
386///
387/// If a user is running a very old git version, our commands may fail
388/// We want to give a good error in this case
389fn parse_unknown_option(stderr: &[u8]) -> Option<String> {
390    let first_line = stderr.lines().next()?;
391    first_line
392        .strip_prefix(b"unknown option: --")
393        .or(first_line
394            .strip_prefix(b"error: unknown option `")
395            .and_then(|s| s.strip_suffix(b"'")))
396        .map(|s| s.to_str_lossy().into())
397}
398
399/// Status of underlying `git fetch` operation.
400#[derive(Clone, Debug)]
401pub enum GitFetchStatus {
402    /// Successfully fetched refs. There may be refs that couldn't be updated.
403    Updates(GitRefUpdates),
404    /// Fully-qualified ref that failed to fetch.
405    ///
406    /// Note that `git fetch` only returns one error at a time.
407    NoRemoteRef(String),
408}
409
410fn parse_git_fetch_output(output: &Output) -> Result<GitFetchStatus, GitSubprocessError> {
411    if output.status.success() {
412        let updates = parse_ref_updates(&output.stdout)?;
413        return Ok(GitFetchStatus::Updates(updates));
414    }
415
416    // There are some git errors we want to parse out
417    if let Some(option) = parse_unknown_option(&output.stderr) {
418        return Err(GitSubprocessError::UnsupportedGitOption(option));
419    }
420
421    if let Some(remote) = parse_no_such_remote(&output.stderr) {
422        return Err(GitSubprocessError::NoSuchRepository(remote));
423    }
424
425    if let Some(refspec) = parse_no_remote_ref(&output.stderr) {
426        return Ok(GitFetchStatus::NoRemoteRef(refspec));
427    }
428
429    let updates = parse_ref_updates(&output.stdout)?;
430    if !updates.rejected.is_empty() || parse_no_remote_tracking_branch(&output.stderr).is_some() {
431        Ok(GitFetchStatus::Updates(updates))
432    } else {
433        Err(external_git_error(&output.stderr))
434    }
435}
436
437/// Local changes made by `git fetch`.
438#[derive(Clone, Debug, Default)]
439pub struct GitRefUpdates {
440    /// Git ref `(name, (old_oid, new_oid))`s that are successfully updated.
441    ///
442    /// `old_oid`/`new_oid` may be null or point to non-commit objects such as
443    /// tags.
444    #[cfg_attr(not(test), expect(dead_code))] // unused as of now
445    pub updated: Vec<(GitRefNameBuf, Diff<gix::ObjectId>)>,
446    /// Git ref `(name, (old_oid, new_oid)`s that are rejected or failed to
447    /// update.
448    pub rejected: Vec<(GitRefNameBuf, Diff<gix::ObjectId>)>,
449}
450
451/// Parses porcelain output of `git fetch`.
452fn parse_ref_updates(stdout: &[u8]) -> Result<GitRefUpdates, GitSubprocessError> {
453    let mut updated = vec![];
454    let mut rejected = vec![];
455    for (i, line) in stdout.lines().enumerate() {
456        let parse_err = |message: &str| {
457            GitSubprocessError::External(format!(
458                "Line {line_no}: {message}: {line}",
459                line_no = i + 1,
460                line = BStr::new(line)
461            ))
462        };
463        // <flag> <old-object-id> <new-object-id> <local-reference>
464        // (<flag> may be space)
465        let mut line_bytes = line.iter();
466        let flag = *line_bytes.next().ok_or_else(|| parse_err("empty line"))?;
467        if line_bytes.next() != Some(&b' ') {
468            return Err(parse_err("no flag separator found"));
469        }
470        let [old_oid, new_oid, name] = line_bytes
471            .as_slice()
472            .splitn(3, |&b| b == b' ')
473            .collect_array()
474            .ok_or_else(|| parse_err("unexpected number of columns"))?;
475        let name: GitRefNameBuf = str::from_utf8(name)
476            .map_err(|_| parse_err("non-UTF-8 ref name"))?
477            .into();
478        let old_oid = gix::ObjectId::from_hex(old_oid).map_err(|_| parse_err("invalid old oid"))?;
479        let new_oid = gix::ObjectId::from_hex(new_oid).map_err(|_| parse_err("invalid new oid"))?;
480        let oid_diff = Diff::new(old_oid, new_oid);
481        match flag {
482            // ' ' for a successfully fetched fast-forward
483            // '+' for a successful forced update
484            // '-' for a successfully pruned ref
485            // 't' for a successful tag update
486            // '*' for a successfully fetched new ref
487            b' ' | b'+' | b'-' | b't' | b'*' => updated.push((name, oid_diff)),
488            // '!' for a ref that was rejected or failed to update
489            b'!' => rejected.push((name, oid_diff)),
490            // '=' for a ref that was up to date and did not need fetching
491            // (included when --verbose)
492            b'=' => {}
493            _ => return Err(parse_err("unknown flag")),
494        }
495    }
496    Ok(GitRefUpdates { updated, rejected })
497}
498
499fn parse_git_branch_prune_output(output: Output) -> Result<(), GitSubprocessError> {
500    if output.status.success() {
501        return Ok(());
502    }
503
504    // There are some git errors we want to parse out
505    if let Some(option) = parse_unknown_option(&output.stderr) {
506        return Err(GitSubprocessError::UnsupportedGitOption(option));
507    }
508
509    if parse_no_remote_tracking_branch(&output.stderr).is_some() {
510        return Ok(());
511    }
512
513    Err(external_git_error(&output.stderr))
514}
515
516fn parse_git_remote_show_output(output: Output) -> Result<Output, GitSubprocessError> {
517    if output.status.success() {
518        return Ok(output);
519    }
520
521    // There are some git errors we want to parse out
522    if let Some(option) = parse_unknown_option(&output.stderr) {
523        return Err(GitSubprocessError::UnsupportedGitOption(option));
524    }
525
526    if let Some(remote) = parse_no_such_remote(&output.stderr) {
527        return Err(GitSubprocessError::NoSuchRepository(remote));
528    }
529
530    Err(external_git_error(&output.stderr))
531}
532
533fn parse_git_remote_show_default_branch(
534    stdout: &[u8],
535) -> Result<Option<String>, GitSubprocessError> {
536    stdout
537        .lines()
538        .map(|x| x.trim())
539        .find(|x| x.starts_with_str("HEAD branch:"))
540        .inspect(|x| tracing::debug!(line = ?x.to_str_lossy(), "default branch"))
541        .and_then(|x| x.split_str(" ").last().map(|y| y.trim()))
542        .filter(|branch_name| branch_name != b"(unknown)")
543        .map(|branch_name| branch_name.to_str())
544        .transpose()
545        .map_err(|e| GitSubprocessError::External(format!("git remote output is not utf-8: {e:?}")))
546        .map(|b| b.map(|x| x.to_string()))
547}
548
549// git-push porcelain has the following format (per line)
550// `<flag>\t<from>:<to>\t<summary> (<reason>)`
551//
552// <flag> is one of:
553//     ' ' for a successfully pushed fast-forward;
554//      + for a successful forced update
555//      - for a successfully deleted ref
556//      * for a successfully pushed new ref
557//      !  for a ref that was rejected or failed to push; and
558//      =  for a ref that was up to date and did not need pushing.
559//
560// <from>:<to> is the refspec
561//
562// <summary> is extra info (commit ranges or reason for rejected)
563//
564// <reason> is a human-readable explanation
565fn parse_ref_pushes(stdout: &[u8]) -> Result<GitPushStats, GitSubprocessError> {
566    if !stdout.starts_with(b"To ") {
567        return Err(GitSubprocessError::External(format!(
568            "Git push output unfamiliar:\n{}",
569            stdout.to_str_lossy()
570        )));
571    }
572
573    let mut push_stats = GitPushStats::default();
574    for (idx, line) in stdout
575        .lines()
576        .skip(1)
577        .take_while(|line| line != b"Done")
578        .enumerate()
579    {
580        tracing::debug!("response #{idx}: {}", line.to_str_lossy());
581        let [flag, reference, summary] = line.split_str("\t").collect_array().ok_or_else(|| {
582            GitSubprocessError::External(format!(
583                "Line #{idx} of git-push has unknown format: {}",
584                line.to_str_lossy()
585            ))
586        })?;
587        let full_refspec = reference
588            .to_str()
589            .map_err(|e| {
590                format!(
591                    "Line #{} of git-push has non-utf8 refspec {}: {}",
592                    idx,
593                    reference.to_str_lossy(),
594                    e
595                )
596            })
597            .map_err(GitSubprocessError::External)?;
598
599        let reference: GitRefNameBuf = full_refspec
600            .split_once(':')
601            .map(|(_refname, reference)| reference.into())
602            .ok_or_else(|| {
603                GitSubprocessError::External(format!(
604                    "Line #{idx} of git-push has full refspec without named ref: {full_refspec}"
605                ))
606            })?;
607
608        match flag {
609            // ' ' for a successfully pushed fast-forward;
610            //  + for a successful forced update
611            //  - for a successfully deleted ref
612            //  * for a successfully pushed new ref
613            //  =  for a ref that was up to date and did not need pushing.
614            b"+" | b"-" | b"*" | b"=" | b" " => {
615                push_stats.pushed.push(reference);
616            }
617            // ! for a ref that was rejected or failed to push; and
618            b"!" => {
619                if let Some(reason) = summary.strip_prefix(b"[remote rejected]") {
620                    let reason = reason
621                        .strip_prefix(b" (")
622                        .and_then(|r| r.strip_suffix(b")"))
623                        .map(|x| x.to_str_lossy().into_owned());
624                    push_stats.remote_rejected.push((reference, reason));
625                } else {
626                    let reason = summary
627                        .split_once_str("]")
628                        .and_then(|(_, reason)| reason.strip_prefix(b" ("))
629                        .and_then(|r| r.strip_suffix(b")"))
630                        .map(|x| x.to_str_lossy().into_owned());
631                    push_stats.rejected.push((reference, reason));
632                }
633            }
634            unknown => {
635                return Err(GitSubprocessError::External(format!(
636                    "Line #{} of git-push starts with an unknown flag '{}': '{}'",
637                    idx,
638                    unknown.to_str_lossy(),
639                    line.to_str_lossy()
640                )));
641            }
642        }
643    }
644
645    Ok(push_stats)
646}
647
648// on Ok, return a tuple with
649//  1. list of failed references from test and set
650//  2. list of successful references pushed
651fn parse_git_push_output(output: Output) -> Result<GitPushStats, GitSubprocessError> {
652    if output.status.success() {
653        let ref_pushes = parse_ref_pushes(&output.stdout)?;
654        return Ok(ref_pushes);
655    }
656
657    if let Some(option) = parse_unknown_option(&output.stderr) {
658        return Err(GitSubprocessError::UnsupportedGitOption(option));
659    }
660
661    if let Some(remote) = parse_no_such_remote(&output.stderr) {
662        return Err(GitSubprocessError::NoSuchRepository(remote));
663    }
664
665    if output
666        .stderr
667        .lines()
668        .any(|line| line.starts_with(b"error: failed to push some refs to "))
669    {
670        parse_ref_pushes(&output.stdout)
671    } else {
672        Err(external_git_error(&output.stderr))
673    }
674}
675
676/// Handles Git command outputs.
677pub trait GitSubprocessCallback {
678    /// Whether to request progress information.
679    fn needs_progress(&self) -> bool;
680
681    /// Progress of local and remote operations.
682    fn progress(&mut self, progress: &GitProgress) -> io::Result<()>;
683
684    /// Single-line message that doesn't look like remote sideband or error.
685    ///
686    /// This may include authentication request from credential helpers.
687    fn local_sideband(
688        &mut self,
689        message: &[u8],
690        term: Option<GitSidebandLineTerminator>,
691    ) -> io::Result<()>;
692
693    /// Single-line sideband message received from remote.
694    fn remote_sideband(
695        &mut self,
696        message: &[u8],
697        term: Option<GitSidebandLineTerminator>,
698    ) -> io::Result<()>;
699}
700
701/// Newline character that terminates sideband message line.
702#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
703#[repr(u8)]
704pub enum GitSidebandLineTerminator {
705    /// CR to remain on the same line.
706    Cr = b'\r',
707    /// LF to move to the next line.
708    Lf = b'\n',
709}
710
711impl GitSidebandLineTerminator {
712    /// Returns byte representation.
713    pub fn as_byte(self) -> u8 {
714        self as u8
715    }
716}
717
718fn wait_with_output(child: Child) -> Result<Output, GitSubprocessError> {
719    child.wait_with_output().map_err(GitSubprocessError::Wait)
720}
721
722/// Like `wait_with_output()`, but also emits sideband data through callback.
723///
724/// Git remotes can send custom messages on fetch and push, which the `git`
725/// command prepends with `remote: `.
726///
727/// For instance, these messages can provide URLs to create Pull Requests
728/// e.g.:
729/// ```ignore
730/// $ jj git push -c @
731/// [...]
732/// remote:
733/// remote: Create a pull request for 'branch' on GitHub by visiting:
734/// remote:      https://github.com/user/repo/pull/new/branch
735/// remote:
736/// ```
737///
738/// The returned `stderr` content does not include sideband messages.
739fn wait_with_progress(
740    mut child: Child,
741    callback: &mut dyn GitSubprocessCallback,
742) -> Result<Output, GitSubprocessError> {
743    let (stdout, stderr) = thread::scope(|s| -> io::Result<_> {
744        drop(child.stdin.take());
745        let mut child_stdout = child.stdout.take().expect("stdout should be piped");
746        let mut child_stderr = child.stderr.take().expect("stderr should be piped");
747        let thread = s.spawn(move || -> io::Result<_> {
748            let mut buf = Vec::new();
749            child_stdout.read_to_end(&mut buf)?;
750            Ok(buf)
751        });
752        let stderr = read_to_end_with_progress(&mut child_stderr, callback)?;
753        let stdout = thread.join().expect("reader thread wouldn't panic")?;
754        Ok((stdout, stderr))
755    })
756    .map_err(GitSubprocessError::Wait)?;
757    let status = child.wait().map_err(GitSubprocessError::Wait)?;
758    Ok(Output {
759        status,
760        stdout,
761        stderr,
762    })
763}
764
765/// Progress of underlying `git` command operation.
766#[derive(Clone, Debug, Default)]
767pub struct GitProgress {
768    /// `(frac, total)` of "Resolving deltas".
769    pub deltas: (u64, u64),
770    /// `(frac, total)` of "Receiving objects".
771    pub objects: (u64, u64),
772    /// `(frac, total)` of remote "Counting objects".
773    pub counted_objects: (u64, u64),
774    /// `(frac, total)` of remote "Compressing objects".
775    pub compressed_objects: (u64, u64),
776}
777
778// TODO: maybe let callers print each field separately and remove overall()?
779impl GitProgress {
780    /// Overall progress normalized to 0 to 1 range.
781    pub fn overall(&self) -> f32 {
782        if self.total() != 0 {
783            self.fraction() as f32 / self.total() as f32
784        } else {
785            0.0
786        }
787    }
788
789    fn fraction(&self) -> u64 {
790        self.objects.0 + self.deltas.0 + self.counted_objects.0 + self.compressed_objects.0
791    }
792
793    fn total(&self) -> u64 {
794        self.objects.1 + self.deltas.1 + self.counted_objects.1 + self.compressed_objects.1
795    }
796}
797
798fn read_to_end_with_progress<R: Read>(
799    src: R,
800    callback: &mut dyn GitSubprocessCallback,
801) -> io::Result<Vec<u8>> {
802    let mut reader = BufReader::new(src);
803    let mut data = Vec::new();
804    let mut progress = GitProgress::default();
805
806    loop {
807        // progress sent through sideband channel may be terminated by \r
808        let start = data.len();
809        read_until_cr_or_lf(&mut reader, &mut data)?;
810        let line = &data[start..];
811        if line.is_empty() {
812            break;
813        }
814
815        // capture error messages which will be interpreted by caller
816        if ERROR_PREFIXES.iter().any(|prefix| line.starts_with(prefix)) {
817            reader.read_to_end(&mut data)?;
818            break;
819        }
820
821        // io::Error coming from callback shouldn't be propagated as an error of
822        // "read" operation. The error is suppressed for now.
823        // TODO: maybe intercept "push" progress? (see builtin/pack-objects.c)
824        if update_progress(line, &mut progress.objects, b"Receiving objects:")
825            || update_progress(line, &mut progress.deltas, b"Resolving deltas:")
826            || update_progress(
827                line,
828                &mut progress.counted_objects,
829                b"remote: Counting objects:",
830            )
831            || update_progress(
832                line,
833                &mut progress.compressed_objects,
834                b"remote: Compressing objects:",
835            )
836        {
837            callback.progress(&progress).ok();
838            data.truncate(start);
839        } else if let Some(message) = line.strip_prefix(b"remote: ") {
840            let (body, term) = trim_sideband_line(message);
841            callback.remote_sideband(body, term).ok();
842            data.truncate(start);
843        } else {
844            let (body, term) = trim_sideband_line(line);
845            callback.local_sideband(body, term).ok();
846            data.truncate(start);
847        }
848    }
849    Ok(data)
850}
851
852fn update_progress(line: &[u8], progress: &mut (u64, u64), prefix: &[u8]) -> bool {
853    if let Some(line) = line.strip_prefix(prefix) {
854        if let Some((frac, total)) = read_progress_line(line) {
855            *progress = (frac, total);
856        }
857
858        true
859    } else {
860        false
861    }
862}
863
864fn read_until_cr_or_lf<R: io::BufRead + ?Sized>(
865    reader: &mut R,
866    dest_buf: &mut Vec<u8>,
867) -> io::Result<()> {
868    loop {
869        let data = match reader.fill_buf() {
870            Ok(data) => data,
871            Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
872            Err(err) => return Err(err),
873        };
874        let (n, found) = match data.iter().position(|&b| matches!(b, b'\r' | b'\n')) {
875            Some(i) => (i + 1, true),
876            None => (data.len(), false),
877        };
878
879        dest_buf.extend_from_slice(&data[..n]);
880        reader.consume(n);
881
882        if found || n == 0 {
883            return Ok(());
884        }
885    }
886}
887
888/// Read progress lines of the form: `<text> (<frac>/<total>)`
889/// Ensures that frac < total
890fn read_progress_line(line: &[u8]) -> Option<(u64, u64)> {
891    // isolate the part between parenthesis
892    let (_prefix, suffix) = line.split_once_str("(")?;
893    let (fraction, _suffix) = suffix.split_once_str(")")?;
894
895    // split over the '/'
896    let (frac_str, total_str) = fraction.split_once_str("/")?;
897
898    // parse to integers
899    let frac = frac_str.to_str().ok()?.parse().ok()?;
900    let total = total_str.to_str().ok()?.parse().ok()?;
901    (frac <= total).then_some((frac, total))
902}
903
904/// Removes trailing spaces from sideband line, which may be padded by the `git`
905/// CLI in order to clear the previous progress line.
906fn trim_sideband_line(line: &[u8]) -> (&[u8], Option<GitSidebandLineTerminator>) {
907    let (body, term) = match line {
908        [body @ .., b'\r'] => (body, Some(GitSidebandLineTerminator::Cr)),
909        [body @ .., b'\n'] => (body, Some(GitSidebandLineTerminator::Lf)),
910        _ => (line, None),
911    };
912    let n = body.iter().rev().take_while(|&&b| b == b' ').count();
913    (&body[..body.len() - n], term)
914}
915
916#[cfg(test)]
917mod test {
918    use std::process::ExitStatus;
919
920    use assert_matches::assert_matches;
921    use bstr::BString;
922    use indoc::formatdoc;
923    use indoc::indoc;
924
925    use super::*;
926
927    const SAMPLE_NO_SUCH_REPOSITORY_ERROR: &[u8] =
928        br###"fatal: unable to access 'origin': Could not resolve host: invalid-remote
929fatal: Could not read from remote repository.
930
931Please make sure you have the correct access rights
932and the repository exists. "###;
933    const SAMPLE_NO_SUCH_REMOTE_ERROR: &[u8] =
934        br###"fatal: 'origin' does not appear to be a git repository
935fatal: Could not read from remote repository.
936
937Please make sure you have the correct access rights
938and the repository exists. "###;
939    const SAMPLE_NO_REMOTE_REF_ERROR: &[u8] = b"fatal: couldn't find remote ref refs/heads/noexist";
940    const SAMPLE_NO_REMOTE_TRACKING_BRANCH_ERROR: &[u8] =
941        b"error: remote-tracking branch 'bookmark' not found";
942    const SAMPLE_PUSH_REFS_PORCELAIN_OUTPUT: &[u8] = b"To origin
943*\tdeadbeef:refs/heads/bookmark1\t[new branch]
944+\tdeadbeef:refs/heads/bookmark2\tabcd..dead
945-\tdeadbeef:refs/heads/bookmark3\t[deleted branch]
946 \tdeadbeef:refs/heads/bookmark4\tabcd..dead
947=\tdeadbeef:refs/heads/bookmark5\tabcd..abcd
948!\tdeadbeef:refs/heads/bookmark6\t[rejected] (failure lease)
949!\tdeadbeef:refs/heads/bookmark7\t[rejected]
950!\tdeadbeef:refs/heads/bookmark8\t[remote rejected] (hook failure)
951!\tdeadbeef:refs/heads/bookmark9\t[remote rejected]
952Done";
953    const SAMPLE_OK_STDERR: &[u8] = b"";
954
955    #[derive(Debug, Default)]
956    struct GitSubprocessCapture {
957        progress: Vec<GitProgress>,
958        local_sideband: Vec<BString>,
959        remote_sideband: Vec<BString>,
960    }
961
962    impl GitSubprocessCallback for GitSubprocessCapture {
963        fn needs_progress(&self) -> bool {
964            true
965        }
966
967        fn progress(&mut self, progress: &GitProgress) -> io::Result<()> {
968            self.progress.push(progress.clone());
969            Ok(())
970        }
971
972        fn local_sideband(
973            &mut self,
974            message: &[u8],
975            term: Option<GitSidebandLineTerminator>,
976        ) -> io::Result<()> {
977            self.local_sideband.push(message.into());
978            if let Some(term) = term {
979                self.local_sideband.push([term.as_byte()].into());
980            }
981            Ok(())
982        }
983
984        fn remote_sideband(
985            &mut self,
986            message: &[u8],
987            term: Option<GitSidebandLineTerminator>,
988        ) -> io::Result<()> {
989            self.remote_sideband.push(message.into());
990            if let Some(term) = term {
991                self.remote_sideband.push([term.as_byte()].into());
992            }
993            Ok(())
994        }
995    }
996
997    fn exit_status_from_code(code: u8) -> ExitStatus {
998        #[cfg(unix)]
999        use std::os::unix::process::ExitStatusExt as _; // i32
1000        #[cfg(windows)]
1001        use std::os::windows::process::ExitStatusExt as _; // u32
1002        ExitStatus::from_raw(code.into())
1003    }
1004
1005    #[test]
1006    fn test_parse_no_such_remote() {
1007        assert_eq!(
1008            parse_no_such_remote(SAMPLE_NO_SUCH_REPOSITORY_ERROR),
1009            Some("origin".to_string())
1010        );
1011        assert_eq!(
1012            parse_no_such_remote(SAMPLE_NO_SUCH_REMOTE_ERROR),
1013            Some("origin".to_string())
1014        );
1015        assert_eq!(parse_no_such_remote(SAMPLE_NO_REMOTE_REF_ERROR), None);
1016        assert_eq!(
1017            parse_no_such_remote(SAMPLE_NO_REMOTE_TRACKING_BRANCH_ERROR),
1018            None
1019        );
1020        assert_eq!(
1021            parse_no_such_remote(SAMPLE_PUSH_REFS_PORCELAIN_OUTPUT),
1022            None
1023        );
1024        assert_eq!(parse_no_such_remote(SAMPLE_OK_STDERR), None);
1025    }
1026
1027    #[test]
1028    fn test_parse_no_remote_ref() {
1029        assert_eq!(parse_no_remote_ref(SAMPLE_NO_SUCH_REPOSITORY_ERROR), None);
1030        assert_eq!(parse_no_remote_ref(SAMPLE_NO_SUCH_REMOTE_ERROR), None);
1031        assert_eq!(
1032            parse_no_remote_ref(SAMPLE_NO_REMOTE_REF_ERROR),
1033            Some("refs/heads/noexist".to_string())
1034        );
1035        assert_eq!(
1036            parse_no_remote_ref(SAMPLE_NO_REMOTE_TRACKING_BRANCH_ERROR),
1037            None
1038        );
1039        assert_eq!(parse_no_remote_ref(SAMPLE_PUSH_REFS_PORCELAIN_OUTPUT), None);
1040        assert_eq!(parse_no_remote_ref(SAMPLE_OK_STDERR), None);
1041    }
1042
1043    #[test]
1044    fn test_parse_no_remote_tracking_branch() {
1045        assert_eq!(
1046            parse_no_remote_tracking_branch(SAMPLE_NO_SUCH_REPOSITORY_ERROR),
1047            None
1048        );
1049        assert_eq!(
1050            parse_no_remote_tracking_branch(SAMPLE_NO_SUCH_REMOTE_ERROR),
1051            None
1052        );
1053        assert_eq!(
1054            parse_no_remote_tracking_branch(SAMPLE_NO_REMOTE_REF_ERROR),
1055            None
1056        );
1057        assert_eq!(
1058            parse_no_remote_tracking_branch(SAMPLE_NO_REMOTE_TRACKING_BRANCH_ERROR),
1059            Some("bookmark".to_string())
1060        );
1061        assert_eq!(
1062            parse_no_remote_tracking_branch(SAMPLE_PUSH_REFS_PORCELAIN_OUTPUT),
1063            None
1064        );
1065        assert_eq!(parse_no_remote_tracking_branch(SAMPLE_OK_STDERR), None);
1066    }
1067
1068    #[test]
1069    fn test_parse_git_fetch_output_rejected() {
1070        // `git fetch` exists with 1 if there are rejected updates.
1071        let output = Output {
1072            status: exit_status_from_code(1),
1073            stdout: b"! d4d535f1d5795c6027f2872b24b7268ece294209 baad96fead6cdc20d47c55a4069c82952f9ac62c refs/remotes/origin/b\n".to_vec(),
1074            stderr: b"".to_vec(),
1075        };
1076        assert_matches!(
1077            parse_git_fetch_output(&output),
1078            Ok(GitFetchStatus::Updates(updates))
1079                if updates.updated.is_empty() && updates.rejected.len() == 1
1080        );
1081    }
1082
1083    #[test]
1084    fn test_parse_ref_updates_sample() {
1085        let sample = indoc! {b"
1086            * 0000000000000000000000000000000000000000 e80d998ab04be7caeac3a732d74b1708aa3d8b26 refs/remotes/origin/a1
1087              ebeb70d8c5f972275f0a22f7af6bc9ddb175ebd9 9175cb3250fd266fe46dcc13664b255a19234286 refs/remotes/origin/a2
1088            + c8303692b8e2f0326cd33873a157b4fa69d54774 798c5e2435e1442946db90a50d47ab90f40c60b7 refs/remotes/origin/a3
1089            - b2ea51c027e11c0f2871cce2a52e648e194df771 0000000000000000000000000000000000000000 refs/remotes/origin/a4
1090            ! d4d535f1d5795c6027f2872b24b7268ece294209 baad96fead6cdc20d47c55a4069c82952f9ac62c refs/remotes/origin/b
1091            = f8e7139764d76132234c13210b6f0abe6b1d9bf6 f8e7139764d76132234c13210b6f0abe6b1d9bf6 refs/remotes/upstream/c
1092            * 0000000000000000000000000000000000000000 fd5b6a095a77575c94fad4164ab580331316c374 refs/tags/v1.0
1093            t 0000000000000000000000000000000000000000 3262fedde0224462bb6ac3015dabc427a4f98316 refs/tags/v2.0
1094        "};
1095        insta::assert_debug_snapshot!(parse_ref_updates(sample).unwrap(), @r#"
1096        GitRefUpdates {
1097            updated: [
1098                (
1099                    GitRefNameBuf(
1100                        "refs/remotes/origin/a1",
1101                    ),
1102                    Diff {
1103                        before: Sha1(0000000000000000000000000000000000000000),
1104                        after: Sha1(e80d998ab04be7caeac3a732d74b1708aa3d8b26),
1105                    },
1106                ),
1107                (
1108                    GitRefNameBuf(
1109                        "refs/remotes/origin/a2",
1110                    ),
1111                    Diff {
1112                        before: Sha1(ebeb70d8c5f972275f0a22f7af6bc9ddb175ebd9),
1113                        after: Sha1(9175cb3250fd266fe46dcc13664b255a19234286),
1114                    },
1115                ),
1116                (
1117                    GitRefNameBuf(
1118                        "refs/remotes/origin/a3",
1119                    ),
1120                    Diff {
1121                        before: Sha1(c8303692b8e2f0326cd33873a157b4fa69d54774),
1122                        after: Sha1(798c5e2435e1442946db90a50d47ab90f40c60b7),
1123                    },
1124                ),
1125                (
1126                    GitRefNameBuf(
1127                        "refs/remotes/origin/a4",
1128                    ),
1129                    Diff {
1130                        before: Sha1(b2ea51c027e11c0f2871cce2a52e648e194df771),
1131                        after: Sha1(0000000000000000000000000000000000000000),
1132                    },
1133                ),
1134                (
1135                    GitRefNameBuf(
1136                        "refs/tags/v1.0",
1137                    ),
1138                    Diff {
1139                        before: Sha1(0000000000000000000000000000000000000000),
1140                        after: Sha1(fd5b6a095a77575c94fad4164ab580331316c374),
1141                    },
1142                ),
1143                (
1144                    GitRefNameBuf(
1145                        "refs/tags/v2.0",
1146                    ),
1147                    Diff {
1148                        before: Sha1(0000000000000000000000000000000000000000),
1149                        after: Sha1(3262fedde0224462bb6ac3015dabc427a4f98316),
1150                    },
1151                ),
1152            ],
1153            rejected: [
1154                (
1155                    GitRefNameBuf(
1156                        "refs/remotes/origin/b",
1157                    ),
1158                    Diff {
1159                        before: Sha1(d4d535f1d5795c6027f2872b24b7268ece294209),
1160                        after: Sha1(baad96fead6cdc20d47c55a4069c82952f9ac62c),
1161                    },
1162                ),
1163            ],
1164        }
1165        "#);
1166    }
1167
1168    #[test]
1169    fn test_parse_ref_updates_malformed() {
1170        assert!(parse_ref_updates(b"").is_ok());
1171        assert!(parse_ref_updates(b"\n").is_err());
1172        assert!(parse_ref_updates(b"*\n").is_err());
1173        let oid = "0000000000000000000000000000000000000000";
1174        assert!(parse_ref_updates(format!("**{oid} {oid} name\n").as_bytes()).is_err());
1175    }
1176
1177    #[test]
1178    fn test_parse_ref_pushes() {
1179        assert!(parse_ref_pushes(SAMPLE_NO_SUCH_REPOSITORY_ERROR).is_err());
1180        assert!(parse_ref_pushes(SAMPLE_NO_SUCH_REMOTE_ERROR).is_err());
1181        assert!(parse_ref_pushes(SAMPLE_NO_REMOTE_REF_ERROR).is_err());
1182        assert!(parse_ref_pushes(SAMPLE_NO_REMOTE_TRACKING_BRANCH_ERROR).is_err());
1183        let GitPushStats {
1184            pushed,
1185            rejected,
1186            remote_rejected,
1187            unexported_bookmarks: _,
1188        } = parse_ref_pushes(SAMPLE_PUSH_REFS_PORCELAIN_OUTPUT).unwrap();
1189        assert_eq!(
1190            pushed,
1191            [
1192                "refs/heads/bookmark1",
1193                "refs/heads/bookmark2",
1194                "refs/heads/bookmark3",
1195                "refs/heads/bookmark4",
1196                "refs/heads/bookmark5",
1197            ]
1198            .map(GitRefNameBuf::from)
1199        );
1200        assert_eq!(
1201            rejected,
1202            vec![
1203                (
1204                    "refs/heads/bookmark6".into(),
1205                    Some("failure lease".to_string())
1206                ),
1207                ("refs/heads/bookmark7".into(), None),
1208            ]
1209        );
1210        assert_eq!(
1211            remote_rejected,
1212            vec![
1213                (
1214                    "refs/heads/bookmark8".into(),
1215                    Some("hook failure".to_string())
1216                ),
1217                ("refs/heads/bookmark9".into(), None)
1218            ]
1219        );
1220        assert!(parse_ref_pushes(SAMPLE_OK_STDERR).is_err());
1221    }
1222
1223    #[test]
1224    fn test_read_to_end_with_progress() {
1225        let read = |sample: &[u8]| {
1226            let mut callback = GitSubprocessCapture::default();
1227            let output = read_to_end_with_progress(&mut &sample[..], &mut callback).unwrap();
1228            (output, callback)
1229        };
1230        const DUMB_SUFFIX: &str = "        ";
1231        let sample = formatdoc! {"
1232            remote: line1{DUMB_SUFFIX}
1233            blah blah
1234            remote: line2.0{DUMB_SUFFIX}\rremote: line2.1{DUMB_SUFFIX}
1235            remote: line3{DUMB_SUFFIX}
1236            Resolving deltas: (12/24)
1237            fatal: some error message
1238            continues
1239        "};
1240
1241        let (output, callback) = read(sample.as_bytes());
1242        assert_eq!(callback.local_sideband, ["blah blah", "\n"]);
1243        assert_eq!(
1244            callback.remote_sideband,
1245            [
1246                "line1", "\n", "line2.0", "\r", "line2.1", "\n", "line3", "\n"
1247            ]
1248        );
1249        assert_eq!(output, b"fatal: some error message\ncontinues\n");
1250        insta::assert_debug_snapshot!(callback.progress, @"
1251        [
1252            GitProgress {
1253                deltas: (
1254                    12,
1255                    24,
1256                ),
1257                objects: (
1258                    0,
1259                    0,
1260                ),
1261                counted_objects: (
1262                    0,
1263                    0,
1264                ),
1265                compressed_objects: (
1266                    0,
1267                    0,
1268                ),
1269            },
1270        ]
1271        ");
1272
1273        // without last newline
1274        let (output, callback) = read(sample.as_bytes().trim_end());
1275        assert_eq!(
1276            callback.remote_sideband,
1277            [
1278                "line1", "\n", "line2.0", "\r", "line2.1", "\n", "line3", "\n"
1279            ]
1280        );
1281        assert_eq!(output, b"fatal: some error message\ncontinues");
1282    }
1283
1284    #[test]
1285    fn test_read_progress_line() {
1286        assert_eq!(
1287            read_progress_line(b"Receiving objects: (42/100)\r"),
1288            Some((42, 100))
1289        );
1290        assert_eq!(
1291            read_progress_line(b"Resolving deltas: (0/1000)\r"),
1292            Some((0, 1000))
1293        );
1294        assert_eq!(read_progress_line(b"Receiving objects: (420/100)\r"), None);
1295        assert_eq!(
1296            read_progress_line(b"remote: this is something else\n"),
1297            None
1298        );
1299        assert_eq!(read_progress_line(b"fatal: this is a git error\n"), None);
1300    }
1301
1302    #[test]
1303    fn test_parse_unknown_option() {
1304        assert_eq!(
1305            parse_unknown_option(b"unknown option: --abc").unwrap(),
1306            "abc".to_string()
1307        );
1308        assert_eq!(
1309            parse_unknown_option(b"error: unknown option `abc'").unwrap(),
1310            "abc".to_string()
1311        );
1312        assert!(parse_unknown_option(b"error: unknown option: 'abc'").is_none());
1313    }
1314
1315    #[test]
1316    fn test_initial_overall_progress_is_zero() {
1317        assert_eq!(GitProgress::default().overall(), 0.0);
1318    }
1319}