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, 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, start_rebase, ApplyOptions, EmptyPolicy, HistoryError, PreRebase,
41 RebaseCheckPlan, RebaseOntoOptions, RebaseOutcome, RebaseRequest, RebaseSessionError, RefScope,
42 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 {
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) = match &options.upstream {
550 Some(fetched) => (fetched.label.clone(), fetched.oid),
551 None => {
552 let upstream =
553 resolve_pull_integration_base(repo, &short_name, &remote_name, explicit_fetch)
554 .ok_or(PullError::NoUpstream {
555 branch: short_name.clone(),
556 })?;
557 (upstream.name, upstream.oid)
558 }
559 };
560
561 let landed = options.upstream.as_ref().and_then(|u| u.landed);
562 if options.check {
563 return pull_check_outcome(repo, head_oid, upstream_oid, &label, integration, landed);
564 }
565
566 match head_oid {
567 None => adopt_unborn_upstream(repo, &refname, &short_name, &label, upstream_oid),
568 Some(head_oid) => integrate_fetched_upstream(
569 repo,
570 &refname,
571 head_oid,
572 Upstream {
573 oid: upstream_oid,
574 label: &label,
575 landed,
576 copies: options
577 .upstream
578 .as_ref()
579 .map_or(&[], |u| u.copies.as_slice()),
580 },
581 integration,
582 stop_on_conflict,
583 progress,
584 cancel,
585 ),
586 }
587 })();
588 let core = core.map(|outcome| PullOutcome {
589 transfer,
590 ..outcome
591 });
592
593 match core {
594 Err(err) => {
595 match stash_oid.as_ref() {
598 Some(oid) if !merge_was_in_progress && merge_in_progress() => {
599 record_merge_autostash(repo, oid).map_err(PullError::Repository)?;
600 }
601 _ => restore_autostash_after_abort(repo, stash_oid.as_ref()),
602 }
603 Err(err)
604 }
605 Ok(outcome) => {
606 if fetch_dry_run {
607 restore_autostash_after_abort(repo, stash_oid.as_ref());
608 return Ok(outcome);
609 }
610 if outcome.result == PullResult::RebaseConflicts {
611 return Ok(PullOutcome {
614 autostash: stash_oid,
615 ..outcome
616 });
617 }
618 if matches!(
619 outcome.result,
620 PullResult::Conflicts | PullResult::RefusedNotFf | PullResult::RebaseStopped
621 ) {
622 restore_autostash_after_abort(repo, stash_oid.as_ref());
623 return Ok(outcome);
624 }
625 apply_autostash_after_pull(repo, stash_oid.as_ref(), Ok(outcome))
626 }
627 }
628}
629
630fn pull_fetch_refspecs(options: &PullOptions) -> (bool, Vec<String>) {
632 let mut refspecs = options.fetch.refspecs.clone();
633 let mut explicit = options.explicit_fetch;
634 if let Some(branch) = options.branch.as_deref().filter(|b| !b.is_empty()) {
635 if refspecs.is_empty() {
636 refspecs.push(branch.to_owned());
637 }
638 explicit = true;
639 }
640 (explicit, refspecs)
641}
642
643fn restore_autostash_after_abort(repo: &Repository, stash_oid: Option<&ObjectId>) {
644 let Some(stash_oid) = stash_oid else {
645 return;
646 };
647 let _ = restore_operation_autostash(repo, stash_oid);
648}
649
650fn apply_autostash_after_pull(
651 repo: &Repository,
652 stash_oid: Option<&ObjectId>,
653 outcome: std::result::Result<PullOutcome, PullError>,
654) -> std::result::Result<PullOutcome, PullError> {
655 let mut outcome = outcome?;
656 if stash_oid.is_none() {
657 return Ok(outcome);
658 }
659 if !matches!(
660 outcome.result,
661 PullResult::UpToDate
662 | PullResult::FastForward
663 | PullResult::Merged
664 | PullResult::Rebased
665 | PullResult::Landed
666 | PullResult::SetUpstream
667 ) {
668 return Ok(outcome);
669 }
670 if let Some(stash_oid) = stash_oid {
671 if apply_operation_autostash(repo, stash_oid).map_err(PullError::Repository)? {
672 outcome.result = PullResult::AutostashConflict;
673 outcome.autostash = Some(*stash_oid);
674 }
675 }
676 Ok(outcome)
677}
678
679fn pull_check_outcome(
680 repo: &Repository,
681 head_oid: Option<ObjectId>,
682 upstream_oid: ObjectId,
683 label: &str,
684 integration: PullIntegration,
685 landed: Option<ObjectId>,
686) -> std::result::Result<PullOutcome, PullError> {
687 let (ahead, behind, would, fast_forward_possible) = match head_oid {
688 None => (0, 0, PullResult::SetUpstream, true),
689 Some(head_oid) => {
690 let (ahead, behind) = count_symmetric_ahead_behind(repo, head_oid, upstream_oid)
691 .map_err(PullError::Repository)?;
692 let would = if landed == Some(head_oid) && diverged(repo, head_oid, upstream_oid)? {
693 PullResult::Landed
694 } else {
695 predict_pull_result(repo, head_oid, upstream_oid, integration)?
696 };
697 let up_to_date = head_oid == upstream_oid || is_ancestor(repo, upstream_oid, head_oid)?;
698 let fast_forward = !up_to_date
699 && is_ancestor(repo, head_oid, upstream_oid).map_err(PullError::Repository)?;
700 (ahead, behind, would, up_to_date || fast_forward)
701 }
702 };
703 Ok(PullOutcome {
704 oid: head_oid,
705 change_id: head_oid
706 .and_then(|oid| logical_change_id_for_commit(repo, oid).ok())
707 .flatten(),
708 ahead: Some(ahead),
709 behind: Some(behind),
710 check: Some(PullCheck {
711 ahead,
712 behind,
713 fast_forward_possible,
714 would,
715 }),
716 ..PullOutcome::new(would, label)
717 })
718}
719
720fn predict_pull_result(
721 repo: &Repository,
722 head_oid: ObjectId,
723 upstream_oid: ObjectId,
724 integration: PullIntegration,
725) -> std::result::Result<PullResult, PullError> {
726 if head_oid == upstream_oid || is_ancestor(repo, upstream_oid, head_oid)? {
727 return Ok(PullResult::UpToDate);
728 }
729 if integration.allow_fast_forward && is_ancestor(repo, head_oid, upstream_oid)? {
730 return Ok(PullResult::FastForward);
731 }
732 let diverged = !is_ancestor(repo, upstream_oid, head_oid)?
733 && !is_ancestor(repo, head_oid, upstream_oid)?
734 && head_oid != upstream_oid;
735 if diverged {
736 return match integration.mode {
737 PullMode::FfOnly => Ok(PullResult::RefusedNotFf),
738 PullMode::Rebase => {
739 let mut opts = pull_rebase_options(repo)?;
740 opts.rewrite.sign = false;
741 let request = RebaseRequest {
742 upstream: upstream_oid,
743 onto: None,
744 branch_tip: head_oid,
745 };
746 match plan_rebase_check(repo, &request, &opts).map_err(map_history_error)? {
747 RebaseCheckPlan::UpToDate | RebaseCheckPlan::Clean { .. } => {
748 Ok(PullResult::Rebased)
749 }
750 RebaseCheckPlan::Conflict { .. } => Ok(PullResult::RebaseConflicts),
751 }
752 }
753 PullMode::Merge => match predict_merge(
754 repo,
755 head_oid,
756 upstream_oid,
757 &MergePredictionOptions {
758 allow_fast_forward: integration.allow_fast_forward,
759 theirs_label: "upstream".into(),
760 ..Default::default()
761 },
762 )
763 .map_err(PullError::Repository)?
764 {
765 MergePrediction::UpToDate => Ok(PullResult::UpToDate),
766 MergePrediction::FastForward => Ok(PullResult::FastForward),
767 MergePrediction::Clean { .. } => Ok(PullResult::Merged),
768 MergePrediction::Conflicts { .. } => Ok(PullResult::Conflicts),
769 MergePrediction::Unrelated => Ok(PullResult::Conflicts),
770 },
771 };
772 }
773 Ok(PullResult::Merged)
774}
775
776fn adopt_unborn_upstream(
777 repo: &Repository,
778 refname: &str,
779 short_name: &str,
780 label: &str,
781 upstream_oid: ObjectId,
782) -> std::result::Result<PullOutcome, PullError> {
783 ensure_branch_tip(repo, refname, None)?;
784 let upstream_tree = repo.odb().commit_tree(&upstream_oid)?;
785 prepare_tree_checkout(repo, None, &upstream_tree).map_err(PullError::Repository)?;
786 checkout_between_trees(repo, None, &upstream_tree).map_err(PullError::Repository)?;
787 update_checked_out_branch(repo, refname, None, upstream_oid, INITIAL_PULL_REFLOG)?;
788 Ok(PullOutcome {
789 upstream: Some(label.to_owned()),
790 ..PullOutcome::at_commit(PullResult::SetUpstream, short_name, repo, upstream_oid)?
791 })
792}
793
794fn diverged(
796 repo: &Repository,
797 head_oid: ObjectId,
798 upstream_oid: ObjectId,
799) -> std::result::Result<bool, PullError> {
800 Ok(head_oid != upstream_oid
801 && !is_ancestor(repo, upstream_oid, head_oid).map_err(PullError::Repository)?
802 && !is_ancestor(repo, head_oid, upstream_oid).map_err(PullError::Repository)?)
803}
804
805#[derive(Clone, Copy)]
807struct Upstream<'a> {
808 oid: ObjectId,
809 label: &'a str,
810 landed: Option<ObjectId>,
812 copies: &'a [ObjectId],
814}
815
816#[allow(clippy::too_many_arguments)]
817fn integrate_fetched_upstream(
818 repo: &Repository,
819 refname: &str,
820 head_oid: ObjectId,
821 upstream: Upstream<'_>,
822 integration: PullIntegration,
823 stop_on_conflict: bool,
824 progress: &mut dyn ProgressSink,
825 cancel: &dyn Cancel,
826) -> std::result::Result<PullOutcome, PullError> {
827 let Upstream {
828 oid: upstream_oid,
829 label,
830 landed,
831 ..
832 } = upstream;
833 let diverged = diverged(repo, head_oid, upstream_oid)?;
834
835 if diverged && landed == Some(head_oid) {
836 return move_landed_branch(
837 repo,
838 refname,
839 head_oid,
840 upstream_oid,
841 label,
842 progress,
843 cancel,
844 );
845 }
846
847 if diverged {
848 match integration.mode {
849 PullMode::FfOnly => {
850 let (ahead, behind) = count_symmetric_ahead_behind(repo, head_oid, upstream_oid)
851 .map_err(PullError::Repository)?;
852 return Ok(PullOutcome {
853 ahead: Some(ahead),
854 behind: Some(behind),
855 ..PullOutcome::at_commit(PullResult::RefusedNotFf, label, repo, head_oid)?
856 });
857 }
858 PullMode::Rebase => {
859 return rebase_fetched_upstream(
860 repo,
861 refname,
862 head_oid,
863 upstream,
864 stop_on_conflict,
865 progress,
866 cancel,
867 );
868 }
869 PullMode::Merge => {}
870 }
871 }
872
873 let integrate = integrate_branch_with_options(
874 repo,
875 refname,
876 head_oid,
877 upstream_oid,
878 label,
879 IntegrateBranchOptions {
880 allow_fast_forward: integration.allow_fast_forward,
881 },
882 progress,
883 cancel,
884 )
885 .map_err(PullError::Repository)?;
886 Ok(map_integrate_outcome(integrate, label))
887}
888
889#[allow(clippy::too_many_arguments)]
890fn rebase_fetched_upstream(
891 repo: &Repository,
892 refname: &str,
893 head_oid: ObjectId,
894 upstream: Upstream<'_>,
895 stop_on_conflict: bool,
896 progress: &mut dyn ProgressSink,
897 cancel: &dyn Cancel,
898) -> std::result::Result<PullOutcome, PullError> {
899 let Upstream {
900 oid: upstream_oid,
901 label,
902 landed,
903 copies,
904 } = upstream;
905 let fork = merge_bases_first_vs_rest(repo, head_oid, &[upstream_oid])
906 .map_err(PullError::Repository)?
907 .into_iter()
908 .next()
909 .ok_or_else(|| {
910 PullError::Repository(Error::Message(format!(
911 "'{label}' has no common history with the current branch"
912 )))
913 })?;
914 let request = match landed {
917 Some(base)
918 if base != fork
919 && is_ancestor(repo, base, head_oid).map_err(PullError::Repository)?
920 && is_ancestor(repo, fork, base).map_err(PullError::Repository)? =>
921 {
922 RebaseRequest {
923 upstream: base,
924 onto: Some(upstream_oid),
925 branch_tip: head_oid,
926 }
927 }
928 _ => RebaseRequest {
929 upstream: upstream_oid,
930 onto: None,
931 branch_tip: head_oid,
932 },
933 };
934
935 let mut rebase = pull_rebase_options(repo)?;
936 rebase.drop = copies.to_vec();
938 let pre_rebase = PreRebase::Run {
940 upstream: Some(request.upstream.to_hex()),
941 };
942 let start = StartRebaseOptions {
943 request,
944 rebase,
945 apply: ApplyOptions {
946 identity: repo
947 .identity(IdentRole::Committer, repo.identity_now())
948 .map_err(PullError::Repository)?,
949 reflog_message: format!("pull: rebase onto {label}"),
950 record_operation: Some("rebase".into()),
951 hooks: crate::hooks::PostOperationHooks::Run,
952 },
953 pre_rebase,
954 };
955 match start_rebase(repo, &start, progress, cancel).map_err(map_rebase_session_error)? {
956 RebaseOutcome::Stopped(stop) if stop_on_conflict => Ok(PullOutcome {
957 conflicts: Some(stop.conflicts),
958 stopped_commit: Some(stop.commit),
959 ..PullOutcome::at_commit(PullResult::RebaseStopped, label, repo, head_oid)?
960 }),
961 RebaseOutcome::Stopped(stop) => Ok(PullOutcome {
962 conflicts: Some(stop.conflicts),
963 stopped_commit: Some(stop.commit),
964 ..PullOutcome::new(PullResult::RebaseConflicts, label)
965 }),
966 RebaseOutcome::Done { applied, .. } | RebaseOutcome::ExecDone { applied, .. }
967 if applied.head != head_oid =>
968 {
969 Ok(PullOutcome::at_commit(
970 PullResult::Rebased,
971 label,
972 repo,
973 applied.head,
974 )?)
975 }
976 RebaseOutcome::Done { .. } | RebaseOutcome::ExecDone { .. } => {
979 let integrate = integrate_branch_with_options(
980 repo,
981 refname,
982 head_oid,
983 upstream_oid,
984 label,
985 IntegrateBranchOptions {
986 allow_fast_forward: true,
987 },
988 progress,
989 cancel,
990 )
991 .map_err(PullError::Repository)?;
992 Ok(map_integrate_outcome(integrate, label))
993 }
994 }
995}
996
997fn pull_rebase_options(repo: &Repository) -> std::result::Result<RebaseOntoOptions, PullError> {
999 let committer = repo
1000 .identity(IdentRole::Committer, repo.identity_now())
1001 .map_err(PullError::Repository)?;
1002 let mut opts = RebaseOntoOptions::new(RewriteOptions {
1003 scope: RefScope::Head,
1004 empty: EmptyPolicy::Drop,
1005 committer,
1006 preserve_committer: false,
1007 sign: true,
1008 });
1009 opts.flatten_leaf_merges = true;
1010 Ok(opts)
1011}
1012
1013#[allow(clippy::too_many_arguments)]
1018fn move_landed_branch(
1019 repo: &Repository,
1020 refname: &str,
1021 head_oid: ObjectId,
1022 upstream_oid: ObjectId,
1023 label: &str,
1024 progress: &mut dyn ProgressSink,
1025 cancel: &dyn Cancel,
1026) -> std::result::Result<PullOutcome, PullError> {
1027 ensure_branch_tip(repo, refname, Some(head_oid))?;
1028 if repo.load_index()?.has_unmerged_entries() {
1029 return Err(Error::IndexUnmerged.into());
1030 }
1031 let from = repo.odb().commit_tree(&head_oid)?;
1032 let to = repo.odb().commit_tree(&upstream_oid)?;
1033 checkout_tree_transition_allow_local_reset(repo, Some(&from), &to, progress, cancel).map_err(
1034 |e| match e {
1035 Error::WouldOverwrite {
1036 reason: OverwriteReason::LocalChanges,
1037 paths,
1038 ..
1039 } => local_changes_would_be_overwritten(paths),
1040 other => other,
1041 },
1042 )?;
1043 update_checked_out_branch(
1044 repo,
1045 refname,
1046 Some(head_oid),
1047 upstream_oid,
1048 &format!("pull: move to {label}, which has the branch's commits"),
1049 )?;
1050 crate::state::write_orig_head(repo, head_oid)?;
1051 Ok(PullOutcome::at_commit(
1052 PullResult::Landed,
1053 label,
1054 repo,
1055 upstream_oid,
1056 )?)
1057}
1058
1059fn local_changes_would_be_overwritten(paths: Vec<String>) -> Error {
1061 Error::WouldOverwrite {
1062 reason: OverwriteReason::LocalChanges,
1063 paths,
1064 message: "local changes would be overwritten".to_owned(),
1065 }
1066}
1067
1068fn map_history_error(err: HistoryError) -> PullError {
1069 match err {
1070 HistoryError::Library(e) => PullError::Repository(*e),
1071 HistoryError::LocalChangesWouldBeOverwritten { paths } => {
1072 PullError::Repository(local_changes_would_be_overwritten(paths))
1073 }
1074 other => PullError::Repository(Error::Message(other.to_string())),
1075 }
1076}
1077
1078fn map_rebase_session_error(err: RebaseSessionError) -> PullError {
1079 match err {
1080 RebaseSessionError::Library(e) => PullError::Repository(e),
1081 RebaseSessionError::History(e) => map_history_error(e),
1082 other => PullError::Rebase(other),
1083 }
1084}
1085
1086fn map_integrate_outcome(outcome: IntegrateBranchOutcome, label: &str) -> PullOutcome {
1087 match outcome {
1088 IntegrateBranchOutcome::UpToDate => PullOutcome::new(PullResult::UpToDate, label),
1089 IntegrateBranchOutcome::FastForward { oid, change_id } => PullOutcome {
1090 oid: Some(oid),
1091 change_id,
1092 ..PullOutcome::new(PullResult::FastForward, label)
1093 },
1094 IntegrateBranchOutcome::Merged { oid, change_id } => PullOutcome {
1095 oid: Some(oid),
1096 change_id,
1097 ..PullOutcome::new(PullResult::Merged, label)
1098 },
1099 IntegrateBranchOutcome::Conflicts { details, .. } => PullOutcome {
1100 conflicts: Some(details),
1101 ..PullOutcome::new(PullResult::Conflicts, label)
1102 },
1103 IntegrateBranchOutcome::Squashed | IntegrateBranchOutcome::Stopped => {
1104 unreachable!("pull does not use squash or no-commit merge modes")
1105 }
1106 }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111 use super::*;
1112 use crate::config::environment::RepositoryOptions;
1113 use crate::config::ConfigSet;
1114 use crate::environment::Environment;
1115 use crate::progress::NeverCancel;
1116 use crate::remote::fetch::NoProgress;
1117 use std::path::{Path, PathBuf};
1118 use std::process::{Command, Output};
1119 use tempfile::TempDir;
1120
1121 fn git_output(dir: &Path, args: &[&str]) -> Output {
1122 Command::new("git")
1123 .current_dir(dir)
1124 .args(args)
1125 .env("GIT_CONFIG_GLOBAL", "/dev/null")
1126 .env("GIT_CONFIG_SYSTEM", "/dev/null")
1127 .env("GIT_AUTHOR_NAME", "T")
1128 .env("GIT_AUTHOR_EMAIL", "t@example.com")
1129 .env("GIT_COMMITTER_NAME", "T")
1130 .env("GIT_COMMITTER_EMAIL", "t@example.com")
1131 .output()
1132 .expect("run git")
1133 }
1134
1135 fn git(dir: &Path, args: &[&str]) -> String {
1136 let out = git_output(dir, args);
1137 assert!(
1138 out.status.success(),
1139 "git {args:?} failed: {}",
1140 String::from_utf8_lossy(&out.stderr)
1141 );
1142 String::from_utf8_lossy(&out.stdout).trim().to_owned()
1143 }
1144
1145 fn open(dir: &Path) -> Repository {
1146 Repository::discover(Some(dir), &RepositoryOptions::empty()).expect("open")
1147 }
1148
1149 fn commit(dir: &Path, file: &str, contents: &str) -> ObjectId {
1150 std::fs::write(dir.join(file), contents).expect("write");
1151 git(dir, &["add", file]);
1152 git(dir, &["commit", "-qm", contents]);
1153 ObjectId::from_hex(&git(dir, &["rev-parse", "HEAD"])).expect("oid")
1154 }
1155
1156 fn origin_and_clone(tmp: &TempDir) -> (PathBuf, PathBuf) {
1157 let origin = tmp.path().join("origin");
1158 std::fs::create_dir_all(&origin).expect("mkdir origin");
1159 git(&origin, &["init", "-q", "-b", "main"]);
1160 commit(&origin, "a.txt", "one");
1161 git(tmp.path(), &["clone", "-q", "origin", "clone"]);
1162 (origin, tmp.path().join("clone"))
1163 }
1164
1165 fn pull_options(repo: &Repository) -> PullOptions {
1166 PullOptions::for_working_branch(repo, Environment::empty(), None).expect("options")
1167 }
1168
1169 fn cfg(pairs: &[(&str, &str)]) -> ConfigSet {
1170 let mut set = ConfigSet::new();
1171 for (key, value) in pairs {
1172 set.add_command_override(key, value).expect("override");
1173 }
1174 set
1175 }
1176
1177 #[test]
1178 fn resolve_pull_mode_cli_ff_only_wins() {
1179 let config = cfg(&[("pull.rebase", "true")]);
1180 assert_eq!(
1181 resolve_pull_mode(&config, Some("main"), Some(PullMode::FfOnly)).expect("mode"),
1182 PullMode::FfOnly
1183 );
1184 }
1185
1186 #[test]
1187 fn resolve_pull_mode_no_rebase_overrides_rebase_config() {
1188 let config = cfg(&[("pull.rebase", "true")]);
1189 assert_eq!(
1190 resolve_pull_mode(&config, Some("main"), Some(PullMode::Merge)).expect("mode"),
1191 PullMode::Merge
1192 );
1193 }
1194
1195 #[test]
1196 fn resolve_pull_mode_rebase_from_config() {
1197 let config = cfg(&[("pull.rebase", "merges")]);
1198 assert_eq!(
1199 resolve_pull_mode(&config, Some("main"), None).expect("mode"),
1200 PullMode::Rebase
1201 );
1202 }
1203
1204 #[test]
1205 fn resolve_pull_mode_branch_rebase_overrides_global() {
1206 let config = cfg(&[("pull.rebase", "false"), ("branch.topic.rebase", "true")]);
1207 assert_eq!(
1208 resolve_pull_mode(&config, Some("topic"), None).expect("mode"),
1209 PullMode::Rebase
1210 );
1211 }
1212
1213 #[test]
1214 fn resolve_pull_mode_interactive_is_error() {
1215 let config = cfg(&[("pull.rebase", "interactive")]);
1216 assert!(matches!(
1217 resolve_pull_mode(&config, Some("main"), None),
1218 Err(PullError::RebaseInteractive)
1219 ));
1220 }
1221
1222 #[test]
1223 fn resolve_pull_mode_ff_only_from_config() {
1224 let config = cfg(&[("pull.ff", "only")]);
1225 assert_eq!(
1226 resolve_pull_mode(&config, Some("main"), None).expect("mode"),
1227 PullMode::FfOnly
1228 );
1229 }
1230
1231 #[test]
1232 fn resolve_pull_mode_ff_only_beats_rebase_config() {
1233 let config = cfg(&[("pull.ff", "only"), ("pull.rebase", "true")]);
1234 assert_eq!(
1235 resolve_pull_mode(&config, Some("main"), None).expect("mode"),
1236 PullMode::FfOnly
1237 );
1238 }
1239
1240 #[test]
1241 fn resolve_pull_mode_defaults_to_merge() {
1242 let config = ConfigSet::new();
1243 assert_eq!(
1244 resolve_pull_mode(&config, Some("main"), None).expect("mode"),
1245 PullMode::Merge
1246 );
1247 }
1248
1249 #[test]
1250 fn resolve_pull_integration_ff_false_disables_fast_forward() {
1251 let config = cfg(&[("pull.ff", "false")]);
1252 let resolved = resolve_pull_integration(&config, Some("main"), None).expect("resolved");
1253 assert_eq!(resolved.mode, PullMode::Merge);
1254 assert!(!resolved.allow_fast_forward);
1255 }
1256
1257 #[cfg_attr(
1258 not(feature = "full-tests"),
1259 ignore = "starts system git or is heavy; run with --features full-tests"
1260 )]
1261 #[test]
1262 fn pull_maps_custom_fetch_refspec() {
1263 let tmp = TempDir::new().expect("tempdir");
1264 let (origin, clone) = origin_and_clone(&tmp);
1265 let tip = commit(&origin, "b.txt", "two");
1266 git(
1267 &clone,
1268 &[
1269 "config",
1270 "--replace-all",
1271 "remote.origin.fetch",
1272 "+refs/heads/*:refs/remotes/mirror/*",
1273 ],
1274 );
1275
1276 let repo = open(&clone);
1277 let outcome = pull(
1278 &repo,
1279 &pull_options(&repo),
1280 &mut NoProgress,
1281 &NeverCancel,
1282 None,
1283 )
1284 .expect("pull succeeds");
1285 assert_eq!(outcome.result, PullResult::FastForward);
1286 assert_eq!(outcome.oid, Some(tip));
1287 assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1288 }
1289
1290 #[cfg_attr(
1291 not(feature = "full-tests"),
1292 ignore = "starts system git or is heavy; run with --features full-tests"
1293 )]
1294 #[test]
1295 fn pull_local_upstream_skips_fetch() {
1296 let tmp = TempDir::new().expect("tempdir");
1297 let dir = tmp.path();
1298 git(dir, &["init", "-q", "-b", "main"]);
1299 commit(dir, "a.txt", "one");
1300 git(dir, &["checkout", "-q", "-b", "dev"]);
1301 let tip = commit(dir, "b.txt", "two");
1302 git(dir, &["checkout", "-q", "main"]);
1303 git(dir, &["branch", "--set-upstream-to", "dev", "main"]);
1304
1305 let repo = open(dir);
1306 let outcome = pull(
1307 &repo,
1308 &pull_options(&repo),
1309 &mut NoProgress,
1310 &NeverCancel,
1311 None,
1312 )
1313 .expect("local pull succeeds");
1314 assert_eq!(outcome.result, PullResult::FastForward);
1315 assert_eq!(outcome.oid, Some(tip));
1316 assert_eq!(git(dir, &["rev-parse", "main"]), tip.to_hex());
1317 assert_eq!(outcome.transfer, TransferStats::default());
1318 }
1319
1320 #[cfg_attr(
1321 not(feature = "full-tests"),
1322 ignore = "starts system git or is heavy; run with --features full-tests"
1323 )]
1324 #[test]
1325 fn pull_reports_objects_received_by_its_fetch() {
1326 let tmp = TempDir::new().expect("tempdir");
1327 let (origin, clone) = origin_and_clone(&tmp);
1328 commit(&origin, "b.txt", "two");
1329
1330 let repo = open(&clone);
1331 let outcome = pull(
1332 &repo,
1333 &pull_options(&repo),
1334 &mut NoProgress,
1335 &NeverCancel,
1336 None,
1337 )
1338 .expect("pull succeeds");
1339 assert_eq!(outcome.result, PullResult::FastForward);
1340 assert_eq!(outcome.transfer.objects, 3, "{:?}", outcome.transfer);
1342 assert!(outcome.transfer.bytes > 0, "{:?}", outcome.transfer);
1343
1344 let again = pull(
1345 &repo,
1346 &pull_options(&repo),
1347 &mut NoProgress,
1348 &NeverCancel,
1349 None,
1350 )
1351 .expect("second pull");
1352 assert_eq!(again.result, PullResult::UpToDate);
1353 assert_eq!(again.transfer.objects, 0, "{:?}", again.transfer);
1354 }
1355
1356 #[cfg_attr(
1357 not(feature = "full-tests"),
1358 ignore = "starts system git or is heavy; run with --features full-tests"
1359 )]
1360 #[test]
1361 fn pull_adopts_upstream_on_unborn_branch_with_reflog() {
1362 let tmp = TempDir::new().expect("tempdir");
1363 let origin = tmp.path().join("origin");
1364 std::fs::create_dir_all(&origin).expect("mkdir origin");
1365 git(&origin, &["init", "-q", "-b", "main"]);
1366 let tip = commit(&origin, "a.txt", "one");
1367 let local = tmp.path().join("local");
1368 std::fs::create_dir_all(&local).expect("mkdir local");
1369 git(&local, &["init", "-q", "-b", "main"]);
1370 git(&local, &["remote", "add", "origin", "../origin"]);
1371
1372 let repo = open(&local);
1373 let outcome = pull(
1374 &repo,
1375 &pull_options(&repo),
1376 &mut NoProgress,
1377 &NeverCancel,
1378 None,
1379 )
1380 .expect("pull succeeds");
1381 assert_eq!(outcome.result, PullResult::SetUpstream);
1382 assert_eq!(outcome.oid, Some(tip));
1383 assert_eq!(
1384 git(&local, &["reflog", "show", "--format=%H %gs", "-1", "main"]),
1385 format!("{} {INITIAL_PULL_REFLOG}", tip.to_hex())
1386 );
1387 }
1388
1389 #[cfg_attr(
1390 not(feature = "full-tests"),
1391 ignore = "starts system git or is heavy; run with --features full-tests"
1392 )]
1393 #[test]
1394 fn pull_integrates_a_fetched_upstream_without_fetching() {
1395 let tmp = TempDir::new().expect("tempdir");
1396 let (_origin, clone) = origin_and_clone(&tmp);
1397 git(&clone, &["checkout", "-q", "-b", "up"]);
1398 let tip = commit(&clone, "b.txt", "two");
1399 git(&clone, &["checkout", "-q", "main"]);
1400 git(
1402 &clone,
1403 &["remote", "set-url", "origin", "/nonexistent/remote"],
1404 );
1405
1406 let repo = open(&clone);
1407 let mut options = pull_options(&repo);
1408 options.upstream = Some(FetchedUpstream {
1409 label: "origin/up".to_owned(),
1410 oid: tip,
1411 landed: None,
1412 copies: Vec::new(),
1413 });
1414 let outcome = pull(&repo, &options, &mut NoProgress, &NeverCancel, None)
1415 .expect("pull without a fetch");
1416 assert_eq!(outcome.result, PullResult::FastForward);
1417 assert_eq!(outcome.label, "origin/up");
1418 assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1419 assert_eq!(outcome.transfer, TransferStats::default());
1420 }
1421
1422 fn conflicting_clone(tmp: &TempDir) -> (PathBuf, ObjectId, ObjectId) {
1424 let (origin, clone) = origin_and_clone(tmp);
1425 let upstream = commit(&origin, "a.txt", "theirs");
1426 let local = commit(&clone, "a.txt", "ours");
1427 git(&clone, &["config", "user.name", "T"]);
1428 git(&clone, &["config", "user.email", "t@example.com"]);
1429 git(&clone, &["fetch", "-q"]);
1430 (clone, local, upstream)
1431 }
1432
1433 fn rebase_options(repo: &Repository, upstream: ObjectId, stop: bool) -> PullOptions {
1434 let mut options = pull_options(repo);
1435 options.mode = Some(PullMode::Rebase);
1436 options.stop_on_conflict = stop;
1437 options.upstream = Some(FetchedUpstream {
1438 label: "origin/main".to_owned(),
1439 oid: upstream,
1440 landed: None,
1441 copies: Vec::new(),
1442 });
1443 options
1444 }
1445
1446 fn abort(repo: &Repository) {
1447 let identity = "T <t@example.com> 0 +0000".parse().expect("signature");
1448 crate::history::abort_rebase(repo, &identity).expect("abort");
1449 }
1450
1451 #[cfg_attr(
1452 not(feature = "full-tests"),
1453 ignore = "starts system git or is heavy; run with --features full-tests"
1454 )]
1455 #[test]
1456 fn a_conflicting_rebase_pull_stops_in_a_resumable_rebase() {
1457 let tmp = TempDir::new().expect("tempdir");
1458 let (clone, local, upstream) = conflicting_clone(&tmp);
1459 let repo = open(&clone);
1460
1461 let plain = pull(
1463 &repo,
1464 &rebase_options(&repo, upstream, false),
1465 &mut NoProgress,
1466 &NeverCancel,
1467 None,
1468 )
1469 .expect("pull");
1470 assert_eq!(plain.result, PullResult::RebaseConflicts);
1471 assert!(clone.join(".git/rebase-merge").exists());
1472 abort(&repo);
1473 assert_eq!(git(&clone, &["rev-parse", "HEAD"]), local.to_hex());
1474
1475 let stopped = pull(
1476 &repo,
1477 &rebase_options(&repo, upstream, true),
1478 &mut NoProgress,
1479 &NeverCancel,
1480 None,
1481 )
1482 .expect("pull");
1483 assert_eq!(stopped.result, PullResult::RebaseStopped);
1484 assert_eq!(stopped.oid, Some(local));
1485 let paths: Vec<&str> = stopped
1486 .conflicts
1487 .iter()
1488 .flatten()
1489 .map(|c| c.path.as_str())
1490 .collect();
1491 assert_eq!(paths, ["a.txt"]);
1492 assert!(clone.join(".git/rebase-merge/grit-plan").is_file());
1494 assert_eq!(git(&clone, &["rev-parse", "main"]), local.to_hex());
1495 assert_eq!(git(&clone, &["rev-parse", "HEAD"]), upstream.to_hex());
1496 assert!(std::fs::read_to_string(clone.join("a.txt"))
1497 .expect("conflicted file")
1498 .contains("<<<<<<<"));
1499 }
1500
1501 #[cfg_attr(
1502 not(feature = "full-tests"),
1503 ignore = "starts system git or is heavy; run with --features full-tests"
1504 )]
1505 #[test]
1506 fn a_rebase_pull_with_local_changes_is_refused() {
1507 let tmp = TempDir::new().expect("tempdir");
1508 let (clone, _, upstream) = conflicting_clone(&tmp);
1509 let local = commit(&clone, "b.txt", "tracked");
1510 std::fs::write(clone.join("b.txt"), "edited, not committed").expect("write");
1511 let repo = open(&clone);
1512 let refused = pull(
1513 &repo,
1514 &rebase_options(&repo, upstream, true),
1515 &mut NoProgress,
1516 &NeverCancel,
1517 None,
1518 );
1519 assert!(
1520 matches!(
1521 refused,
1522 Err(PullError::Repository(Error::DirtyWorktree { .. }))
1523 ),
1524 "{refused:?}"
1525 );
1526 assert!(!clone.join(".git/rebase-merge").exists());
1527 assert_eq!(git(&clone, &["rev-parse", "HEAD"]), local.to_hex());
1528 assert_eq!(
1529 std::fs::read_to_string(clone.join("b.txt")).expect("read"),
1530 "edited, not committed"
1531 );
1532 }
1533
1534 #[cfg_attr(
1535 not(feature = "full-tests"),
1536 ignore = "starts system git or is heavy; run with --features full-tests"
1537 )]
1538 #[test]
1539 fn reflog_failure_after_ref_update_is_a_warning() {
1540 let tmp = TempDir::new().expect("tempdir");
1541 let (origin, clone) = origin_and_clone(&tmp);
1542 let tip = commit(&origin, "b.txt", "two");
1543 let branch_log = clone.join(".git/logs/refs/heads/main");
1544 std::fs::remove_file(&branch_log).expect("remove branch reflog");
1545 std::fs::create_dir(&branch_log).expect("replace reflog with directory");
1546
1547 let mut repo = open(&clone);
1548 let sink = std::sync::Arc::new(crate::diagnostics::CollectingDiagnostics::new());
1549 repo.set_diagnostics(sink.clone());
1550 let outcome = pull(
1551 &repo,
1552 &pull_options(&repo),
1553 &mut NoProgress,
1554 &NeverCancel,
1555 None,
1556 )
1557 .expect("pull completes");
1558 assert_eq!(outcome.result, PullResult::FastForward);
1559 assert_eq!(outcome.oid, Some(tip));
1560 assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1561 assert!(sink.warnings().iter().any(|warning| matches!(
1562 warning,
1563 crate::diagnostics::Warning::ReflogNotWritten { refname, .. }
1564 if refname == "refs/heads/main"
1565 )));
1566 }
1567
1568 fn landed_clone(tmp: &TempDir) -> (PathBuf, ObjectId, ObjectId) {
1572 let (_origin, clone) = origin_and_clone(tmp);
1573 git(&clone, &["config", "user.name", "T"]);
1574 git(&clone, &["config", "user.email", "t@example.com"]);
1575 std::fs::write(clone.join("b.txt"), "b\n").expect("write");
1576 git(&clone, &["add", "b.txt"]);
1577 git(&clone, &["commit", "-qm", "b"]);
1578 let base = git(&clone, &["rev-parse", "HEAD"]);
1579 let mut text = "one".to_owned();
1580 for line in ["1", "2", "3"] {
1581 text = format!("{text}\n{line}");
1582 commit(&clone, "a.txt", &text);
1583 }
1584 let tip = ObjectId::from_hex(&git(&clone, &["rev-parse", "HEAD"])).expect("oid");
1585 git(&clone, &["switch", "-q", "-c", "up", &base]);
1586 let upstream = commit(&clone, "a.txt", &text);
1587 git(&clone, &["switch", "-q", "main"]);
1588 (clone, tip, upstream)
1589 }
1590
1591 fn landed_options(
1592 repo: &Repository,
1593 upstream: ObjectId,
1594 landed: ObjectId,
1595 stop: bool,
1596 ) -> PullOptions {
1597 let mut options = rebase_options(repo, upstream, stop);
1598 if let Some(fetched) = options.upstream.as_mut() {
1599 fetched.landed = Some(landed);
1600 }
1601 options
1602 }
1603
1604 #[cfg_attr(
1605 not(feature = "full-tests"),
1606 ignore = "starts system git or is heavy; run with --features full-tests"
1607 )]
1608 #[test]
1609 fn a_landed_branch_moves_to_the_upstream_and_keeps_local_changes() {
1610 let tmp = TempDir::new().expect("tempdir");
1611 let (clone, tip, upstream) = landed_clone(&tmp);
1612 let repo = open(&clone);
1613 let replayed = pull(
1615 &repo,
1616 &rebase_options(&repo, upstream, false),
1617 &mut NoProgress,
1618 &NeverCancel,
1619 None,
1620 )
1621 .expect("pull");
1622 assert_eq!(replayed.result, PullResult::RebaseConflicts);
1623 abort(&repo);
1624 assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1625
1626 std::fs::write(clone.join("b.txt"), "b, edited\n").expect("write");
1627 std::fs::write(clone.join("draft.txt"), "not committed\n").expect("write");
1628 let mut check = landed_options(&repo, upstream, tip, false);
1629 check.check = true;
1630 let predicted = pull(&repo, &check, &mut NoProgress, &NeverCancel, None).expect("check");
1631 assert_eq!(predicted.check.map(|c| c.would), Some(PullResult::Landed));
1632
1633 git(&clone, &["update-ref", "-d", "ORIG_HEAD"]);
1634 let landed = pull(
1635 &repo,
1636 &landed_options(&repo, upstream, tip, true),
1637 &mut NoProgress,
1638 &NeverCancel,
1639 None,
1640 )
1641 .expect("pull");
1642 assert_eq!(landed.result, PullResult::Landed);
1643 assert_eq!(landed.oid, Some(upstream));
1644 assert_eq!(git(&clone, &["rev-parse", "main"]), upstream.to_hex());
1645 assert_eq!(git(&clone, &["rev-parse", "ORIG_HEAD"]), tip.to_hex());
1646 assert_eq!(
1647 git(&clone, &["status", "--porcelain"]),
1648 "M b.txt\n?? draft.txt"
1649 );
1650 assert_eq!(
1651 git(&clone, &["reflog", "-1", "--format=%gs", "main"]),
1652 "pull: move to origin/main, which has the branch's commits"
1653 );
1654 }
1655
1656 #[cfg_attr(
1657 not(feature = "full-tests"),
1658 ignore = "starts system git or is heavy; run with --features full-tests"
1659 )]
1660 #[test]
1661 fn a_landed_branch_does_not_move_over_local_changes_the_upstream_has_other_content_for() {
1662 let tmp = TempDir::new().expect("tempdir");
1663 let (clone, tip, _) = landed_clone(&tmp);
1664 git(&clone, &["switch", "-q", "up"]);
1666 let upstream = commit(&clone, "b.txt", "b, upstream");
1667 git(&clone, &["switch", "-q", "main"]);
1668 std::fs::write(clone.join("b.txt"), "b, edited\n").expect("write");
1669 let repo = open(&clone);
1670 let err = pull(
1671 &repo,
1672 &landed_options(&repo, upstream, tip, true),
1673 &mut NoProgress,
1674 &NeverCancel,
1675 None,
1676 )
1677 .expect_err("blocked");
1678 assert!(
1679 matches!(
1680 &err,
1681 PullError::Repository(Error::WouldOverwrite { paths, .. }) if paths == &["b.txt"]
1682 ),
1683 "{err:?}"
1684 );
1685 assert_eq!(git(&clone, &["rev-parse", "main"]), tip.to_hex());
1686 assert_eq!(git(&clone, &["status", "--porcelain"]), "M b.txt");
1687 }
1688
1689 #[cfg_attr(
1690 not(feature = "full-tests"),
1691 ignore = "starts system git or is heavy; run with --features full-tests"
1692 )]
1693 #[test]
1694 fn commits_above_a_landed_one_are_rebased_without_it() {
1695 let tmp = TempDir::new().expect("tempdir");
1696 let (clone, landed, upstream) = landed_clone(&tmp);
1697 let own = commit(&clone, "c.txt", "c");
1698 let repo = open(&clone);
1699 let outcome = pull(
1700 &repo,
1701 &landed_options(&repo, upstream, landed, true),
1702 &mut NoProgress,
1703 &NeverCancel,
1704 None,
1705 )
1706 .expect("pull");
1707 assert_eq!(outcome.result, PullResult::Rebased);
1708 assert_eq!(git(&clone, &["rev-parse", "main^"]), upstream.to_hex());
1709 assert_eq!(git(&clone, &["log", "-1", "--format=%s", "main"]), "c");
1710 assert_ne!(git(&clone, &["rev-parse", "main"]), own.to_hex());
1711
1712 let tmp = TempDir::new().expect("tempdir");
1714 let (clone, landed, _) = landed_clone(&tmp);
1715 commit(&clone, "b.txt", "b, ours");
1716 git(&clone, &["switch", "-q", "up"]);
1717 let upstream = commit(&clone, "b.txt", "b, theirs");
1718 git(&clone, &["switch", "-q", "main"]);
1719 let repo = open(&clone);
1720 let stopped = pull(
1721 &repo,
1722 &landed_options(&repo, upstream, landed, true),
1723 &mut NoProgress,
1724 &NeverCancel,
1725 None,
1726 )
1727 .expect("pull");
1728 assert_eq!(stopped.result, PullResult::RebaseStopped);
1729 let paths: Vec<&str> = stopped
1730 .conflicts
1731 .iter()
1732 .flatten()
1733 .map(|c| c.path.as_str())
1734 .collect();
1735 assert_eq!(paths, ["b.txt"]);
1736 assert_eq!(git(&clone, &["rev-parse", "HEAD"]), upstream.to_hex());
1738 }
1739}