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