1use crate::branch_tracking::resolve_pull_integration_base;
12use crate::change_id::{logical_change_id_for_commit, ChangeId};
13use crate::config::environment::Environment;
14use crate::config::ident_resolve::IdentRole;
15use crate::config::parse_git_config_maybe_bool;
16use crate::config::ConfigSet;
17use crate::error::{Error, OverwriteReason};
18use crate::fetch_request::{configured_remote_for_pull, resolve_remote_argument};
19use crate::merge_base::{count_symmetric_ahead_behind, is_ancestor};
20use crate::objects::ObjectId;
21use crate::operation::Conflict;
22use crate::porcelain::checkout::checkout_between_trees;
23use crate::porcelain::checkout_reflog::checkout_reflog_identity;
24use crate::porcelain::merge::{
25 ensure_branch_tip, integrate_branch_with_options, predict_merge, update_checked_out_branch,
26 IntegrateBranchOptions, IntegrateBranchOutcome, MergePrediction, MergePredictionOptions,
27};
28use crate::porcelain::stash::{
29 apply_operation_autostash, push_operation_autostash, record_merge_autostash,
30 restore_operation_autostash,
31};
32use crate::porcelain::switch::checkout_tree_transition_allow_local_reset;
33use crate::porcelain::worktree_guard::prepare_tree_checkout;
34use crate::progress::{Cancel, ProgressSink};
35use crate::remote::fetch::Progress;
36use crate::remote::transfer::{resolve_fetch_prune, FetchOptions, TagMode, TransferStats};
37use crate::remote::{HttpClientFactory, RemoteError};
38use crate::repo::Repository;
39use crate::revwalk::history::{
40 plan_rebase_check, rebase_request_after_fetch, start_rebase, ApplyOptions, EmptyPolicy,
41 HistoryError, PreRebase, RebaseCheckPlan, RebaseOntoOptions, RebaseOutcome, RebaseSessionError,
42 RefScope, RewriteOptions, StartRebaseOptions,
43};
44use crate::state::{resolve_head_for_repository, HeadState};
45use thiserror::Error;
46
47const INITIAL_PULL_REFLOG: &str = "initial pull";
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51struct PullIntegration {
52 mode: PullMode,
53 allow_fast_forward: bool,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum PullMode {
59 FfOnly,
61 Merge,
63 Rebase,
66}
67
68#[derive(Clone, Default)]
70pub struct PullOptions {
71 pub remote: Option<String>,
73 pub branch: Option<String>,
75 pub explicit_fetch: bool,
77 pub autostash: bool,
79 pub check: bool,
81 pub mode: Option<PullMode>,
83 pub fetch: FetchOptions,
85 pub upstream: Option<FetchedUpstream>,
90 pub stop_on_conflict: bool,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct FetchedUpstream {
101 pub label: String,
103 pub oid: ObjectId,
105 pub landed: Option<ObjectId>,
113 pub copies: Vec<ObjectId>,
118}
119
120#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct PullCheck {
123 pub ahead: usize,
125 pub behind: usize,
127 pub fast_forward_possible: bool,
129 pub would: PullResult,
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub enum PullResult {
136 UpToDate,
138 FastForward,
140 Merged,
142 RefusedNotFf,
144 Conflicts,
146 SetUpstream,
148 Rebased,
150 RebaseConflicts,
159 AutostashConflict,
161 RebaseStopped,
166 Landed,
170}
171
172#[derive(Clone, Debug, PartialEq, Eq)]
174pub struct PullOutcome {
175 pub result: PullResult,
177 pub label: String,
179 pub oid: Option<ObjectId>,
181 pub change_id: Option<ChangeId>,
183 pub upstream: Option<String>,
185 pub conflicts: Option<Vec<Conflict>>,
188 pub stopped_commit: Option<ObjectId>,
190 pub autostash: Option<ObjectId>,
194 pub ahead: Option<usize>,
196 pub behind: Option<usize>,
198 pub check: Option<PullCheck>,
200 pub transfer: TransferStats,
203}
204
205impl PullOutcome {
206 fn new(result: PullResult, label: &str) -> Self {
207 Self {
208 result,
209 label: label.to_owned(),
210 oid: None,
211 change_id: None,
212 upstream: None,
213 conflicts: None,
214 stopped_commit: None,
215 autostash: None,
216 ahead: None,
217 behind: None,
218 check: None,
219 transfer: TransferStats::default(),
220 }
221 }
222
223 fn at_commit(
224 result: PullResult,
225 label: &str,
226 repo: &Repository,
227 oid: ObjectId,
228 ) -> std::result::Result<Self, Error> {
229 Ok(Self {
230 oid: Some(oid),
231 change_id: logical_change_id_for_commit(repo, oid)?,
232 ..Self::new(result, label)
233 })
234 }
235}
236
237#[derive(Debug, Error)]
239pub enum PullError {
240 #[error("HEAD is detached; pull needs a branch")]
242 DetachedHead,
243 #[error("HEAD is in an unknown state")]
245 InvalidHead,
246 #[error("no upstream configured for branch {branch}")]
248 NoUpstream {
249 branch: String,
251 },
252 #[error("interactive rebase is not supported; use grit history instead")]
254 RebaseInteractive,
255 #[error(transparent)]
257 Rebase(RebaseSessionError),
258 #[error(transparent)]
260 Remote(#[from] RemoteError),
261 #[error(transparent)]
263 Repository(#[from] Error),
264}
265
266impl PullError {
267 #[must_use]
269 pub fn is_remote_unreadable(&self) -> bool {
270 match self {
271 Self::Remote(e) => e.is_remote_unreadable(),
272 Self::Repository(e) => e.is_remote_unreadable(),
273 Self::DetachedHead
274 | Self::InvalidHead
275 | Self::NoUpstream { .. }
276 | Self::RebaseInteractive
277 | Self::Rebase(_) => false,
278 }
279 }
280
281 #[must_use]
283 pub fn is_cancelled(&self) -> bool {
284 match self {
285 Self::Remote(e) => e.is_cancelled(),
286 Self::Repository(e) => e.is_cancelled(),
287 Self::Rebase(e) => e.is_cancelled(),
288 Self::DetachedHead
289 | Self::InvalidHead
290 | Self::NoUpstream { .. }
291 | Self::RebaseInteractive => false,
292 }
293 }
294}
295
296pub fn resolve_pull_mode(
305 config: &ConfigSet,
306 branch: Option<&str>,
307 cli: Option<PullMode>,
308) -> std::result::Result<PullMode, PullError> {
309 Ok(resolve_pull_integration(config, branch, cli)?.mode)
310}
311
312fn resolve_pull_integration(
313 config: &ConfigSet,
314 branch: Option<&str>,
315 cli: Option<PullMode>,
316) -> std::result::Result<PullIntegration, PullError> {
317 if cli == Some(PullMode::FfOnly) {
318 return Ok(PullIntegration {
319 mode: PullMode::FfOnly,
320 allow_fast_forward: true,
321 });
322 }
323 if cli == Some(PullMode::Merge) {
324 return Ok(PullIntegration {
325 mode: PullMode::Merge,
326 allow_fast_forward: true,
327 });
328 }
329 if cli == Some(PullMode::Rebase) {
330 return Ok(PullIntegration {
331 mode: PullMode::Rebase,
332 allow_fast_forward: true,
333 });
334 }
335 if let Some(raw) = config.get("pull.ff") {
336 let value = raw.trim();
337 if value.eq_ignore_ascii_case("only") {
338 return Ok(PullIntegration {
339 mode: PullMode::FfOnly,
340 allow_fast_forward: true,
341 });
342 }
343 }
344 if pull_rebase_enabled(config, branch)? {
345 return Ok(PullIntegration {
346 mode: PullMode::Rebase,
347 allow_fast_forward: true,
348 });
349 }
350 if let Some(raw) = config.get("pull.ff") {
351 let value = raw.trim();
352 if parse_git_config_maybe_bool(value) == Some(false) {
353 return Ok(PullIntegration {
354 mode: PullMode::Merge,
355 allow_fast_forward: false,
356 });
357 }
358 }
359 Ok(PullIntegration {
360 mode: PullMode::Merge,
361 allow_fast_forward: true,
362 })
363}
364
365impl PullOptions {
366 pub fn for_working_branch(
373 repo: &Repository,
374 environment: Environment,
375 cli_mode: Option<PullMode>,
376 ) -> std::result::Result<Self, PullError> {
377 if let Some(op) = crate::state::operation_blocking_switch(repo.git_dir())
378 .map_err(PullError::Repository)?
379 {
380 return Err(PullError::Repository(Error::OperationInProgress(op)));
381 }
382 let short_name = match resolve_head_for_repository(repo).map_err(PullError::Repository)? {
383 HeadState::Branch { short_name, .. } => short_name,
384 HeadState::Detached { .. } => return Err(PullError::DetachedHead),
385 HeadState::Invalid => return Err(PullError::InvalidHead),
386 };
387 let config = repo.config().map_err(PullError::Repository)?;
388 let remote_name = configured_remote_for_pull(config.as_ref(), Some(&short_name));
389 let prune = resolve_fetch_prune(config.as_ref(), &remote_name, None)
390 .map_err(PullError::Repository)?;
391 Ok(Self {
392 remote: None,
393 branch: None,
394 explicit_fetch: false,
395 autostash: false,
396 check: false,
397 mode: cli_mode,
398 fetch: {
399 let mut opts = FetchOptions::new();
400 opts.tags = TagMode::Following;
401 opts.thread_policy = repo.thread_policy();
402 opts.environment = environment;
403 opts.prune = prune;
404 opts.remote_name = Some(remote_name);
405 opts.reflog_action = "pull";
406 opts
407 },
408 upstream: None,
409 stop_on_conflict: false,
410 })
411 }
412}
413
414fn pull_rebase_enabled(
415 config: &ConfigSet,
416 branch: Option<&str>,
417) -> std::result::Result<bool, PullError> {
418 if let Some(name) = branch {
419 if let Some(raw) = config.get(&format!("branch.{name}.rebase")) {
420 return parse_pull_rebase_config(raw.as_str());
421 }
422 }
423 if let Some(raw) = config.get("pull.rebase") {
424 return parse_pull_rebase_config(raw.as_str());
425 }
426 Ok(false)
427}
428
429#[must_use]
431pub fn resolve_pull_autostash(config: &ConfigSet, mode: PullMode, cli_autostash: bool) -> bool {
432 if cli_autostash {
433 return true;
434 }
435 let key = match mode {
436 PullMode::Rebase => "rebase.autoStash",
437 PullMode::FfOnly | PullMode::Merge => "merge.autoStash",
438 };
439 config
440 .get(key)
441 .and_then(|raw| parse_git_config_maybe_bool(raw.trim()))
442 .unwrap_or(false)
443}
444
445fn parse_pull_rebase_config(raw: &str) -> std::result::Result<bool, PullError> {
446 let value = raw.trim();
447 if value.eq_ignore_ascii_case("interactive") {
448 return Err(PullError::RebaseInteractive);
449 }
450 if value.eq_ignore_ascii_case("false") || value == "0" {
451 return Ok(false);
452 }
453 if let Some(parsed) = parse_git_config_maybe_bool(value) {
454 return Ok(parsed);
455 }
456 Ok(true)
457}
458
459pub fn pull<P>(
468 repo: &Repository,
469 options: &PullOptions,
470 progress: &mut P,
471 cancel: &dyn crate::progress::Cancel,
472 http_factory: Option<&dyn HttpClientFactory>,
473) -> std::result::Result<PullOutcome, PullError>
474where
475 P: Progress,
476{
477 if let Some(op) = crate::state::operation_blocking_switch(repo.git_dir())? {
478 return Err(PullError::Repository(Error::OperationInProgress(op)));
479 }
480 let (refname, short_name, head_oid) = match resolve_head_for_repository(repo)? {
481 HeadState::Branch {
482 refname,
483 short_name,
484 oid,
485 } => (refname, short_name, oid),
486 HeadState::Detached { .. } => return Err(PullError::DetachedHead),
487 HeadState::Invalid => return Err(PullError::InvalidHead),
488 };
489
490 let config = repo.config().map_err(PullError::Repository)?;
491 let remote_name = options
492 .remote
493 .clone()
494 .unwrap_or_else(|| configured_remote_for_pull(config.as_ref(), Some(&short_name)));
495
496 let integration = resolve_pull_integration(config.as_ref(), Some(&short_name), options.mode)?;
497 let (explicit_fetch, branch_refspecs) = pull_fetch_refspecs(options);
498 let mut fetch_opts = options.fetch.clone();
499 fetch_opts.refspecs = branch_refspecs;
500 if fetch_opts.remote_name.is_none() {
501 fetch_opts.remote_name = Some(remote_name.clone());
502 }
503 if options.check {
504 fetch_opts.dry_run = false;
505 }
506 let fetch_dry_run = fetch_opts.dry_run;
507
508 let autostash = resolve_pull_autostash(config.as_ref(), integration.mode, options.autostash)
509 && !options.check
510 && !fetch_dry_run;
511
512 let merge_in_progress =
513 || crate::state::read_merge_heads(repo.git_dir()).is_ok_and(|heads| !heads.is_empty());
514 let merge_was_in_progress = merge_in_progress();
515 let stash_oid = if autostash && head_oid.is_some() {
516 push_operation_autostash(repo, &checkout_reflog_identity(repo))
517 .map_err(PullError::Repository)?
518 } else {
519 None
520 };
521
522 let stop_on_conflict = options.stop_on_conflict && stash_oid.is_none();
523 let mut transfer = TransferStats::default();
524 let core: std::result::Result<PullOutcome, PullError> = (|| {
525 if options.upstream.is_none() && remote_name != "." {
526 let remote = resolve_remote_argument(config.as_ref(), &remote_name)
527 .map_err(PullError::Remote)?;
528 let fetch_outcome = remote
529 .fetch(repo, fetch_opts, progress, cancel, http_factory)
530 .map_err(PullError::Remote)?;
531 if fetch_outcome.has_rejections() {
532 return Err(PullError::Repository(Error::Message(
533 "some ref updates from the remote were rejected".into(),
534 )));
535 }
536 transfer = fetch_outcome.transfer;
537 }
538
539 if fetch_dry_run {
540 return Ok(PullOutcome {
541 oid: head_oid,
542 change_id: head_oid
543 .and_then(|oid| logical_change_id_for_commit(repo, oid).ok())
544 .flatten(),
545 ..PullOutcome::new(PullResult::UpToDate, &short_name)
546 });
547 }
548
549 let (label, upstream_oid, upstream_full_ref) = match &options.upstream {
550 Some(fetched) => (
551 fetched.label.clone(),
552 fetched.oid,
553 resolve_fork_point_upstream_ref(repo, &fetched.label),
554 ),
555 None => {
556 let upstream =
557 resolve_pull_integration_base(repo, &short_name, &remote_name, explicit_fetch)
558 .ok_or(PullError::NoUpstream {
559 branch: short_name.clone(),
560 })?;
561 (upstream.name, upstream.oid, upstream.full_ref)
562 }
563 };
564
565 let landed = options.upstream.as_ref().and_then(|u| u.landed);
566 if options.check {
567 return pull_check_outcome(
568 repo,
569 head_oid,
570 upstream_oid,
571 &label,
572 &upstream_full_ref,
573 integration,
574 landed,
575 );
576 }
577
578 match head_oid {
579 None => adopt_unborn_upstream(repo, &refname, &short_name, &label, upstream_oid),
580 Some(head_oid) => integrate_fetched_upstream(
581 repo,
582 &refname,
583 head_oid,
584 Upstream {
585 oid: upstream_oid,
586 label: &label,
587 full_ref: &upstream_full_ref,
588 landed,
589 copies: options
590 .upstream
591 .as_ref()
592 .map_or(&[], |u| u.copies.as_slice()),
593 },
594 integration,
595 stop_on_conflict,
596 progress,
597 cancel,
598 ),
599 }
600 })();
601 let core = core.map(|outcome| PullOutcome {
602 transfer,
603 ..outcome
604 });
605
606 match core {
607 Err(err) => {
608 match stash_oid.as_ref() {
611 Some(oid) if !merge_was_in_progress && merge_in_progress() => {
612 record_merge_autostash(repo, oid).map_err(PullError::Repository)?;
613 }
614 _ => restore_autostash_after_abort(repo, stash_oid.as_ref()),
615 }
616 Err(err)
617 }
618 Ok(outcome) => {
619 if fetch_dry_run {
620 restore_autostash_after_abort(repo, stash_oid.as_ref());
621 return Ok(outcome);
622 }
623 if outcome.result == PullResult::RebaseConflicts {
624 return Ok(PullOutcome {
627 autostash: stash_oid,
628 ..outcome
629 });
630 }
631 if matches!(
632 outcome.result,
633 PullResult::Conflicts | PullResult::RefusedNotFf | PullResult::RebaseStopped
634 ) {
635 restore_autostash_after_abort(repo, stash_oid.as_ref());
636 return Ok(outcome);
637 }
638 apply_autostash_after_pull(repo, stash_oid.as_ref(), Ok(outcome))
639 }
640 }
641}
642
643fn pull_fetch_refspecs(options: &PullOptions) -> (bool, Vec<String>) {
645 let mut refspecs = options.fetch.refspecs.clone();
646 let mut explicit = options.explicit_fetch;
647 if let Some(branch) = options.branch.as_deref().filter(|b| !b.is_empty()) {
648 if refspecs.is_empty() {
649 refspecs.push(branch.to_owned());
650 }
651 explicit = true;
652 }
653 (explicit, refspecs)
654}
655
656fn restore_autostash_after_abort(repo: &Repository, stash_oid: Option<&ObjectId>) {
657 let Some(stash_oid) = stash_oid else {
658 return;
659 };
660 let _ = restore_operation_autostash(repo, stash_oid);
661}
662
663fn apply_autostash_after_pull(
664 repo: &Repository,
665 stash_oid: Option<&ObjectId>,
666 outcome: std::result::Result<PullOutcome, PullError>,
667) -> std::result::Result<PullOutcome, PullError> {
668 let mut outcome = outcome?;
669 if stash_oid.is_none() {
670 return Ok(outcome);
671 }
672 if !matches!(
673 outcome.result,
674 PullResult::UpToDate
675 | PullResult::FastForward
676 | PullResult::Merged
677 | PullResult::Rebased
678 | PullResult::Landed
679 | PullResult::SetUpstream
680 ) {
681 return Ok(outcome);
682 }
683 if let Some(stash_oid) = stash_oid {
684 if apply_operation_autostash(repo, stash_oid).map_err(PullError::Repository)? {
685 outcome.result = PullResult::AutostashConflict;
686 outcome.autostash = Some(*stash_oid);
687 }
688 }
689 Ok(outcome)
690}
691
692fn pull_check_outcome(
693 repo: &Repository,
694 head_oid: Option<ObjectId>,
695 upstream_oid: ObjectId,
696 label: &str,
697 upstream_full_ref: &str,
698 integration: PullIntegration,
699 landed: Option<ObjectId>,
700) -> std::result::Result<PullOutcome, PullError> {
701 let (ahead, behind, would, fast_forward_possible) = match head_oid {
702 None => (0, 0, PullResult::SetUpstream, true),
703 Some(head_oid) => {
704 let (ahead, behind) = count_symmetric_ahead_behind(repo, head_oid, upstream_oid)
705 .map_err(PullError::Repository)?;
706 let would = if landed == Some(head_oid) && diverged(repo, head_oid, upstream_oid)? {
707 PullResult::Landed
708 } else {
709 predict_pull_result(
710 repo,
711 head_oid,
712 upstream_oid,
713 upstream_full_ref,
714 integration,
715 landed,
716 )?
717 };
718 let up_to_date = head_oid == upstream_oid || is_ancestor(repo, upstream_oid, head_oid)?;
719 let fast_forward = !up_to_date
720 && is_ancestor(repo, head_oid, upstream_oid).map_err(PullError::Repository)?;
721 (ahead, behind, would, up_to_date || fast_forward)
722 }
723 };
724 Ok(PullOutcome {
725 oid: head_oid,
726 change_id: head_oid
727 .and_then(|oid| logical_change_id_for_commit(repo, oid).ok())
728 .flatten(),
729 ahead: Some(ahead),
730 behind: Some(behind),
731 check: Some(PullCheck {
732 ahead,
733 behind,
734 fast_forward_possible,
735 would,
736 }),
737 ..PullOutcome::new(would, label)
738 })
739}
740
741fn resolve_fork_point_upstream_ref(repo: &Repository, label: &str) -> String {
742 crate::merge_base::resolve_fork_point_reflog_ref(repo, label)
743}
744
745fn predict_pull_result(
746 repo: &Repository,
747 head_oid: ObjectId,
748 upstream_oid: ObjectId,
749 upstream_full_ref: &str,
750 integration: PullIntegration,
751 landed: Option<ObjectId>,
752) -> std::result::Result<PullResult, PullError> {
753 if head_oid == upstream_oid || is_ancestor(repo, upstream_oid, head_oid)? {
754 return Ok(PullResult::UpToDate);
755 }
756 if integration.allow_fast_forward && is_ancestor(repo, head_oid, upstream_oid)? {
757 return Ok(PullResult::FastForward);
758 }
759 let diverged = !is_ancestor(repo, upstream_oid, head_oid)?
760 && !is_ancestor(repo, head_oid, upstream_oid)?
761 && head_oid != upstream_oid;
762 if diverged {
763 return match integration.mode {
764 PullMode::FfOnly => Ok(PullResult::RefusedNotFf),
765 PullMode::Rebase => {
766 let mut opts = pull_rebase_options(repo)?;
767 opts.rewrite.sign = false;
768 let request = rebase_request_after_fetch(
769 repo,
770 head_oid,
771 upstream_oid,
772 upstream_full_ref,
773 landed,
774 true,
775 )
776 .map_err(map_history_error)?;
777 match plan_rebase_check(repo, &request, &opts).map_err(map_history_error)? {
778 RebaseCheckPlan::UpToDate | RebaseCheckPlan::Clean { .. } => {
779 Ok(PullResult::Rebased)
780 }
781 RebaseCheckPlan::Conflict { .. } => Ok(PullResult::RebaseConflicts),
782 }
783 }
784 PullMode::Merge => match predict_merge(
785 repo,
786 head_oid,
787 upstream_oid,
788 &MergePredictionOptions {
789 allow_fast_forward: integration.allow_fast_forward,
790 theirs_label: "upstream".into(),
791 ..Default::default()
792 },
793 )
794 .map_err(PullError::Repository)?
795 {
796 MergePrediction::UpToDate => Ok(PullResult::UpToDate),
797 MergePrediction::FastForward => Ok(PullResult::FastForward),
798 MergePrediction::Clean { .. } => Ok(PullResult::Merged),
799 MergePrediction::Conflicts { .. } => Ok(PullResult::Conflicts),
800 MergePrediction::Unrelated => Ok(PullResult::Conflicts),
801 },
802 };
803 }
804 Ok(PullResult::Merged)
805}
806
807fn adopt_unborn_upstream(
808 repo: &Repository,
809 refname: &str,
810 short_name: &str,
811 label: &str,
812 upstream_oid: ObjectId,
813) -> std::result::Result<PullOutcome, PullError> {
814 ensure_branch_tip(repo, refname, None)?;
815 let upstream_tree = repo.odb().commit_tree(&upstream_oid)?;
816 prepare_tree_checkout(repo, None, &upstream_tree).map_err(PullError::Repository)?;
817 checkout_between_trees(repo, None, &upstream_tree).map_err(PullError::Repository)?;
818 update_checked_out_branch(repo, refname, None, upstream_oid, INITIAL_PULL_REFLOG)?;
819 Ok(PullOutcome {
820 upstream: Some(label.to_owned()),
821 ..PullOutcome::at_commit(PullResult::SetUpstream, short_name, repo, upstream_oid)?
822 })
823}
824
825fn diverged(
827 repo: &Repository,
828 head_oid: ObjectId,
829 upstream_oid: ObjectId,
830) -> std::result::Result<bool, PullError> {
831 Ok(head_oid != upstream_oid
832 && !is_ancestor(repo, upstream_oid, head_oid).map_err(PullError::Repository)?
833 && !is_ancestor(repo, head_oid, upstream_oid).map_err(PullError::Repository)?)
834}
835
836#[derive(Clone, Copy)]
838struct Upstream<'a> {
839 oid: ObjectId,
840 label: &'a str,
841 full_ref: &'a str,
843 landed: Option<ObjectId>,
845 copies: &'a [ObjectId],
847}
848
849#[allow(clippy::too_many_arguments)]
850fn integrate_fetched_upstream(
851 repo: &Repository,
852 refname: &str,
853 head_oid: ObjectId,
854 upstream: Upstream<'_>,
855 integration: PullIntegration,
856 stop_on_conflict: bool,
857 progress: &mut dyn ProgressSink,
858 cancel: &dyn Cancel,
859) -> std::result::Result<PullOutcome, PullError> {
860 let Upstream {
861 oid: upstream_oid,
862 label,
863 landed,
864 ..
865 } = upstream;
866 let diverged = diverged(repo, head_oid, upstream_oid)?;
867
868 if diverged && landed == Some(head_oid) {
869 return move_landed_branch(
870 repo,
871 refname,
872 head_oid,
873 upstream_oid,
874 label,
875 progress,
876 cancel,
877 );
878 }
879
880 if diverged {
881 match integration.mode {
882 PullMode::FfOnly => {
883 let (ahead, behind) = count_symmetric_ahead_behind(repo, head_oid, upstream_oid)
884 .map_err(PullError::Repository)?;
885 return Ok(PullOutcome {
886 ahead: Some(ahead),
887 behind: Some(behind),
888 ..PullOutcome::at_commit(PullResult::RefusedNotFf, label, repo, head_oid)?
889 });
890 }
891 PullMode::Rebase => {
892 return rebase_fetched_upstream(
893 repo,
894 refname,
895 head_oid,
896 upstream,
897 stop_on_conflict,
898 progress,
899 cancel,
900 );
901 }
902 PullMode::Merge => {}
903 }
904 }
905
906 let integrate = integrate_branch_with_options(
907 repo,
908 refname,
909 head_oid,
910 upstream_oid,
911 label,
912 IntegrateBranchOptions {
913 allow_fast_forward: integration.allow_fast_forward,
914 },
915 progress,
916 cancel,
917 )
918 .map_err(PullError::Repository)?;
919 Ok(map_integrate_outcome(integrate, label))
920}
921
922#[allow(clippy::too_many_arguments)]
923fn rebase_fetched_upstream(
924 repo: &Repository,
925 refname: &str,
926 head_oid: ObjectId,
927 upstream: Upstream<'_>,
928 stop_on_conflict: bool,
929 progress: &mut dyn ProgressSink,
930 cancel: &dyn Cancel,
931) -> std::result::Result<PullOutcome, PullError> {
932 let Upstream {
933 oid: upstream_oid,
934 label,
935 full_ref,
936 landed,
937 copies,
938 } = upstream;
939 let request = rebase_request_after_fetch(repo, head_oid, upstream_oid, full_ref, landed, true)
940 .map_err(map_history_error)?;
941
942 let mut rebase = pull_rebase_options(repo)?;
943 rebase.drop = copies.to_vec();
945 let pre_rebase = PreRebase::Run {
947 upstream: Some(request.upstream.to_hex()),
948 };
949 let start = StartRebaseOptions {
950 request,
951 rebase,
952 apply: ApplyOptions {
953 identity: repo
954 .identity(IdentRole::Committer, repo.identity_now())
955 .map_err(PullError::Repository)?,
956 reflog_message: format!("pull: rebase onto {label}"),
957 record_operation: Some("rebase".into()),
958 hooks: crate::hooks::PostOperationHooks::Run,
959 },
960 pre_rebase,
961 };
962 match start_rebase(repo, &start, progress, cancel).map_err(map_rebase_session_error)? {
963 RebaseOutcome::Stopped(stop) if stop_on_conflict => Ok(PullOutcome {
964 conflicts: Some(stop.conflicts),
965 stopped_commit: Some(stop.commit),
966 ..PullOutcome::at_commit(PullResult::RebaseStopped, label, repo, head_oid)?
967 }),
968 RebaseOutcome::Stopped(stop) => Ok(PullOutcome {
969 conflicts: Some(stop.conflicts),
970 stopped_commit: Some(stop.commit),
971 ..PullOutcome::new(PullResult::RebaseConflicts, label)
972 }),
973 RebaseOutcome::Done { applied, .. } | RebaseOutcome::ExecDone { applied, .. }
974 if applied.head != head_oid =>
975 {
976 Ok(PullOutcome::at_commit(
977 PullResult::Rebased,
978 label,
979 repo,
980 applied.head,
981 )?)
982 }
983 RebaseOutcome::Done { .. } | RebaseOutcome::ExecDone { .. } => {
986 let integrate = integrate_branch_with_options(
987 repo,
988 refname,
989 head_oid,
990 upstream_oid,
991 label,
992 IntegrateBranchOptions {
993 allow_fast_forward: true,
994 },
995 progress,
996 cancel,
997 )
998 .map_err(PullError::Repository)?;
999 Ok(map_integrate_outcome(integrate, label))
1000 }
1001 }
1002}
1003
1004fn pull_rebase_options(repo: &Repository) -> std::result::Result<RebaseOntoOptions, PullError> {
1006 let committer = repo
1007 .identity(IdentRole::Committer, repo.identity_now())
1008 .map_err(PullError::Repository)?;
1009 let mut opts = RebaseOntoOptions::new(RewriteOptions {
1010 scope: RefScope::Head,
1011 empty: EmptyPolicy::Drop,
1012 committer,
1013 preserve_committer: false,
1014 sign: true,
1015 });
1016 opts.flatten_leaf_merges = true;
1017 Ok(opts)
1018}
1019
1020#[allow(clippy::too_many_arguments)]
1025fn move_landed_branch(
1026 repo: &Repository,
1027 refname: &str,
1028 head_oid: ObjectId,
1029 upstream_oid: ObjectId,
1030 label: &str,
1031 progress: &mut dyn ProgressSink,
1032 cancel: &dyn Cancel,
1033) -> std::result::Result<PullOutcome, PullError> {
1034 ensure_branch_tip(repo, refname, Some(head_oid))?;
1035 if repo.load_index()?.has_unmerged_entries() {
1036 return Err(Error::IndexUnmerged.into());
1037 }
1038 let from = repo.odb().commit_tree(&head_oid)?;
1039 let to = repo.odb().commit_tree(&upstream_oid)?;
1040 checkout_tree_transition_allow_local_reset(repo, Some(&from), &to, progress, cancel).map_err(
1041 |e| match e {
1042 Error::WouldOverwrite {
1043 reason: OverwriteReason::LocalChanges,
1044 paths,
1045 ..
1046 } => local_changes_would_be_overwritten(paths),
1047 other => other,
1048 },
1049 )?;
1050 update_checked_out_branch(
1051 repo,
1052 refname,
1053 Some(head_oid),
1054 upstream_oid,
1055 &format!("pull: move to {label}, which has the branch's commits"),
1056 )?;
1057 crate::state::write_orig_head(repo, head_oid)?;
1058 Ok(PullOutcome::at_commit(
1059 PullResult::Landed,
1060 label,
1061 repo,
1062 upstream_oid,
1063 )?)
1064}
1065
1066fn local_changes_would_be_overwritten(paths: Vec<String>) -> Error {
1068 Error::WouldOverwrite {
1069 reason: OverwriteReason::LocalChanges,
1070 paths,
1071 message: "local changes would be overwritten".to_owned(),
1072 }
1073}
1074
1075fn map_history_error(err: HistoryError) -> PullError {
1076 match err {
1077 HistoryError::Library(e) => PullError::Repository(*e),
1078 HistoryError::LocalChangesWouldBeOverwritten { paths } => {
1079 PullError::Repository(local_changes_would_be_overwritten(paths))
1080 }
1081 other => PullError::Repository(Error::Message(other.to_string())),
1082 }
1083}
1084
1085fn map_rebase_session_error(err: RebaseSessionError) -> PullError {
1086 match err {
1087 RebaseSessionError::Library(e) => PullError::Repository(e),
1088 RebaseSessionError::History(e) => map_history_error(e),
1089 other => PullError::Rebase(other),
1090 }
1091}
1092
1093fn map_integrate_outcome(outcome: IntegrateBranchOutcome, label: &str) -> PullOutcome {
1094 match outcome {
1095 IntegrateBranchOutcome::UpToDate => PullOutcome::new(PullResult::UpToDate, label),
1096 IntegrateBranchOutcome::FastForward { oid, change_id } => PullOutcome {
1097 oid: Some(oid),
1098 change_id,
1099 ..PullOutcome::new(PullResult::FastForward, label)
1100 },
1101 IntegrateBranchOutcome::Merged { oid, change_id } => PullOutcome {
1102 oid: Some(oid),
1103 change_id,
1104 ..PullOutcome::new(PullResult::Merged, label)
1105 },
1106 IntegrateBranchOutcome::Conflicts { details, .. } => PullOutcome {
1107 conflicts: Some(details),
1108 ..PullOutcome::new(PullResult::Conflicts, label)
1109 },
1110 IntegrateBranchOutcome::Squashed | IntegrateBranchOutcome::Stopped => {
1111 unreachable!("pull does not use squash or no-commit merge modes")
1112 }
1113 }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 use super::*;
1119 use crate::config::environment::RepositoryOptions;
1120 use crate::config::ConfigSet;
1121 use crate::environment::Environment;
1122 use crate::progress::NeverCancel;
1123 use crate::remote::fetch::NoProgress;
1124 use std::path::{Path, PathBuf};
1125 use std::process::{Command, Output};
1126 use tempfile::TempDir;
1127
1128 fn git_output(dir: &Path, args: &[&str]) -> Output {
1129 Command::new("git")
1130 .current_dir(dir)
1131 .args(args)
1132 .env("GIT_CONFIG_GLOBAL", "/dev/null")
1133 .env("GIT_CONFIG_SYSTEM", "/dev/null")
1134 .env("GIT_AUTHOR_NAME", "T")
1135 .env("GIT_AUTHOR_EMAIL", "t@example.com")
1136 .env("GIT_COMMITTER_NAME", "T")
1137 .env("GIT_COMMITTER_EMAIL", "t@example.com")
1138 .output()
1139 .expect("run git")
1140 }
1141
1142 fn git(dir: &Path, args: &[&str]) -> String {
1143 let out = git_output(dir, args);
1144 assert!(
1145 out.status.success(),
1146 "git {args:?} failed: {}",
1147 String::from_utf8_lossy(&out.stderr)
1148 );
1149 String::from_utf8_lossy(&out.stdout).trim().to_owned()
1150 }
1151
1152 fn open(dir: &Path) -> Repository {
1153 Repository::discover(Some(dir), &RepositoryOptions::empty()).expect("open")
1154 }
1155
1156 fn commit(dir: &Path, file: &str, contents: &str) -> ObjectId {
1157 std::fs::write(dir.join(file), contents).expect("write");
1158 git(dir, &["add", file]);
1159 git(dir, &["commit", "-qm", contents]);
1160 ObjectId::from_hex(&git(dir, &["rev-parse", "HEAD"])).expect("oid")
1161 }
1162
1163 fn origin_and_clone(tmp: &TempDir) -> (PathBuf, PathBuf) {
1164 let origin = tmp.path().join("origin");
1165 std::fs::create_dir_all(&origin).expect("mkdir origin");
1166 git(&origin, &["init", "-q", "-b", "main"]);
1167 commit(&origin, "a.txt", "one");
1168 git(tmp.path(), &["clone", "-q", "origin", "clone"]);
1169 (origin, tmp.path().join("clone"))
1170 }
1171
1172 fn pull_options(repo: &Repository) -> PullOptions {
1173 PullOptions::for_working_branch(repo, Environment::empty(), None).expect("options")
1174 }
1175
1176 fn cfg(pairs: &[(&str, &str)]) -> ConfigSet {
1177 let mut set = ConfigSet::new();
1178 for (key, value) in pairs {
1179 set.add_command_override(key, value).expect("override");
1180 }
1181 set
1182 }
1183
1184 #[test]
1185 fn resolve_pull_mode_cli_ff_only_wins() {
1186 let config = cfg(&[("pull.rebase", "true")]);
1187 assert_eq!(
1188 resolve_pull_mode(&config, Some("main"), Some(PullMode::FfOnly)).expect("mode"),
1189 PullMode::FfOnly
1190 );
1191 }
1192
1193 #[test]
1194 fn resolve_pull_mode_no_rebase_overrides_rebase_config() {
1195 let config = cfg(&[("pull.rebase", "true")]);
1196 assert_eq!(
1197 resolve_pull_mode(&config, Some("main"), Some(PullMode::Merge)).expect("mode"),
1198 PullMode::Merge
1199 );
1200 }
1201
1202 #[test]
1203 fn resolve_pull_mode_rebase_from_config() {
1204 let config = cfg(&[("pull.rebase", "merges")]);
1205 assert_eq!(
1206 resolve_pull_mode(&config, Some("main"), None).expect("mode"),
1207 PullMode::Rebase
1208 );
1209 }
1210
1211 #[test]
1212 fn resolve_pull_mode_branch_rebase_overrides_global() {
1213 let config = cfg(&[("pull.rebase", "false"), ("branch.topic.rebase", "true")]);
1214 assert_eq!(
1215 resolve_pull_mode(&config, Some("topic"), None).expect("mode"),
1216 PullMode::Rebase
1217 );
1218 }
1219
1220 #[test]
1221 fn resolve_pull_mode_interactive_is_error() {
1222 let config = cfg(&[("pull.rebase", "interactive")]);
1223 assert!(matches!(
1224 resolve_pull_mode(&config, Some("main"), None),
1225 Err(PullError::RebaseInteractive)
1226 ));
1227 }
1228
1229 #[test]
1230 fn resolve_pull_mode_ff_only_from_config() {
1231 let config = cfg(&[("pull.ff", "only")]);
1232 assert_eq!(
1233 resolve_pull_mode(&config, Some("main"), None).expect("mode"),
1234 PullMode::FfOnly
1235 );
1236 }
1237
1238 #[test]
1239 fn resolve_pull_mode_ff_only_beats_rebase_config() {
1240 let config = cfg(&[("pull.ff", "only"), ("pull.rebase", "true")]);
1241 assert_eq!(
1242 resolve_pull_mode(&config, Some("main"), None).expect("mode"),
1243 PullMode::FfOnly
1244 );
1245 }
1246
1247 #[test]
1248 fn resolve_pull_mode_defaults_to_merge() {
1249 let config = ConfigSet::new();
1250 assert_eq!(
1251 resolve_pull_mode(&config, Some("main"), None).expect("mode"),
1252 PullMode::Merge
1253 );
1254 }
1255
1256 #[test]
1257 fn resolve_pull_integration_ff_false_disables_fast_forward() {
1258 let config = cfg(&[("pull.ff", "false")]);
1259 let resolved = resolve_pull_integration(&config, Some("main"), None).expect("resolved");
1260 assert_eq!(resolved.mode, PullMode::Merge);
1261 assert!(!resolved.allow_fast_forward);
1262 }
1263
1264 #[cfg_attr(
1265 not(feature = "full-tests"),
1266 ignore = "starts system git or is heavy; run with --features full-tests"
1267 )]
1268 #[test]
1269 fn fast_forward_pull_refuses_overlapping_local_changes() {
1270 let tmp = TempDir::new().expect("tempdir");
1271 let (origin, clone) = origin_and_clone(&tmp);
1272 let tip_before = git(&clone, &["rev-parse", "main"]);
1273 commit(&origin, "a.txt", "two");
1274 std::fs::write(clone.join("a.txt"), "local edit\n").expect("write");
1275 let repo = open(&clone);
1276 let err = pull(
1277 &repo,
1278 &pull_options(&repo),
1279 &mut NoProgress,
1280 &NeverCancel,
1281 None,
1282 )
1283 .expect_err("blocked");
1284 assert!(
1285 matches!(
1286 &err,
1287 PullError::Repository(Error::WouldOverwrite { paths, .. }) if paths == &["a.txt"]
1288 ),
1289 "{err:?}"
1290 );
1291 assert_eq!(git(&clone, &["rev-parse", "main"]), tip_before);
1292 assert!(
1293 git(&clone, &["status", "--porcelain"]).contains("M a.txt"),
1294 "worktree still dirty"
1295 );
1296 }
1297
1298 #[cfg_attr(
1299 not(feature = "full-tests"),
1300 ignore = "starts system git or is heavy; run with --features full-tests"
1301 )]
1302 #[test]
1303 fn pull_maps_custom_fetch_refspec() {
1304 let tmp = TempDir::new().expect("tempdir");
1305 let (origin, clone) = origin_and_clone(&tmp);
1306 let tip = commit(&origin, "b.txt", "two");
1307 git(
1308 &clone,
1309 &[
1310 "config",
1311 "--replace-all",
1312 "remote.origin.fetch",
1313 "+refs/heads/*:refs/remotes/mirror/*",
1314 ],
1315 );
1316
1317 let repo = open(&clone);
1318 let outcome = pull(
1319 &repo,
1320 &pull_options(&repo),
1321 &mut NoProgress,
1322 &NeverCancel,
1323 None,
1324 )
1325 .expect("pull succeeds");
1326 assert_eq!(outcome.result, PullResult::FastForward);
1327 assert_eq!(outcome.oid, Some(tip));
1328 assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1329 }
1330
1331 #[cfg_attr(
1332 not(feature = "full-tests"),
1333 ignore = "starts system git or is heavy; run with --features full-tests"
1334 )]
1335 #[test]
1336 fn pull_local_upstream_skips_fetch() {
1337 let tmp = TempDir::new().expect("tempdir");
1338 let dir = tmp.path();
1339 git(dir, &["init", "-q", "-b", "main"]);
1340 commit(dir, "a.txt", "one");
1341 git(dir, &["checkout", "-q", "-b", "dev"]);
1342 let tip = commit(dir, "b.txt", "two");
1343 git(dir, &["checkout", "-q", "main"]);
1344 git(dir, &["branch", "--set-upstream-to", "dev", "main"]);
1345
1346 let repo = open(dir);
1347 let outcome = pull(
1348 &repo,
1349 &pull_options(&repo),
1350 &mut NoProgress,
1351 &NeverCancel,
1352 None,
1353 )
1354 .expect("local pull succeeds");
1355 assert_eq!(outcome.result, PullResult::FastForward);
1356 assert_eq!(outcome.oid, Some(tip));
1357 assert_eq!(git(dir, &["rev-parse", "main"]), tip.to_hex());
1358 assert_eq!(outcome.transfer, TransferStats::default());
1359 }
1360
1361 #[cfg_attr(
1362 not(feature = "full-tests"),
1363 ignore = "starts system git or is heavy; run with --features full-tests"
1364 )]
1365 #[test]
1366 fn pull_reports_objects_received_by_its_fetch() {
1367 let tmp = TempDir::new().expect("tempdir");
1368 let (origin, clone) = origin_and_clone(&tmp);
1369 commit(&origin, "b.txt", "two");
1370
1371 let repo = open(&clone);
1372 let outcome = pull(
1373 &repo,
1374 &pull_options(&repo),
1375 &mut NoProgress,
1376 &NeverCancel,
1377 None,
1378 )
1379 .expect("pull succeeds");
1380 assert_eq!(outcome.result, PullResult::FastForward);
1381 assert_eq!(outcome.transfer.objects, 3, "{:?}", outcome.transfer);
1383 assert!(outcome.transfer.bytes > 0, "{:?}", outcome.transfer);
1384
1385 let again = pull(
1386 &repo,
1387 &pull_options(&repo),
1388 &mut NoProgress,
1389 &NeverCancel,
1390 None,
1391 )
1392 .expect("second pull");
1393 assert_eq!(again.result, PullResult::UpToDate);
1394 assert_eq!(again.transfer.objects, 0, "{:?}", again.transfer);
1395 }
1396
1397 #[cfg_attr(
1398 not(feature = "full-tests"),
1399 ignore = "starts system git or is heavy; run with --features full-tests"
1400 )]
1401 #[test]
1402 fn pull_adopts_upstream_on_unborn_branch_with_reflog() {
1403 let tmp = TempDir::new().expect("tempdir");
1404 let origin = tmp.path().join("origin");
1405 std::fs::create_dir_all(&origin).expect("mkdir origin");
1406 git(&origin, &["init", "-q", "-b", "main"]);
1407 let tip = commit(&origin, "a.txt", "one");
1408 let local = tmp.path().join("local");
1409 std::fs::create_dir_all(&local).expect("mkdir local");
1410 git(&local, &["init", "-q", "-b", "main"]);
1411 git(&local, &["remote", "add", "origin", "../origin"]);
1412
1413 let repo = open(&local);
1414 let outcome = pull(
1415 &repo,
1416 &pull_options(&repo),
1417 &mut NoProgress,
1418 &NeverCancel,
1419 None,
1420 )
1421 .expect("pull succeeds");
1422 assert_eq!(outcome.result, PullResult::SetUpstream);
1423 assert_eq!(outcome.oid, Some(tip));
1424 assert_eq!(
1425 git(&local, &["reflog", "show", "--format=%H %gs", "-1", "main"]),
1426 format!("{} {INITIAL_PULL_REFLOG}", tip.to_hex())
1427 );
1428 }
1429
1430 #[cfg_attr(
1431 not(feature = "full-tests"),
1432 ignore = "starts system git or is heavy; run with --features full-tests"
1433 )]
1434 #[test]
1435 fn pull_unborn_branch_skips_autostash_with_cli_flag() {
1436 let tmp = TempDir::new().expect("tempdir");
1437 let origin = tmp.path().join("origin");
1438 std::fs::create_dir_all(&origin).expect("mkdir origin");
1439 git(&origin, &["init", "-q", "-b", "main"]);
1440 let tip = commit(&origin, "a.txt", "one");
1441 let local = tmp.path().join("local");
1442 std::fs::create_dir_all(&local).expect("mkdir local");
1443 git(&local, &["init", "-q", "-b", "main"]);
1444 git(&local, &["remote", "add", "origin", "../origin"]);
1445
1446 let repo = open(&local);
1447 let mut options = pull_options(&repo);
1448 options.autostash = true;
1449 let outcome = pull(&repo, &options, &mut NoProgress, &NeverCancel, None)
1450 .expect("pull with --autostash on unborn branch");
1451 assert_eq!(outcome.result, PullResult::SetUpstream);
1452 assert_eq!(outcome.oid, Some(tip));
1453 assert!(
1454 git_output(&local, &["rev-parse", "refs/stash"])
1455 .status
1456 .code()
1457 == Some(128),
1458 "autostash must not create refs/stash without a commit"
1459 );
1460 }
1461
1462 #[cfg_attr(
1463 not(feature = "full-tests"),
1464 ignore = "starts system git or is heavy; run with --features full-tests"
1465 )]
1466 #[test]
1467 fn pull_unborn_branch_skips_autostash_from_config() {
1468 let tmp = TempDir::new().expect("tempdir");
1469 let origin = tmp.path().join("origin");
1470 std::fs::create_dir_all(&origin).expect("mkdir origin");
1471 git(&origin, &["init", "-q", "-b", "main"]);
1472 let tip = commit(&origin, "a.txt", "one");
1473 let local = tmp.path().join("local");
1474 std::fs::create_dir_all(&local).expect("mkdir local");
1475 git(&local, &["init", "-q", "-b", "main"]);
1476 git(&local, &["remote", "add", "origin", "../origin"]);
1477 git(&local, &["config", "rebase.autoStash", "true"]);
1478 git(&local, &["config", "pull.rebase", "true"]);
1479
1480 let repo = open(&local);
1481 let outcome = pull(
1482 &repo,
1483 &pull_options(&repo),
1484 &mut NoProgress,
1485 &NeverCancel,
1486 None,
1487 )
1488 .expect("pull with rebase.autoStash on unborn branch");
1489 assert_eq!(outcome.result, PullResult::SetUpstream);
1490 assert_eq!(outcome.oid, Some(tip));
1491 assert!(
1492 git_output(&local, &["rev-parse", "refs/stash"])
1493 .status
1494 .code()
1495 == Some(128),
1496 "config autostash must not run without a commit"
1497 );
1498 }
1499
1500 #[cfg_attr(
1501 not(feature = "full-tests"),
1502 ignore = "starts system git or is heavy; run with --features full-tests"
1503 )]
1504 #[test]
1505 fn pull_unborn_branch_with_autostash_still_refuses_overlapping_paths() {
1506 let tmp = TempDir::new().expect("tempdir");
1507 let origin = tmp.path().join("origin");
1508 std::fs::create_dir_all(&origin).expect("mkdir origin");
1509 git(&origin, &["init", "-q", "-b", "main"]);
1510 commit(&origin, "a.txt", "upstream");
1511 let local = tmp.path().join("local");
1512 std::fs::create_dir_all(&local).expect("mkdir local");
1513 git(&local, &["init", "-q", "-b", "main"]);
1514 git(&local, &["remote", "add", "origin", "../origin"]);
1515 std::fs::write(local.join("a.txt"), "local\n").expect("write");
1516
1517 let repo = open(&local);
1518 let mut options = pull_options(&repo);
1519 options.autostash = true;
1520 let err = pull(&repo, &options, &mut NoProgress, &NeverCancel, None)
1521 .expect_err("checkout collision");
1522 assert!(
1523 matches!(
1524 &err,
1525 PullError::Repository(Error::WouldOverwrite { paths, .. }) if paths == &["a.txt"]
1526 ),
1527 "{err:?}"
1528 );
1529 assert!(
1530 git_output(&local, &["rev-parse", "refs/stash"])
1531 .status
1532 .code()
1533 == Some(128),
1534 "must not stash when HEAD is unborn"
1535 );
1536 }
1537
1538 #[cfg_attr(
1539 not(feature = "full-tests"),
1540 ignore = "starts system git or is heavy; run with --features full-tests"
1541 )]
1542 #[test]
1543 fn pull_integrates_a_fetched_upstream_without_fetching() {
1544 let tmp = TempDir::new().expect("tempdir");
1545 let (_origin, clone) = origin_and_clone(&tmp);
1546 git(&clone, &["checkout", "-q", "-b", "up"]);
1547 let tip = commit(&clone, "b.txt", "two");
1548 git(&clone, &["checkout", "-q", "main"]);
1549 git(
1551 &clone,
1552 &["remote", "set-url", "origin", "/nonexistent/remote"],
1553 );
1554
1555 let repo = open(&clone);
1556 let mut options = pull_options(&repo);
1557 options.upstream = Some(FetchedUpstream {
1558 label: "origin/up".to_owned(),
1559 oid: tip,
1560 landed: None,
1561 copies: Vec::new(),
1562 });
1563 let outcome = pull(&repo, &options, &mut NoProgress, &NeverCancel, None)
1564 .expect("pull without a fetch");
1565 assert_eq!(outcome.result, PullResult::FastForward);
1566 assert_eq!(outcome.label, "origin/up");
1567 assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1568 assert_eq!(outcome.transfer, TransferStats::default());
1569 }
1570
1571 fn conflicting_clone(tmp: &TempDir) -> (PathBuf, ObjectId, ObjectId) {
1573 let (origin, clone) = origin_and_clone(tmp);
1574 let upstream = commit(&origin, "a.txt", "theirs");
1575 let local = commit(&clone, "a.txt", "ours");
1576 git(&clone, &["config", "user.name", "T"]);
1577 git(&clone, &["config", "user.email", "t@example.com"]);
1578 git(&clone, &["fetch", "-q"]);
1579 (clone, local, upstream)
1580 }
1581
1582 fn rebase_options(repo: &Repository, upstream: ObjectId, stop: bool) -> PullOptions {
1583 let mut options = pull_options(repo);
1584 options.mode = Some(PullMode::Rebase);
1585 options.stop_on_conflict = stop;
1586 options.upstream = Some(FetchedUpstream {
1587 label: "origin/main".to_owned(),
1588 oid: upstream,
1589 landed: None,
1590 copies: Vec::new(),
1591 });
1592 options
1593 }
1594
1595 fn abort(repo: &Repository) {
1596 let identity = "T <t@example.com> 0 +0000".parse().expect("signature");
1597 crate::history::abort_rebase(repo, &identity).expect("abort");
1598 }
1599
1600 #[cfg_attr(
1601 not(feature = "full-tests"),
1602 ignore = "starts system git or is heavy; run with --features full-tests"
1603 )]
1604 #[test]
1605 fn a_conflicting_rebase_pull_stops_in_a_resumable_rebase() {
1606 let tmp = TempDir::new().expect("tempdir");
1607 let (clone, local, upstream) = conflicting_clone(&tmp);
1608 let repo = open(&clone);
1609
1610 let plain = pull(
1612 &repo,
1613 &rebase_options(&repo, upstream, false),
1614 &mut NoProgress,
1615 &NeverCancel,
1616 None,
1617 )
1618 .expect("pull");
1619 assert_eq!(plain.result, PullResult::RebaseConflicts);
1620 assert!(clone.join(".git/rebase-merge").exists());
1621 abort(&repo);
1622 assert_eq!(git(&clone, &["rev-parse", "HEAD"]), local.to_hex());
1623
1624 let stopped = pull(
1625 &repo,
1626 &rebase_options(&repo, upstream, true),
1627 &mut NoProgress,
1628 &NeverCancel,
1629 None,
1630 )
1631 .expect("pull");
1632 assert_eq!(stopped.result, PullResult::RebaseStopped);
1633 assert_eq!(stopped.oid, Some(local));
1634 let paths: Vec<&str> = stopped
1635 .conflicts
1636 .iter()
1637 .flatten()
1638 .map(|c| c.path.as_str())
1639 .collect();
1640 assert_eq!(paths, ["a.txt"]);
1641 assert!(clone.join(".git/rebase-merge/grit-plan").is_file());
1643 assert_eq!(git(&clone, &["rev-parse", "main"]), local.to_hex());
1644 assert_eq!(git(&clone, &["rev-parse", "HEAD"]), upstream.to_hex());
1645 assert!(std::fs::read_to_string(clone.join("a.txt"))
1646 .expect("conflicted file")
1647 .contains("<<<<<<<"));
1648 }
1649
1650 #[cfg_attr(
1651 not(feature = "full-tests"),
1652 ignore = "starts system git or is heavy; run with --features full-tests"
1653 )]
1654 #[test]
1655 fn a_rebase_pull_with_local_changes_is_refused() {
1656 let tmp = TempDir::new().expect("tempdir");
1657 let (clone, _, upstream) = conflicting_clone(&tmp);
1658 let local = commit(&clone, "b.txt", "tracked");
1659 std::fs::write(clone.join("b.txt"), "edited, not committed").expect("write");
1660 let repo = open(&clone);
1661 let refused = pull(
1662 &repo,
1663 &rebase_options(&repo, upstream, true),
1664 &mut NoProgress,
1665 &NeverCancel,
1666 None,
1667 );
1668 assert!(
1669 matches!(
1670 refused,
1671 Err(PullError::Repository(Error::DirtyWorktree { .. }))
1672 ),
1673 "{refused:?}"
1674 );
1675 assert!(!clone.join(".git/rebase-merge").exists());
1676 assert_eq!(git(&clone, &["rev-parse", "HEAD"]), local.to_hex());
1677 assert_eq!(
1678 std::fs::read_to_string(clone.join("b.txt")).expect("read"),
1679 "edited, not committed"
1680 );
1681 }
1682
1683 #[cfg_attr(
1684 not(feature = "full-tests"),
1685 ignore = "starts system git or is heavy; run with --features full-tests"
1686 )]
1687 #[test]
1688 fn reflog_failure_aborts_branch_update() {
1689 let tmp = TempDir::new().expect("tempdir");
1690 let (origin, clone) = origin_and_clone(&tmp);
1691 let _tip = commit(&origin, "b.txt", "two");
1692 let before = git(&clone, &["rev-parse", "main"]);
1693 let branch_log = clone.join(".git/logs/refs/heads/main");
1694 std::fs::remove_file(&branch_log).expect("remove branch reflog");
1695 std::fs::create_dir(&branch_log).expect("replace reflog with directory");
1696
1697 let repo = open(&clone);
1698 let err = pull(
1699 &repo,
1700 &pull_options(&repo),
1701 &mut NoProgress,
1702 &NeverCancel,
1703 None,
1704 )
1705 .expect_err("pull fails when branch reflog cannot be written");
1706 assert!(matches!(err, PullError::Repository(_)), "{err:?}");
1707 assert_eq!(git(&clone, &["rev-parse", "main"]), before);
1708 }
1709
1710 fn landed_clone(tmp: &TempDir) -> (PathBuf, ObjectId, ObjectId) {
1714 let (_origin, clone) = origin_and_clone(tmp);
1715 git(&clone, &["config", "user.name", "T"]);
1716 git(&clone, &["config", "user.email", "t@example.com"]);
1717 std::fs::write(clone.join("b.txt"), "b\n").expect("write");
1718 git(&clone, &["add", "b.txt"]);
1719 git(&clone, &["commit", "-qm", "b"]);
1720 let base = git(&clone, &["rev-parse", "HEAD"]);
1721 let mut text = "one".to_owned();
1722 for line in ["1", "2", "3"] {
1723 text = format!("{text}\n{line}");
1724 commit(&clone, "a.txt", &text);
1725 }
1726 let tip = ObjectId::from_hex(&git(&clone, &["rev-parse", "HEAD"])).expect("oid");
1727 git(&clone, &["switch", "-q", "-c", "up", &base]);
1728 let upstream = commit(&clone, "a.txt", &text);
1729 git(&clone, &["switch", "-q", "main"]);
1730 (clone, tip, upstream)
1731 }
1732
1733 fn landed_options(
1734 repo: &Repository,
1735 upstream: ObjectId,
1736 landed: ObjectId,
1737 stop: bool,
1738 ) -> PullOptions {
1739 let mut options = rebase_options(repo, upstream, stop);
1740 if let Some(fetched) = options.upstream.as_mut() {
1741 fetched.landed = Some(landed);
1742 }
1743 options
1744 }
1745
1746 #[cfg_attr(
1747 not(feature = "full-tests"),
1748 ignore = "starts system git or is heavy; run with --features full-tests"
1749 )]
1750 #[test]
1751 fn a_landed_branch_moves_to_the_upstream_and_keeps_local_changes() {
1752 let tmp = TempDir::new().expect("tempdir");
1753 let (clone, tip, upstream) = landed_clone(&tmp);
1754 let repo = open(&clone);
1755 let replayed = pull(
1757 &repo,
1758 &rebase_options(&repo, upstream, false),
1759 &mut NoProgress,
1760 &NeverCancel,
1761 None,
1762 )
1763 .expect("pull");
1764 assert_eq!(replayed.result, PullResult::RebaseConflicts);
1765 abort(&repo);
1766 assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1767
1768 std::fs::write(clone.join("b.txt"), "b, edited\n").expect("write");
1769 std::fs::write(clone.join("draft.txt"), "not committed\n").expect("write");
1770 let mut check = landed_options(&repo, upstream, tip, false);
1771 check.check = true;
1772 let predicted = pull(&repo, &check, &mut NoProgress, &NeverCancel, None).expect("check");
1773 assert_eq!(predicted.check.map(|c| c.would), Some(PullResult::Landed));
1774
1775 git(&clone, &["update-ref", "-d", "ORIG_HEAD"]);
1776 let landed = pull(
1777 &repo,
1778 &landed_options(&repo, upstream, tip, true),
1779 &mut NoProgress,
1780 &NeverCancel,
1781 None,
1782 )
1783 .expect("pull");
1784 assert_eq!(landed.result, PullResult::Landed);
1785 assert_eq!(landed.oid, Some(upstream));
1786 assert_eq!(git(&clone, &["rev-parse", "main"]), upstream.to_hex());
1787 assert_eq!(git(&clone, &["rev-parse", "ORIG_HEAD"]), tip.to_hex());
1788 assert_eq!(
1789 git(&clone, &["status", "--porcelain"]),
1790 "M b.txt\n?? draft.txt"
1791 );
1792 assert_eq!(
1793 git(&clone, &["reflog", "-1", "--format=%gs", "main"]),
1794 "pull: move to origin/main, which has the branch's commits"
1795 );
1796 }
1797
1798 #[cfg_attr(
1799 not(feature = "full-tests"),
1800 ignore = "starts system git or is heavy; run with --features full-tests"
1801 )]
1802 #[test]
1803 fn a_landed_branch_does_not_move_over_local_changes_the_upstream_has_other_content_for() {
1804 let tmp = TempDir::new().expect("tempdir");
1805 let (clone, tip, _) = landed_clone(&tmp);
1806 git(&clone, &["switch", "-q", "up"]);
1808 let upstream = commit(&clone, "b.txt", "b, upstream");
1809 git(&clone, &["switch", "-q", "main"]);
1810 std::fs::write(clone.join("b.txt"), "b, edited\n").expect("write");
1811 let repo = open(&clone);
1812 let err = pull(
1813 &repo,
1814 &landed_options(&repo, upstream, tip, true),
1815 &mut NoProgress,
1816 &NeverCancel,
1817 None,
1818 )
1819 .expect_err("blocked");
1820 assert!(
1821 matches!(
1822 &err,
1823 PullError::Repository(Error::WouldOverwrite { paths, .. }) if paths == &["b.txt"]
1824 ),
1825 "{err:?}"
1826 );
1827 assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1828 assert_eq!(git(&clone, &["status", "--porcelain"]), "M b.txt");
1829 }
1830
1831 #[cfg_attr(
1832 not(feature = "full-tests"),
1833 ignore = "starts system git or is heavy; run with --features full-tests"
1834 )]
1835 #[test]
1836 fn commits_above_a_landed_one_are_rebased_without_it() {
1837 let tmp = TempDir::new().expect("tempdir");
1838 let (clone, landed, upstream) = landed_clone(&tmp);
1839 let own = commit(&clone, "c.txt", "c");
1840 let repo = open(&clone);
1841 let outcome = pull(
1842 &repo,
1843 &landed_options(&repo, upstream, landed, true),
1844 &mut NoProgress,
1845 &NeverCancel,
1846 None,
1847 )
1848 .expect("pull");
1849 assert_eq!(outcome.result, PullResult::Rebased);
1850 assert_eq!(git(&clone, &["rev-parse", "main^"]), upstream.to_hex());
1851 assert_eq!(git(&clone, &["log", "-1", "--format=%s", "main"]), "c");
1852 assert_ne!(git(&clone, &["rev-parse", "main"]), own.to_hex());
1853
1854 let tmp = TempDir::new().expect("tempdir");
1856 let (clone, landed, _) = landed_clone(&tmp);
1857 commit(&clone, "b.txt", "b, ours");
1858 git(&clone, &["switch", "-q", "up"]);
1859 let upstream = commit(&clone, "b.txt", "b, theirs");
1860 git(&clone, &["switch", "-q", "main"]);
1861 let repo = open(&clone);
1862 let stopped = pull(
1863 &repo,
1864 &landed_options(&repo, upstream, landed, true),
1865 &mut NoProgress,
1866 &NeverCancel,
1867 None,
1868 )
1869 .expect("pull");
1870 assert_eq!(stopped.result, PullResult::RebaseStopped);
1871 let paths: Vec<&str> = stopped
1872 .conflicts
1873 .iter()
1874 .flatten()
1875 .map(|c| c.path.as_str())
1876 .collect();
1877 assert_eq!(paths, ["b.txt"]);
1878 assert_eq!(git(&clone, &["rev-parse", "HEAD"]), upstream.to_hex());
1880 }
1881}