Skip to main content

grit_lib/porcelain/
pull.rs

1//! Fetch from a remote and integrate the configured upstream into the current branch.
2//!
3//! [`pull`] runs fetch, resolves the branch upstream via [`resolve_pull_integration_base`], then
4//! fast-forwards, merges, rebases local commits onto the upstream tip, or refuses
5//! when `--ff-only` / `pull.ff=only` forbids a merge commit. Rebase conflicts stop a
6//! resumable rebase session ([`start_rebase`]) with Git-compatible `.git/rebase-merge/`
7//! state, finished with [`continue_rebase`](crate::history::continue_rebase),
8//! [`skip_rebase`](crate::history::skip_rebase) or [`abort_rebase`](crate::history::abort_rebase).
9
10use crate::branch_tracking::resolve_pull_integration_base;
11use crate::change_id::{logical_change_id_for_commit, ChangeId};
12use crate::config::environment::Environment;
13use crate::config::ident_resolve::IdentRole;
14use crate::config::parse_git_config_maybe_bool;
15use crate::config::ConfigSet;
16use crate::error::Error;
17use crate::fetch_request::{configured_remote_for_pull, resolve_remote_argument};
18use crate::merge_base::{count_symmetric_ahead_behind, is_ancestor, merge_bases_first_vs_rest};
19use crate::objects::ObjectId;
20use crate::porcelain::checkout::checkout_between_trees;
21use crate::porcelain::checkout_reflog::checkout_reflog_identity;
22use crate::porcelain::merge::{
23    ensure_branch_tip, integrate_branch_with_options, predict_merge, update_checked_out_branch,
24    IntegrateBranchOptions, IntegrateBranchOutcome, MergeConflictDetail, MergePrediction,
25    MergePredictionOptions,
26};
27use crate::porcelain::stash::{
28    apply_operation_autostash, push_operation_autostash, restore_operation_autostash,
29};
30use crate::porcelain::worktree_guard::prepare_tree_checkout;
31use crate::remote::fetch::Progress;
32use crate::remote::transfer::{resolve_fetch_prune, FetchOptions, TagMode, TransferStats};
33use crate::remote::{HttpClientFactory, RemoteError};
34use crate::repo::Repository;
35use crate::revwalk::history::{
36    plan_rebase_check, start_rebase, ApplyOptions, EmptyPolicy, HistoryError, RebaseCheckPlan,
37    RebaseOntoOptions, RebaseOutcome, RebaseRequest, RebaseSessionError, RefScope, RewriteOptions,
38    StartRebaseOptions,
39};
40use crate::state::{resolve_head_for_repository, HeadState};
41use thiserror::Error;
42
43const INITIAL_PULL_REFLOG: &str = "initial pull";
44
45/// Resolved integration behaviour after config and CLI overrides.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47struct PullIntegration {
48    mode: PullMode,
49    allow_fast_forward: bool,
50}
51
52/// How [`pull`] integrates upstream commits when a fast-forward is not possible.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum PullMode {
55    /// Refuse when diverged history would require a merge commit.
56    FfOnly,
57    /// Merge (or fast-forward when possible).
58    Merge,
59    /// Rebase local commits onto the upstream tip via the history engine.
60    Rebase,
61}
62
63/// Options for [`pull`].
64#[derive(Clone, Default)]
65pub struct PullOptions {
66    /// Remote name override; when `None`, uses [`configured_remote_for_pull`].
67    pub remote: Option<String>,
68    /// Remote branch or refspec to integrate (`pull origin feature`); also see [`Self::fetch`].
69    pub branch: Option<String>,
70    /// When true, integration uses [`FETCH_HEAD`](https://git-scm.com/docs/git-fetch#_discussion) after fetch.
71    pub explicit_fetch: bool,
72    /// Stash dirty worktree/index changes around the integration (`pull --autostash`).
73    pub autostash: bool,
74    /// Fetch and report whether a fast-forward is possible without changing refs or the worktree.
75    pub check: bool,
76    /// CLI override for integration mode; when `None`, [`resolve_pull_mode`] applies config.
77    pub mode: Option<PullMode>,
78    /// Fetch options (tags, prune, depth, environment, …).
79    pub fetch: FetchOptions,
80}
81
82/// Dry-run result for [`PullOptions::check`].
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct PullCheck {
85    /// Commits on the current branch not reachable from the upstream tip.
86    pub ahead: usize,
87    /// Commits on the upstream tip not reachable from the current branch.
88    pub behind: usize,
89    /// Whether the branch could fast-forward to the upstream tip.
90    pub fast_forward_possible: bool,
91    /// Integration result if `--check` were not set.
92    pub would: PullResult,
93}
94
95/// High-level result of a successful [`pull`].
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum PullResult {
98    /// The branch already contained the upstream tip.
99    UpToDate,
100    /// The branch fast-forwarded to the upstream tip.
101    FastForward,
102    /// A merge commit integrated the upstream tip.
103    Merged,
104    /// Diverged history and ff-only mode refused to merge.
105    RefusedNotFf,
106    /// A merge was attempted but stopped with conflicts.
107    Conflicts,
108    /// An unborn branch adopted the upstream as its first commit.
109    SetUpstream,
110    /// Local commits were replayed onto the upstream tip.
111    Rebased,
112    /// Replaying local commits onto the upstream tip stopped on conflicts.
113    ///
114    /// A resumable rebase session is in progress (`.git/rebase-merge/`, conflict markers and
115    /// unmerged index entries, as `git pull --rebase` leaves them); the branch ref still points
116    /// at its pre-pull tip. Finish it with [`continue_rebase`](crate::history::continue_rebase)
117    /// after staging the resolution, or [`skip_rebase`](crate::history::skip_rebase) /
118    /// [`abort_rebase`](crate::history::abort_rebase). With [`PullOptions::check`] this
119    /// predicts the stop without touching anything.
120    RebaseConflicts,
121    /// Integration succeeded but re-applying `--autostash` left conflicts (stash kept).
122    AutostashConflict,
123}
124
125/// Structured outcome of [`pull`].
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub struct PullOutcome {
128    /// Integration result.
129    pub result: PullResult,
130    /// Remote-tracking label integrated (for example `origin/main`).
131    pub label: String,
132    /// Branch tip after a successful integration, when applicable.
133    pub oid: Option<ObjectId>,
134    /// Stored `change-id` on [`Self::oid`] when present.
135    pub change_id: Option<ChangeId>,
136    /// For [`PullResult::SetUpstream`], the upstream label that was adopted.
137    pub upstream: Option<String>,
138    /// Conflicted paths when [`Self::result`] is [`PullResult::Conflicts`] or
139    /// [`PullResult::RebaseConflicts`].
140    pub conflicts: Option<Vec<MergeConflictDetail>>,
141    /// For [`PullResult::RebaseConflicts`], the local commit that could not be replayed.
142    pub stopped_commit: Option<ObjectId>,
143    /// Autostash commit left on `refs/stash` because it could not be re-applied: the pull
144    /// stopped on rebase conflicts ([`PullResult::RebaseConflicts`]) or re-applying it
145    /// conflicted ([`PullResult::AutostashConflict`]).
146    pub autostash: Option<ObjectId>,
147    /// Commits on the branch not reachable from upstream when refused as not ff.
148    pub ahead: Option<usize>,
149    /// Commits on upstream not reachable from the branch when refused as not ff.
150    pub behind: Option<usize>,
151    /// Populated when [`PullOptions::check`] is set; the repository is unchanged.
152    pub check: Option<PullCheck>,
153    /// Objects and pack bytes the fetch step received (zero when nothing was
154    /// transferred or the upstream is local, `remote = .`).
155    pub transfer: TransferStats,
156}
157
158impl PullOutcome {
159    fn new(result: PullResult, label: &str) -> Self {
160        Self {
161            result,
162            label: label.to_owned(),
163            oid: None,
164            change_id: None,
165            upstream: None,
166            conflicts: None,
167            stopped_commit: None,
168            autostash: None,
169            ahead: None,
170            behind: None,
171            check: None,
172            transfer: TransferStats::default(),
173        }
174    }
175
176    fn at_commit(
177        result: PullResult,
178        label: &str,
179        repo: &Repository,
180        oid: ObjectId,
181    ) -> std::result::Result<Self, Error> {
182        Ok(Self {
183            oid: Some(oid),
184            change_id: logical_change_id_for_commit(repo, oid)?,
185            ..Self::new(result, label)
186        })
187    }
188}
189
190/// Errors from [`pull`].
191#[derive(Debug, Error)]
192pub enum PullError {
193    /// `HEAD` points at a detached commit.
194    #[error("HEAD is detached; pull needs a branch")]
195    DetachedHead,
196    /// `HEAD` could not be interpreted.
197    #[error("HEAD is in an unknown state")]
198    InvalidHead,
199    /// No upstream is configured or the tracking ref did not resolve after fetch.
200    #[error("no upstream configured for branch {branch}")]
201    NoUpstream {
202        /// Local branch short name.
203        branch: String,
204    },
205    /// `pull.rebase=interactive` (or branch rebase) is not supported.
206    #[error("interactive rebase is not supported; use grit history instead")]
207    RebaseInteractive,
208    /// Starting the rebase session failed (for example a rebase is already in progress).
209    #[error(transparent)]
210    Rebase(RebaseSessionError),
211    /// Remote resolution or transport failure.
212    #[error(transparent)]
213    Remote(#[from] RemoteError),
214    /// Other repository failures.
215    #[error(transparent)]
216    Repository(#[from] Error),
217}
218
219impl PullError {
220    /// Whether the remote could not be read at all ([`Error::RemoteUnreadable`]).
221    #[must_use]
222    pub fn is_remote_unreadable(&self) -> bool {
223        match self {
224            Self::Remote(e) => e.is_remote_unreadable(),
225            Self::Repository(e) => e.is_remote_unreadable(),
226            Self::DetachedHead
227            | Self::InvalidHead
228            | Self::NoUpstream { .. }
229            | Self::RebaseInteractive
230            | Self::Rebase(_) => false,
231        }
232    }
233
234    /// Whether the pull was cancelled ([`Error::Cancelled`]).
235    #[must_use]
236    pub fn is_cancelled(&self) -> bool {
237        match self {
238            Self::Remote(e) => e.is_cancelled(),
239            Self::Repository(e) => e.is_cancelled(),
240            Self::Rebase(e) => e.is_cancelled(),
241            Self::DetachedHead
242            | Self::InvalidHead
243            | Self::NoUpstream { .. }
244            | Self::RebaseInteractive => false,
245        }
246    }
247}
248
249/// Resolve pull integration mode from config with optional CLI override.
250///
251/// Precedence matches Git: explicit `--ff-only` / `--no-rebase` / `--rebase`, then
252/// `pull.ff=only`, then `branch.<name>.rebase` / `pull.rebase`, then remaining `pull.ff`.
253///
254/// # Errors
255///
256/// Returns [`PullError::RebaseInteractive`] when rebase config requests interactive mode.
257pub fn resolve_pull_mode(
258    config: &ConfigSet,
259    branch: Option<&str>,
260    cli: Option<PullMode>,
261) -> std::result::Result<PullMode, PullError> {
262    Ok(resolve_pull_integration(config, branch, cli)?.mode)
263}
264
265fn resolve_pull_integration(
266    config: &ConfigSet,
267    branch: Option<&str>,
268    cli: Option<PullMode>,
269) -> std::result::Result<PullIntegration, PullError> {
270    if cli == Some(PullMode::FfOnly) {
271        return Ok(PullIntegration {
272            mode: PullMode::FfOnly,
273            allow_fast_forward: true,
274        });
275    }
276    if cli == Some(PullMode::Merge) {
277        return Ok(PullIntegration {
278            mode: PullMode::Merge,
279            allow_fast_forward: true,
280        });
281    }
282    if cli == Some(PullMode::Rebase) {
283        return Ok(PullIntegration {
284            mode: PullMode::Rebase,
285            allow_fast_forward: true,
286        });
287    }
288    if let Some(raw) = config.get("pull.ff") {
289        let value = raw.trim();
290        if value.eq_ignore_ascii_case("only") {
291            return Ok(PullIntegration {
292                mode: PullMode::FfOnly,
293                allow_fast_forward: true,
294            });
295        }
296    }
297    if pull_rebase_enabled(config, branch)? {
298        return Ok(PullIntegration {
299            mode: PullMode::Rebase,
300            allow_fast_forward: true,
301        });
302    }
303    if let Some(raw) = config.get("pull.ff") {
304        let value = raw.trim();
305        if parse_git_config_maybe_bool(value) == Some(false) {
306            return Ok(PullIntegration {
307                mode: PullMode::Merge,
308                allow_fast_forward: false,
309            });
310        }
311    }
312    Ok(PullIntegration {
313        mode: PullMode::Merge,
314        allow_fast_forward: true,
315    })
316}
317
318impl PullOptions {
319    /// Build pull options for the current branch from config and CLI mode flags.
320    ///
321    /// # Errors
322    ///
323    /// Returns [`PullError::DetachedHead`] or [`PullError::InvalidHead`] when `HEAD`
324    /// is not on a branch.
325    pub fn for_working_branch(
326        repo: &Repository,
327        environment: Environment,
328        cli_mode: Option<PullMode>,
329    ) -> std::result::Result<Self, PullError> {
330        let short_name = match resolve_head_for_repository(repo).map_err(PullError::Repository)? {
331            HeadState::Branch { short_name, .. } => short_name,
332            HeadState::Detached { .. } => return Err(PullError::DetachedHead),
333            HeadState::Invalid => return Err(PullError::InvalidHead),
334        };
335        let config = repo.config().map_err(PullError::Repository)?;
336        let remote_name = configured_remote_for_pull(config.as_ref(), Some(&short_name));
337        let prune = resolve_fetch_prune(config.as_ref(), &remote_name, None)
338            .map_err(PullError::Repository)?;
339        Ok(Self {
340            remote: None,
341            branch: None,
342            explicit_fetch: false,
343            autostash: false,
344            check: false,
345            mode: cli_mode,
346            fetch: FetchOptions {
347                tags: TagMode::Following,
348                thread_policy: repo.thread_policy(),
349                environment,
350                prune,
351                remote_name: Some(remote_name),
352                ..Default::default()
353            },
354        })
355    }
356}
357
358fn pull_rebase_enabled(
359    config: &ConfigSet,
360    branch: Option<&str>,
361) -> std::result::Result<bool, PullError> {
362    if let Some(name) = branch {
363        if let Some(raw) = config.get(&format!("branch.{name}.rebase")) {
364            return parse_pull_rebase_config(raw.as_str());
365        }
366    }
367    if let Some(raw) = config.get("pull.rebase") {
368        return parse_pull_rebase_config(raw.as_str());
369    }
370    Ok(false)
371}
372
373/// Whether `--autostash` is active for this pull (CLI flag or config).
374#[must_use]
375pub fn resolve_pull_autostash(config: &ConfigSet, mode: PullMode, cli_autostash: bool) -> bool {
376    if cli_autostash {
377        return true;
378    }
379    let key = match mode {
380        PullMode::Rebase => "rebase.autoStash",
381        PullMode::FfOnly | PullMode::Merge => "merge.autoStash",
382    };
383    config
384        .get(key)
385        .and_then(|raw| parse_git_config_maybe_bool(raw.trim()))
386        .unwrap_or(false)
387}
388
389fn parse_pull_rebase_config(raw: &str) -> std::result::Result<bool, PullError> {
390    let value = raw.trim();
391    if value.eq_ignore_ascii_case("interactive") {
392        return Err(PullError::RebaseInteractive);
393    }
394    if value.eq_ignore_ascii_case("false") || value == "0" {
395        return Ok(false);
396    }
397    if let Some(parsed) = parse_git_config_maybe_bool(value) {
398        return Ok(parsed);
399    }
400    Ok(true)
401}
402
403/// Fetch from a remote and integrate its upstream into the current branch.
404///
405/// # Errors
406///
407/// Returns [`PullError`] for detached HEAD, missing upstream, interactive rebase config,
408/// or transport/repository failures.
409pub fn pull<P>(
410    repo: &Repository,
411    options: &PullOptions,
412    progress: &mut P,
413    cancel: &dyn crate::progress::Cancel,
414    http_factory: Option<&dyn HttpClientFactory>,
415) -> std::result::Result<PullOutcome, PullError>
416where
417    P: Progress,
418{
419    let (refname, short_name, head_oid) = match resolve_head_for_repository(repo)? {
420        HeadState::Branch {
421            refname,
422            short_name,
423            oid,
424        } => (refname, short_name, oid),
425        HeadState::Detached { .. } => return Err(PullError::DetachedHead),
426        HeadState::Invalid => return Err(PullError::InvalidHead),
427    };
428
429    let config = repo.config().map_err(PullError::Repository)?;
430    let remote_name = options
431        .remote
432        .clone()
433        .unwrap_or_else(|| configured_remote_for_pull(config.as_ref(), Some(&short_name)));
434
435    let integration = resolve_pull_integration(config.as_ref(), Some(&short_name), options.mode)?;
436    let (explicit_fetch, branch_refspecs) = pull_fetch_refspecs(options);
437    let mut fetch_opts = options.fetch.clone();
438    fetch_opts.refspecs = branch_refspecs;
439    if fetch_opts.remote_name.is_none() {
440        fetch_opts.remote_name = Some(remote_name.clone());
441    }
442    if options.check {
443        fetch_opts.dry_run = false;
444    }
445    let fetch_dry_run = fetch_opts.dry_run;
446
447    let autostash = resolve_pull_autostash(config.as_ref(), integration.mode, options.autostash)
448        && !options.check
449        && !fetch_dry_run;
450
451    let stash_oid = if autostash {
452        push_operation_autostash(repo, &checkout_reflog_identity(repo))
453            .map_err(PullError::Repository)?
454    } else {
455        None
456    };
457
458    let mut transfer = TransferStats::default();
459    let core: std::result::Result<PullOutcome, PullError> = (|| {
460        if remote_name != "." {
461            let remote = resolve_remote_argument(config.as_ref(), &remote_name)
462                .map_err(PullError::Remote)?;
463            transfer = remote
464                .fetch(repo, fetch_opts, progress, cancel, http_factory)
465                .map_err(PullError::Remote)?
466                .transfer;
467        }
468
469        if fetch_dry_run {
470            return Ok(PullOutcome {
471                oid: head_oid,
472                change_id: head_oid
473                    .and_then(|oid| logical_change_id_for_commit(repo, oid).ok())
474                    .flatten(),
475                ..PullOutcome::new(PullResult::UpToDate, &short_name)
476            });
477        }
478
479        let upstream =
480            resolve_pull_integration_base(repo, &short_name, &remote_name, explicit_fetch).ok_or(
481                PullError::NoUpstream {
482                    branch: short_name.clone(),
483                },
484            )?;
485        let label = upstream.name.clone();
486        let upstream_oid = upstream.oid;
487
488        if options.check {
489            return pull_check_outcome(repo, head_oid, upstream_oid, &label, integration);
490        }
491
492        match head_oid {
493            None => adopt_unborn_upstream(repo, &refname, &short_name, &label, upstream_oid),
494            Some(head_oid) => integrate_fetched_upstream(
495                repo,
496                &refname,
497                head_oid,
498                upstream_oid,
499                &label,
500                integration,
501            ),
502        }
503    })();
504    let core = core.map(|outcome| PullOutcome {
505        transfer,
506        ..outcome
507    });
508
509    match core {
510        Err(err) => {
511            restore_autostash_after_abort(repo, stash_oid.as_ref());
512            Err(err)
513        }
514        Ok(outcome) => {
515            if fetch_dry_run {
516                restore_autostash_after_abort(repo, stash_oid.as_ref());
517                return Ok(outcome);
518            }
519            if outcome.result == PullResult::RebaseConflicts {
520                // Applying the stash now would mix it into the conflict markers; it stays on
521                // `refs/stash` for the caller to pop once the rebase is finished or aborted.
522                return Ok(PullOutcome {
523                    autostash: stash_oid,
524                    ..outcome
525                });
526            }
527            if matches!(
528                outcome.result,
529                PullResult::Conflicts | PullResult::RefusedNotFf
530            ) {
531                restore_autostash_after_abort(repo, stash_oid.as_ref());
532                return Ok(outcome);
533            }
534            apply_autostash_after_pull(repo, stash_oid.as_ref(), Ok(outcome))
535        }
536    }
537}
538
539/// Merge [`PullOptions::branch`] into fetch refspecs for embedders and tests.
540fn pull_fetch_refspecs(options: &PullOptions) -> (bool, Vec<String>) {
541    let mut refspecs = options.fetch.refspecs.clone();
542    let mut explicit = options.explicit_fetch;
543    if let Some(branch) = options.branch.as_deref().filter(|b| !b.is_empty()) {
544        if refspecs.is_empty() {
545            refspecs.push(branch.to_owned());
546        }
547        explicit = true;
548    }
549    (explicit, refspecs)
550}
551
552fn restore_autostash_after_abort(repo: &Repository, stash_oid: Option<&ObjectId>) {
553    let Some(stash_oid) = stash_oid else {
554        return;
555    };
556    let _ = restore_operation_autostash(repo, stash_oid);
557}
558
559fn apply_autostash_after_pull(
560    repo: &Repository,
561    stash_oid: Option<&ObjectId>,
562    outcome: std::result::Result<PullOutcome, PullError>,
563) -> std::result::Result<PullOutcome, PullError> {
564    let mut outcome = outcome?;
565    if stash_oid.is_none() {
566        return Ok(outcome);
567    }
568    if !matches!(
569        outcome.result,
570        PullResult::UpToDate
571            | PullResult::FastForward
572            | PullResult::Merged
573            | PullResult::Rebased
574            | PullResult::SetUpstream
575    ) {
576        return Ok(outcome);
577    }
578    if let Some(stash_oid) = stash_oid {
579        if apply_operation_autostash(repo, stash_oid).map_err(PullError::Repository)? {
580            outcome.result = PullResult::AutostashConflict;
581            outcome.autostash = Some(*stash_oid);
582        }
583    }
584    Ok(outcome)
585}
586
587fn pull_check_outcome(
588    repo: &Repository,
589    head_oid: Option<ObjectId>,
590    upstream_oid: ObjectId,
591    label: &str,
592    integration: PullIntegration,
593) -> std::result::Result<PullOutcome, PullError> {
594    let (ahead, behind, would, fast_forward_possible) = match head_oid {
595        None => (0, 0, PullResult::SetUpstream, true),
596        Some(head_oid) => {
597            let (ahead, behind) = count_symmetric_ahead_behind(repo, head_oid, upstream_oid)
598                .map_err(PullError::Repository)?;
599            let would = predict_pull_result(repo, head_oid, upstream_oid, integration)?;
600            let up_to_date = head_oid == upstream_oid || is_ancestor(repo, upstream_oid, head_oid)?;
601            let fast_forward = !up_to_date
602                && is_ancestor(repo, head_oid, upstream_oid).map_err(PullError::Repository)?;
603            (ahead, behind, would, up_to_date || fast_forward)
604        }
605    };
606    Ok(PullOutcome {
607        oid: head_oid,
608        change_id: head_oid
609            .and_then(|oid| logical_change_id_for_commit(repo, oid).ok())
610            .flatten(),
611        ahead: Some(ahead),
612        behind: Some(behind),
613        check: Some(PullCheck {
614            ahead,
615            behind,
616            fast_forward_possible,
617            would,
618        }),
619        ..PullOutcome::new(would, label)
620    })
621}
622
623fn predict_pull_result(
624    repo: &Repository,
625    head_oid: ObjectId,
626    upstream_oid: ObjectId,
627    integration: PullIntegration,
628) -> std::result::Result<PullResult, PullError> {
629    if head_oid == upstream_oid || is_ancestor(repo, upstream_oid, head_oid)? {
630        return Ok(PullResult::UpToDate);
631    }
632    if integration.allow_fast_forward && is_ancestor(repo, head_oid, upstream_oid)? {
633        return Ok(PullResult::FastForward);
634    }
635    let diverged = !is_ancestor(repo, upstream_oid, head_oid)?
636        && !is_ancestor(repo, head_oid, upstream_oid)?
637        && head_oid != upstream_oid;
638    if diverged {
639        return match integration.mode {
640            PullMode::FfOnly => Ok(PullResult::RefusedNotFf),
641            PullMode::Rebase => {
642                let mut opts = pull_rebase_options(repo)?;
643                opts.rewrite.sign = false;
644                let request = RebaseRequest {
645                    upstream: upstream_oid,
646                    onto: None,
647                    branch_tip: head_oid,
648                };
649                match plan_rebase_check(repo, &request, &opts).map_err(map_history_error)? {
650                    RebaseCheckPlan::UpToDate | RebaseCheckPlan::Clean { .. } => {
651                        Ok(PullResult::Rebased)
652                    }
653                    RebaseCheckPlan::Conflict { .. } => Ok(PullResult::RebaseConflicts),
654                }
655            }
656            PullMode::Merge => match predict_merge(
657                repo,
658                head_oid,
659                upstream_oid,
660                &MergePredictionOptions {
661                    allow_fast_forward: integration.allow_fast_forward,
662                    theirs_label: "upstream".into(),
663                    ..Default::default()
664                },
665            )
666            .map_err(PullError::Repository)?
667            {
668                MergePrediction::UpToDate => Ok(PullResult::UpToDate),
669                MergePrediction::FastForward => Ok(PullResult::FastForward),
670                MergePrediction::Clean { .. } => Ok(PullResult::Merged),
671                MergePrediction::Conflicts { .. } => Ok(PullResult::Conflicts),
672                MergePrediction::Unrelated => Ok(PullResult::Conflicts),
673            },
674        };
675    }
676    Ok(PullResult::Merged)
677}
678
679fn adopt_unborn_upstream(
680    repo: &Repository,
681    refname: &str,
682    short_name: &str,
683    label: &str,
684    upstream_oid: ObjectId,
685) -> std::result::Result<PullOutcome, PullError> {
686    ensure_branch_tip(repo, refname, None)?;
687    let upstream_tree = repo.odb().commit_tree(&upstream_oid)?;
688    prepare_tree_checkout(repo, None, &upstream_tree).map_err(PullError::Repository)?;
689    checkout_between_trees(repo, None, &upstream_tree).map_err(PullError::Repository)?;
690    update_checked_out_branch(repo, refname, None, upstream_oid, INITIAL_PULL_REFLOG)?;
691    Ok(PullOutcome {
692        upstream: Some(label.to_owned()),
693        ..PullOutcome::at_commit(PullResult::SetUpstream, short_name, repo, upstream_oid)?
694    })
695}
696
697fn integrate_fetched_upstream(
698    repo: &Repository,
699    refname: &str,
700    head_oid: ObjectId,
701    upstream_oid: ObjectId,
702    label: &str,
703    integration: PullIntegration,
704) -> std::result::Result<PullOutcome, PullError> {
705    let diverged = !is_ancestor(repo, upstream_oid, head_oid).map_err(PullError::Repository)?
706        && !is_ancestor(repo, head_oid, upstream_oid).map_err(PullError::Repository)?
707        && head_oid != upstream_oid;
708
709    if diverged {
710        match integration.mode {
711            PullMode::FfOnly => {
712                let (ahead, behind) = count_symmetric_ahead_behind(repo, head_oid, upstream_oid)
713                    .map_err(PullError::Repository)?;
714                return Ok(PullOutcome {
715                    ahead: Some(ahead),
716                    behind: Some(behind),
717                    ..PullOutcome::at_commit(PullResult::RefusedNotFf, label, repo, head_oid)?
718                });
719            }
720            PullMode::Rebase => {
721                return rebase_fetched_upstream(repo, refname, head_oid, upstream_oid, label);
722            }
723            PullMode::Merge => {}
724        }
725    }
726
727    let integrate = integrate_branch_with_options(
728        repo,
729        refname,
730        head_oid,
731        upstream_oid,
732        label,
733        IntegrateBranchOptions {
734            allow_fast_forward: integration.allow_fast_forward,
735        },
736    )
737    .map_err(PullError::Repository)?;
738    Ok(map_integrate_outcome(integrate, label))
739}
740
741fn rebase_fetched_upstream(
742    repo: &Repository,
743    refname: &str,
744    head_oid: ObjectId,
745    upstream_oid: ObjectId,
746    label: &str,
747) -> std::result::Result<PullOutcome, PullError> {
748    if merge_bases_first_vs_rest(repo, head_oid, &[upstream_oid])
749        .map_err(PullError::Repository)?
750        .is_empty()
751    {
752        return Err(PullError::Repository(Error::Message(format!(
753            "'{label}' has no common history with the current branch"
754        ))));
755    }
756
757    let start = StartRebaseOptions {
758        request: RebaseRequest {
759            upstream: upstream_oid,
760            onto: None,
761            branch_tip: head_oid,
762        },
763        rebase: pull_rebase_options(repo)?,
764        apply: ApplyOptions {
765            identity: repo
766                .identity(IdentRole::Committer, repo.identity_now())
767                .map_err(PullError::Repository)?,
768            reflog_message: format!("pull: rebase onto {label}"),
769            record_operation: Some("rebase".into()),
770            hooks: crate::hooks::PostOperationHooks::Run,
771        },
772    };
773    match start_rebase(repo, &start).map_err(map_rebase_session_error)? {
774        RebaseOutcome::Stopped(stop) => Ok(PullOutcome {
775            conflicts: Some(stop.conflicts),
776            stopped_commit: Some(stop.commit),
777            ..PullOutcome::new(PullResult::RebaseConflicts, label)
778        }),
779        RebaseOutcome::Done { applied, .. } if applied.head != head_oid => Ok(
780            PullOutcome::at_commit(PullResult::Rebased, label, repo, applied.head)?,
781        ),
782        // Nothing was rewritten: integrate like `git pull --rebase`, which fast-forwards when
783        // the branch is simply behind the upstream tip.
784        RebaseOutcome::Done { .. } => {
785            let integrate = integrate_branch_with_options(
786                repo,
787                refname,
788                head_oid,
789                upstream_oid,
790                label,
791                IntegrateBranchOptions {
792                    allow_fast_forward: true,
793                },
794            )
795            .map_err(PullError::Repository)?;
796            Ok(map_integrate_outcome(integrate, label))
797        }
798    }
799}
800
801/// Replay options shared by the rebase pull and its `--check` prediction.
802fn pull_rebase_options(repo: &Repository) -> std::result::Result<RebaseOntoOptions, PullError> {
803    let committer = repo
804        .identity(IdentRole::Committer, repo.identity_now())
805        .map_err(PullError::Repository)?;
806    let mut opts = RebaseOntoOptions::new(RewriteOptions {
807        scope: RefScope::Head,
808        empty: EmptyPolicy::Drop,
809        committer,
810        preserve_committer: false,
811        sign: true,
812    });
813    opts.flatten_leaf_merges = true;
814    Ok(opts)
815}
816
817fn map_history_error(err: HistoryError) -> PullError {
818    match err {
819        HistoryError::Library(e) => PullError::Repository(*e),
820        other => PullError::Repository(Error::Message(other.to_string())),
821    }
822}
823
824fn map_rebase_session_error(err: RebaseSessionError) -> PullError {
825    match err {
826        RebaseSessionError::Library(e) => PullError::Repository(e),
827        RebaseSessionError::History(e) => map_history_error(e),
828        other => PullError::Rebase(other),
829    }
830}
831
832fn map_integrate_outcome(outcome: IntegrateBranchOutcome, label: &str) -> PullOutcome {
833    match outcome {
834        IntegrateBranchOutcome::UpToDate => PullOutcome::new(PullResult::UpToDate, label),
835        IntegrateBranchOutcome::FastForward { oid, change_id } => PullOutcome {
836            oid: Some(oid),
837            change_id,
838            ..PullOutcome::new(PullResult::FastForward, label)
839        },
840        IntegrateBranchOutcome::Merged { oid, change_id } => PullOutcome {
841            oid: Some(oid),
842            change_id,
843            ..PullOutcome::new(PullResult::Merged, label)
844        },
845        IntegrateBranchOutcome::Conflicts { details, .. } => PullOutcome {
846            conflicts: Some(details),
847            ..PullOutcome::new(PullResult::Conflicts, label)
848        },
849        IntegrateBranchOutcome::Squashed | IntegrateBranchOutcome::Stopped => {
850            unreachable!("pull does not use squash or no-commit merge modes")
851        }
852    }
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use crate::config::environment::RepositoryOptions;
859    use crate::config::ConfigSet;
860    use crate::environment::Environment;
861    use crate::progress::NeverCancel;
862    use crate::remote::fetch::NoProgress;
863    use std::path::{Path, PathBuf};
864    use std::process::{Command, Output};
865    use tempfile::TempDir;
866
867    fn git_output(dir: &Path, args: &[&str]) -> Output {
868        Command::new("git")
869            .current_dir(dir)
870            .args(args)
871            .env("GIT_CONFIG_GLOBAL", "/dev/null")
872            .env("GIT_CONFIG_SYSTEM", "/dev/null")
873            .env("GIT_AUTHOR_NAME", "T")
874            .env("GIT_AUTHOR_EMAIL", "t@example.com")
875            .env("GIT_COMMITTER_NAME", "T")
876            .env("GIT_COMMITTER_EMAIL", "t@example.com")
877            .output()
878            .expect("run git")
879    }
880
881    fn git(dir: &Path, args: &[&str]) -> String {
882        let out = git_output(dir, args);
883        assert!(
884            out.status.success(),
885            "git {args:?} failed: {}",
886            String::from_utf8_lossy(&out.stderr)
887        );
888        String::from_utf8_lossy(&out.stdout).trim().to_owned()
889    }
890
891    fn open(dir: &Path) -> Repository {
892        Repository::discover(Some(dir), &RepositoryOptions::empty()).expect("open")
893    }
894
895    fn commit(dir: &Path, file: &str, contents: &str) -> ObjectId {
896        std::fs::write(dir.join(file), contents).expect("write");
897        git(dir, &["add", file]);
898        git(dir, &["commit", "-qm", contents]);
899        ObjectId::from_hex(&git(dir, &["rev-parse", "HEAD"])).expect("oid")
900    }
901
902    fn origin_and_clone(tmp: &TempDir) -> (PathBuf, PathBuf) {
903        let origin = tmp.path().join("origin");
904        std::fs::create_dir_all(&origin).expect("mkdir origin");
905        git(&origin, &["init", "-q", "-b", "main"]);
906        commit(&origin, "a.txt", "one");
907        git(tmp.path(), &["clone", "-q", "origin", "clone"]);
908        (origin, tmp.path().join("clone"))
909    }
910
911    fn pull_options(repo: &Repository) -> PullOptions {
912        PullOptions::for_working_branch(repo, Environment::empty(), None).expect("options")
913    }
914
915    fn cfg(pairs: &[(&str, &str)]) -> ConfigSet {
916        let mut set = ConfigSet::new();
917        for (key, value) in pairs {
918            set.add_command_override(key, value).expect("override");
919        }
920        set
921    }
922
923    #[test]
924    fn resolve_pull_mode_cli_ff_only_wins() {
925        let config = cfg(&[("pull.rebase", "true")]);
926        assert_eq!(
927            resolve_pull_mode(&config, Some("main"), Some(PullMode::FfOnly)).expect("mode"),
928            PullMode::FfOnly
929        );
930    }
931
932    #[test]
933    fn resolve_pull_mode_no_rebase_overrides_rebase_config() {
934        let config = cfg(&[("pull.rebase", "true")]);
935        assert_eq!(
936            resolve_pull_mode(&config, Some("main"), Some(PullMode::Merge)).expect("mode"),
937            PullMode::Merge
938        );
939    }
940
941    #[test]
942    fn resolve_pull_mode_rebase_from_config() {
943        let config = cfg(&[("pull.rebase", "merges")]);
944        assert_eq!(
945            resolve_pull_mode(&config, Some("main"), None).expect("mode"),
946            PullMode::Rebase
947        );
948    }
949
950    #[test]
951    fn resolve_pull_mode_branch_rebase_overrides_global() {
952        let config = cfg(&[("pull.rebase", "false"), ("branch.topic.rebase", "true")]);
953        assert_eq!(
954            resolve_pull_mode(&config, Some("topic"), None).expect("mode"),
955            PullMode::Rebase
956        );
957    }
958
959    #[test]
960    fn resolve_pull_mode_interactive_is_error() {
961        let config = cfg(&[("pull.rebase", "interactive")]);
962        assert!(matches!(
963            resolve_pull_mode(&config, Some("main"), None),
964            Err(PullError::RebaseInteractive)
965        ));
966    }
967
968    #[test]
969    fn resolve_pull_mode_ff_only_from_config() {
970        let config = cfg(&[("pull.ff", "only")]);
971        assert_eq!(
972            resolve_pull_mode(&config, Some("main"), None).expect("mode"),
973            PullMode::FfOnly
974        );
975    }
976
977    #[test]
978    fn resolve_pull_mode_ff_only_beats_rebase_config() {
979        let config = cfg(&[("pull.ff", "only"), ("pull.rebase", "true")]);
980        assert_eq!(
981            resolve_pull_mode(&config, Some("main"), None).expect("mode"),
982            PullMode::FfOnly
983        );
984    }
985
986    #[test]
987    fn resolve_pull_mode_defaults_to_merge() {
988        let config = ConfigSet::new();
989        assert_eq!(
990            resolve_pull_mode(&config, Some("main"), None).expect("mode"),
991            PullMode::Merge
992        );
993    }
994
995    #[test]
996    fn resolve_pull_integration_ff_false_disables_fast_forward() {
997        let config = cfg(&[("pull.ff", "false")]);
998        let resolved = resolve_pull_integration(&config, Some("main"), None).expect("resolved");
999        assert_eq!(resolved.mode, PullMode::Merge);
1000        assert!(!resolved.allow_fast_forward);
1001    }
1002
1003    #[test]
1004    fn pull_maps_custom_fetch_refspec() {
1005        let tmp = TempDir::new().expect("tempdir");
1006        let (origin, clone) = origin_and_clone(&tmp);
1007        let tip = commit(&origin, "b.txt", "two");
1008        git(
1009            &clone,
1010            &[
1011                "config",
1012                "--replace-all",
1013                "remote.origin.fetch",
1014                "+refs/heads/*:refs/remotes/mirror/*",
1015            ],
1016        );
1017
1018        let repo = open(&clone);
1019        let outcome = pull(
1020            &repo,
1021            &pull_options(&repo),
1022            &mut NoProgress,
1023            &NeverCancel,
1024            None,
1025        )
1026        .expect("pull succeeds");
1027        assert_eq!(outcome.result, PullResult::FastForward);
1028        assert_eq!(outcome.oid, Some(tip));
1029        assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1030    }
1031
1032    #[test]
1033    fn pull_local_upstream_skips_fetch() {
1034        let tmp = TempDir::new().expect("tempdir");
1035        let dir = tmp.path();
1036        git(dir, &["init", "-q", "-b", "main"]);
1037        commit(dir, "a.txt", "one");
1038        git(dir, &["checkout", "-q", "-b", "dev"]);
1039        let tip = commit(dir, "b.txt", "two");
1040        git(dir, &["checkout", "-q", "main"]);
1041        git(dir, &["branch", "--set-upstream-to", "dev", "main"]);
1042
1043        let repo = open(dir);
1044        let outcome = pull(
1045            &repo,
1046            &pull_options(&repo),
1047            &mut NoProgress,
1048            &NeverCancel,
1049            None,
1050        )
1051        .expect("local pull succeeds");
1052        assert_eq!(outcome.result, PullResult::FastForward);
1053        assert_eq!(outcome.oid, Some(tip));
1054        assert_eq!(git(dir, &["rev-parse", "main"]), tip.to_hex());
1055        assert_eq!(outcome.transfer, TransferStats::default());
1056    }
1057
1058    #[test]
1059    fn pull_reports_objects_received_by_its_fetch() {
1060        let tmp = TempDir::new().expect("tempdir");
1061        let (origin, clone) = origin_and_clone(&tmp);
1062        commit(&origin, "b.txt", "two");
1063
1064        let repo = open(&clone);
1065        let outcome = pull(
1066            &repo,
1067            &pull_options(&repo),
1068            &mut NoProgress,
1069            &NeverCancel,
1070            None,
1071        )
1072        .expect("pull succeeds");
1073        assert_eq!(outcome.result, PullResult::FastForward);
1074        // One new commit, its root tree and the added blob.
1075        assert_eq!(outcome.transfer.objects, 3, "{:?}", outcome.transfer);
1076        assert!(outcome.transfer.bytes > 0, "{:?}", outcome.transfer);
1077
1078        let again = pull(
1079            &repo,
1080            &pull_options(&repo),
1081            &mut NoProgress,
1082            &NeverCancel,
1083            None,
1084        )
1085        .expect("second pull");
1086        assert_eq!(again.result, PullResult::UpToDate);
1087        assert_eq!(again.transfer.objects, 0, "{:?}", again.transfer);
1088    }
1089
1090    #[test]
1091    fn pull_adopts_upstream_on_unborn_branch_with_reflog() {
1092        let tmp = TempDir::new().expect("tempdir");
1093        let origin = tmp.path().join("origin");
1094        std::fs::create_dir_all(&origin).expect("mkdir origin");
1095        git(&origin, &["init", "-q", "-b", "main"]);
1096        let tip = commit(&origin, "a.txt", "one");
1097        let local = tmp.path().join("local");
1098        std::fs::create_dir_all(&local).expect("mkdir local");
1099        git(&local, &["init", "-q", "-b", "main"]);
1100        git(&local, &["remote", "add", "origin", "../origin"]);
1101
1102        let repo = open(&local);
1103        let outcome = pull(
1104            &repo,
1105            &pull_options(&repo),
1106            &mut NoProgress,
1107            &NeverCancel,
1108            None,
1109        )
1110        .expect("pull succeeds");
1111        assert_eq!(outcome.result, PullResult::SetUpstream);
1112        assert_eq!(outcome.oid, Some(tip));
1113        assert_eq!(
1114            git(&local, &["reflog", "show", "--format=%H %gs", "-1", "main"]),
1115            format!("{} {INITIAL_PULL_REFLOG}", tip.to_hex())
1116        );
1117    }
1118
1119    #[test]
1120    fn reflog_failure_after_ref_update_is_a_warning() {
1121        let tmp = TempDir::new().expect("tempdir");
1122        let (origin, clone) = origin_and_clone(&tmp);
1123        let tip = commit(&origin, "b.txt", "two");
1124        let branch_log = clone.join(".git/logs/refs/heads/main");
1125        std::fs::remove_file(&branch_log).expect("remove branch reflog");
1126        std::fs::create_dir(&branch_log).expect("replace reflog with directory");
1127
1128        let mut repo = open(&clone);
1129        let sink = std::sync::Arc::new(crate::diagnostics::CollectingDiagnostics::new());
1130        repo.set_diagnostics(sink.clone());
1131        let outcome = pull(
1132            &repo,
1133            &pull_options(&repo),
1134            &mut NoProgress,
1135            &NeverCancel,
1136            None,
1137        )
1138        .expect("pull completes");
1139        assert_eq!(outcome.result, PullResult::FastForward);
1140        assert_eq!(outcome.oid, Some(tip));
1141        assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1142        assert!(sink.warnings().iter().any(|warning| matches!(
1143            warning,
1144            crate::diagnostics::Warning::ReflogNotWritten { refname, .. }
1145                if refname == "refs/heads/main"
1146        )));
1147    }
1148}