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