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