just playing with tangled
1// Copyright 2020-2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashSet;
16
17use clap_complete::ArgValueCandidates;
18use itertools::Itertools as _;
19use jj_lib::config::ConfigGetResultExt as _;
20use jj_lib::git;
21use jj_lib::git::GitFetch;
22use jj_lib::ref_name::RemoteName;
23use jj_lib::repo::Repo as _;
24use jj_lib::str_util::StringPattern;
25
26use crate::cli_util::CommandHelper;
27use crate::cli_util::WorkspaceCommandHelper;
28use crate::cli_util::WorkspaceCommandTransaction;
29use crate::command_error::config_error;
30use crate::command_error::user_error;
31use crate::command_error::CommandError;
32use crate::commands::git::get_single_remote;
33use crate::complete;
34use crate::git_util::print_git_import_stats;
35use crate::git_util::with_remote_git_callbacks;
36use crate::ui::Ui;
37
38/// Fetch from a Git remote
39///
40/// If a working-copy commit gets abandoned, it will be given a new, empty
41/// commit. This is true in general; it is not specific to this command.
42#[derive(clap::Args, Clone, Debug)]
43pub struct GitFetchArgs {
44 /// Fetch only some of the branches
45 ///
46 /// By default, the specified name matches exactly. Use `glob:` prefix to
47 /// expand `*` as a glob, e.g. `--branch 'glob:push-*'`. Other wildcard
48 /// characters such as `?` are *not* supported.
49 #[arg(
50 long, short,
51 alias = "bookmark",
52 default_value = "glob:*",
53 value_parser = StringPattern::parse,
54 add = ArgValueCandidates::new(complete::bookmarks),
55 )]
56 branch: Vec<StringPattern>,
57 /// The remote to fetch from (only named remotes are supported, can be
58 /// repeated)
59 ///
60 /// This defaults to the `git.fetch` setting. If that is not configured, and
61 /// if there are multiple remotes, the remote named "origin" will be used.
62 ///
63 /// By default, the specified remote names matches exactly. Use a [string
64 /// pattern], e.g. `--remote 'glob:*'`, to select remotes using
65 /// patterns.
66 ///
67 /// [string pattern]:
68 /// https://jj-vcs.github.io/jj/latest/revsets#string-patterns
69 #[arg(
70 long = "remote",
71 value_name = "REMOTE",
72 value_parser = StringPattern::parse,
73 add = ArgValueCandidates::new(complete::git_remotes),
74 )]
75 remotes: Vec<StringPattern>,
76 /// Fetch from all remotes
77 #[arg(long, conflicts_with = "remotes")]
78 all_remotes: bool,
79}
80
81#[tracing::instrument(skip_all)]
82pub fn cmd_git_fetch(
83 ui: &mut Ui,
84 command: &CommandHelper,
85 args: &GitFetchArgs,
86) -> Result<(), CommandError> {
87 let mut workspace_command = command.workspace_helper(ui)?;
88 let remote_patterns = if args.all_remotes {
89 vec![StringPattern::everything()]
90 } else if args.remotes.is_empty() {
91 get_default_fetch_remotes(ui, &workspace_command)?
92 } else {
93 args.remotes.clone()
94 };
95
96 let all_remotes = git::get_all_remote_names(workspace_command.repo().store())?;
97
98 let mut matching_remotes = HashSet::new();
99 let mut unmatched_patterns = Vec::new();
100 for pattern in remote_patterns {
101 let remotes = all_remotes
102 .iter()
103 .filter(|r| pattern.matches(r.as_str()))
104 .collect_vec();
105 if remotes.is_empty() {
106 unmatched_patterns.push(pattern);
107 } else {
108 matching_remotes.extend(remotes);
109 }
110 }
111
112 match &unmatched_patterns[..] {
113 [] => {} // Everything matched, all good
114 [pattern] if pattern.is_exact() => {
115 return Err(user_error(format!("No git remote named '{pattern}'")))
116 }
117 patterns => {
118 return Err(user_error(format!(
119 "No matching git remotes for patterns: {}",
120 patterns.iter().join(", ")
121 )))
122 }
123 }
124
125 let remotes = all_remotes
126 .iter()
127 .filter(|r| matching_remotes.contains(r))
128 .map(|r| r.as_ref())
129 .collect_vec();
130
131 let mut tx = workspace_command.start_transaction();
132 do_git_fetch(ui, &mut tx, &remotes, &args.branch)?;
133 tx.finish(
134 ui,
135 format!(
136 "fetch from git remote(s) {}",
137 remotes.iter().map(|n| n.as_symbol()).join(",")
138 ),
139 )?;
140 Ok(())
141}
142
143const DEFAULT_REMOTE: &RemoteName = RemoteName::new("origin");
144
145fn get_default_fetch_remotes(
146 ui: &Ui,
147 workspace_command: &WorkspaceCommandHelper,
148) -> Result<Vec<StringPattern>, CommandError> {
149 const KEY: &str = "git.fetch";
150 let settings = workspace_command.settings();
151 if let Ok(remotes) = settings.get::<Vec<String>>(KEY) {
152 remotes
153 .into_iter()
154 .map(|r| parse_remote_pattern(&r))
155 .try_collect()
156 } else if let Some(remote) = settings.get_string(KEY).optional()? {
157 Ok(vec![parse_remote_pattern(&remote)?])
158 } else if let Some(remote) = get_single_remote(workspace_command.repo().store())? {
159 // if nothing was explicitly configured, try to guess
160 if remote != DEFAULT_REMOTE {
161 writeln!(
162 ui.hint_default(),
163 "Fetching from the only existing remote: {remote}",
164 remote = remote.as_symbol()
165 )?;
166 }
167 Ok(vec![StringPattern::exact(remote)])
168 } else {
169 Ok(vec![StringPattern::exact(DEFAULT_REMOTE)])
170 }
171}
172
173fn parse_remote_pattern(remote: &str) -> Result<StringPattern, CommandError> {
174 StringPattern::parse(remote).map_err(config_error)
175}
176
177fn do_git_fetch(
178 ui: &mut Ui,
179 tx: &mut WorkspaceCommandTransaction,
180 remotes: &[&RemoteName],
181 branch_names: &[StringPattern],
182) -> Result<(), CommandError> {
183 let git_settings = tx.settings().git_settings()?;
184 let mut git_fetch = GitFetch::new(tx.repo_mut(), &git_settings)?;
185
186 for remote_name in remotes {
187 with_remote_git_callbacks(ui, |callbacks| {
188 git_fetch.fetch(remote_name, branch_names, callbacks, None)
189 })?;
190 }
191 let import_stats = git_fetch.import_refs()?;
192 print_git_import_stats(ui, tx.repo(), &import_stats, true)?;
193 warn_if_branches_not_found(ui, tx, branch_names, remotes)
194}
195
196fn warn_if_branches_not_found(
197 ui: &mut Ui,
198 tx: &WorkspaceCommandTransaction,
199 branches: &[StringPattern],
200 remotes: &[&RemoteName],
201) -> Result<(), CommandError> {
202 for branch in branches {
203 let matches = remotes.iter().any(|&remote| {
204 let remote = StringPattern::exact(remote);
205 tx.repo()
206 .view()
207 .remote_bookmarks_matching(branch, &remote)
208 .next()
209 .is_some()
210 || tx
211 .base_repo()
212 .view()
213 .remote_bookmarks_matching(branch, &remote)
214 .next()
215 .is_some()
216 });
217 if !matches {
218 writeln!(
219 ui.warning_default(),
220 "No branch matching `{branch}` found on any specified/configured remote",
221 )?;
222 }
223 }
224
225 Ok(())
226}