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 for pattern in remote_patterns {
100 let remotes = all_remotes
101 .iter()
102 .filter(|r| pattern.matches(r.as_str()))
103 .collect_vec();
104 if remotes.is_empty() {
105 writeln!(ui.warning_default(), "No git remotes matching '{pattern}'")?;
106 } else {
107 matching_remotes.extend(remotes);
108 }
109 }
110
111 if matching_remotes.is_empty() {
112 return Err(user_error("No git remotes to push"));
113 }
114
115 let remotes = matching_remotes
116 .iter()
117 .map(|r| r.as_ref())
118 .sorted()
119 .collect_vec();
120
121 let mut tx = workspace_command.start_transaction();
122 do_git_fetch(ui, &mut tx, &remotes, &args.branch)?;
123 tx.finish(
124 ui,
125 format!(
126 "fetch from git remote(s) {}",
127 remotes.iter().map(|n| n.as_symbol()).join(",")
128 ),
129 )?;
130 Ok(())
131}
132
133const DEFAULT_REMOTE: &RemoteName = RemoteName::new("origin");
134
135fn get_default_fetch_remotes(
136 ui: &Ui,
137 workspace_command: &WorkspaceCommandHelper,
138) -> Result<Vec<StringPattern>, CommandError> {
139 const KEY: &str = "git.fetch";
140 let settings = workspace_command.settings();
141 if let Ok(remotes) = settings.get::<Vec<String>>(KEY) {
142 remotes
143 .into_iter()
144 .map(|r| parse_remote_pattern(&r))
145 .try_collect()
146 } else if let Some(remote) = settings.get_string(KEY).optional()? {
147 Ok(vec![parse_remote_pattern(&remote)?])
148 } else if let Some(remote) = get_single_remote(workspace_command.repo().store())? {
149 // if nothing was explicitly configured, try to guess
150 if remote != DEFAULT_REMOTE {
151 writeln!(
152 ui.hint_default(),
153 "Fetching from the only existing remote: {remote}",
154 remote = remote.as_symbol()
155 )?;
156 }
157 Ok(vec![StringPattern::exact(remote)])
158 } else {
159 Ok(vec![StringPattern::exact(DEFAULT_REMOTE)])
160 }
161}
162
163fn parse_remote_pattern(remote: &str) -> Result<StringPattern, CommandError> {
164 StringPattern::parse(remote).map_err(config_error)
165}
166
167fn do_git_fetch(
168 ui: &mut Ui,
169 tx: &mut WorkspaceCommandTransaction,
170 remotes: &[&RemoteName],
171 branch_names: &[StringPattern],
172) -> Result<(), CommandError> {
173 let git_settings = tx.settings().git_settings()?;
174 let mut git_fetch = GitFetch::new(tx.repo_mut(), &git_settings)?;
175
176 for remote_name in remotes {
177 with_remote_git_callbacks(ui, |callbacks| {
178 git_fetch.fetch(remote_name, branch_names, callbacks, None)
179 })?;
180 }
181 let import_stats = git_fetch.import_refs()?;
182 print_git_import_stats(ui, tx.repo(), &import_stats, true)?;
183 warn_if_branches_not_found(ui, tx, branch_names, remotes)
184}
185
186fn warn_if_branches_not_found(
187 ui: &mut Ui,
188 tx: &WorkspaceCommandTransaction,
189 branches: &[StringPattern],
190 remotes: &[&RemoteName],
191) -> Result<(), CommandError> {
192 let mut missing_branches = vec![];
193 for branch in branches {
194 let matches = remotes.iter().any(|&remote| {
195 let remote = StringPattern::exact(remote);
196 tx.repo()
197 .view()
198 .remote_bookmarks_matching(branch, &remote)
199 .next()
200 .is_some()
201 || tx
202 .base_repo()
203 .view()
204 .remote_bookmarks_matching(branch, &remote)
205 .next()
206 .is_some()
207 });
208 if !matches {
209 missing_branches.push(branch);
210 }
211 }
212
213 if !missing_branches.is_empty() {
214 writeln!(
215 ui.warning_default(),
216 "No branch matching {} found on any specified/configured remote",
217 missing_branches.iter().map(|b| format!("`{b}`")).join(", ")
218 )?;
219 }
220
221 Ok(())
222}