Line data Source code
1 : /*!
2 : * \file esys/repo/libgit2/git_libgit2.cpp
3 : * \brief
4 : *
5 : * \cond
6 : * __legal_b__
7 : *
8 : * Copyright (c) 2020-2023 Michel Gillet
9 : * Distributed under the MIT License.
10 : * (See accompanying file LICENSE.txt or
11 : * copy at https://opensource.org/licenses/MIT)
12 : *
13 : * __legal_e__
14 : * \endcond
15 : *
16 : */
17 :
18 : #include "esys/repo/esysrepo_prec.h"
19 : #include "esys/repo/libgit2/git.h"
20 : #include "esys/repo/libgit2/gitimpl.h"
21 :
22 : #include <git2.h>
23 :
24 : #include <boost/filesystem.hpp>
25 : #include <boost/process/search_path.hpp>
26 :
27 : #include <cstdlib>
28 : #include <string>
29 : #include <string_view>
30 :
31 : namespace esys::repo::libgit2
32 : {
33 :
34 : bool Git::s_detect_ssh_agent_done = false;
35 : bool Git::s_ssh_agent_running = false;
36 : bool Git::s_exec_probe_done = false;
37 : bool Git::s_exec_available = false;
38 : int Git::s_libssh2_supported = -1;
39 : int Git::s_exec_supported = -1;
40 :
41 145 : std::shared_ptr<GitBase> Git::new_ptr()
42 : {
43 145 : return std::make_shared<Git>();
44 : }
45 :
46 203 : Git::Git()
47 203 : : GitBase()
48 : {
49 203 : m_impl = std::make_unique<GitImpl>(this);
50 203 : }
51 :
52 203 : Git::~Git() = default;
53 :
54 143 : Result Git::open(const std::string &folder)
55 : {
56 143 : set_folder(folder);
57 143 : return get_impl()->open(folder);
58 : }
59 :
60 268 : bool Git::is_open()
61 : {
62 268 : return get_impl()->is_open();
63 : }
64 :
65 241 : Result Git::close()
66 : {
67 241 : return get_impl()->close();
68 : }
69 :
70 3 : Result Git::init_bare(const std::string &folder_path)
71 : {
72 3 : return get_impl()->init_bare(folder_path);
73 : }
74 :
75 1 : Result Git::init(const std::string &folder_path)
76 : {
77 1 : return get_impl()->init(folder_path);
78 : }
79 :
80 0 : void Git::close_on_error()
81 : {
82 0 : return get_impl()->close_on_error();
83 : }
84 :
85 4 : Result Git::get_remotes(std::vector<git::Remote> &remotes)
86 : {
87 4 : return get_impl()->get_remotes(remotes);
88 : }
89 :
90 7 : Result Git::get_head_branch_remote(git::Branch &branch, git::Remote &remote)
91 : {
92 7 : return get_impl()->get_head_branch_remote(branch, remote);
93 : }
94 :
95 5 : Result Git::add_remote(const std::string &name, const std::string &url)
96 : {
97 5 : return get_impl()->add_remote(name, url);
98 : }
99 :
100 95 : Result Git::get_branches(git::Branches &branches, git::BranchType branch_type)
101 : {
102 95 : return get_impl()->get_branches(branches, branch_type);
103 : }
104 :
105 150 : Result Git::clone(const std::string &url, const std::string &path, const std::string &rev,
106 : const git::CloneOptions &options)
107 : {
108 150 : set_url(url);
109 150 : return get_impl()->clone(url, path, rev, options);
110 : }
111 :
112 74 : Result Git::checkout(const std::string &branch, bool force)
113 : {
114 74 : return get_impl()->checkout(branch, force);
115 : }
116 :
117 2 : Result Git::reset(const git::CommitHash &commit, git::ResetType type)
118 : {
119 2 : return get_impl()->reset(commit, type);
120 : }
121 :
122 2 : Result Git::fastforward(const git::CommitHash &commit)
123 : {
124 2 : return get_impl()->fastforward(commit);
125 : }
126 :
127 29 : Result Git::get_last_commit(git::CommitHash &commit)
128 : {
129 29 : return get_impl()->get_last_commit(commit);
130 : }
131 :
132 7 : Result Git::get_last_commit(git::Commit &commit, bool get_all_notes)
133 : {
134 7 : return get_impl()->get_last_commit(commit, get_all_notes);
135 : }
136 :
137 5 : Result Git::get_parent_commit(const git::CommitHash &commit, git::CommitHash &parent, int nth_parent)
138 : {
139 5 : return get_impl()->get_parent_commit(commit, parent, nth_parent);
140 : }
141 :
142 30 : Result Git::is_dirty(bool &dirty)
143 : {
144 30 : return get_impl()->is_dirty(dirty);
145 : }
146 :
147 26 : Result Git::is_detached(bool &detached)
148 : {
149 26 : return get_impl()->is_detached(detached);
150 : }
151 :
152 31 : Result Git::get_status(git::RepoStatus &repo_status)
153 : {
154 31 : return m_impl->get_status(repo_status);
155 : }
156 :
157 20 : Result_t<bool> Git::is_ssh_agent_running(bool log_once)
158 : {
159 20 : if (!s_detect_ssh_agent_done) detect_ssh_agent(log_once);
160 :
161 20 : return s_ssh_agent_running;
162 : }
163 :
164 20 : void Git::detect_ssh_agent(bool log_once)
165 : {
166 20 : if (s_detect_ssh_agent_done) return;
167 :
168 1 : s_detect_ssh_agent_done = true;
169 1 : s_ssh_agent_running = m_impl->is_ssh_agent_running();
170 :
171 2 : if (s_ssh_agent_running && log_once) info("SSH agent detected");
172 : }
173 :
174 11 : bool Git::do_is_ssh_backend_supported(git::SshBackend backend) const
175 : {
176 11 : if (!(git_libgit2_features() & GIT_FEATURE_SSH)) return false;
177 :
178 11 : switch (backend)
179 : {
180 7 : case git::SshBackend::LibSSH2: return probe_ssh_backend_compiled("libssh2", s_libssh2_supported);
181 4 : case git::SshBackend::Exec: return probe_ssh_backend_compiled("exec", s_exec_supported);
182 : }
183 : return false;
184 : }
185 :
186 11 : bool Git::probe_ssh_backend_compiled(const char *name, int &cache)
187 : {
188 11 : if (cache >= 0) return cache != 0;
189 :
190 2 : git_buf previous = GIT_BUF_INIT;
191 2 : const bool had_previous = (git_libgit2_opts(GIT_OPT_GET_SSH_BACKEND, &previous) == 0);
192 :
193 2 : const int set_error = git_libgit2_opts(GIT_OPT_SET_SSH_BACKEND, name);
194 2 : cache = (set_error == 0) ? 1 : 0;
195 :
196 2 : if (had_previous && previous.ptr != nullptr)
197 2 : git_libgit2_opts(GIT_OPT_SET_SSH_BACKEND, previous.ptr);
198 0 : else if (set_error == 0)
199 : // Do not leave a temporary probe selection as the process-wide backend
200 : // when there was no prior GET value to restore.
201 0 : git_libgit2_opts(GIT_OPT_SET_SSH_BACKEND, "libssh2");
202 :
203 2 : git_buf_dispose(&previous);
204 2 : return cache != 0;
205 : }
206 :
207 : namespace
208 : {
209 :
210 2 : std::string get_env_var(const char *name)
211 : {
212 : #ifdef _WIN32
213 : char *buffer = nullptr;
214 : size_t length = 0;
215 : if (_dupenv_s(&buffer, &length, name) != 0 || buffer == nullptr) return {};
216 : std::string value(buffer);
217 : free(buffer);
218 : return value;
219 : #else
220 2 : const char *value = std::getenv(name);
221 2 : return value != nullptr ? std::string(value) : std::string();
222 : #endif
223 : }
224 :
225 : } // namespace
226 :
227 1 : bool Git::probe_openssh_client()
228 : {
229 2 : auto command_exists = [](const std::string &command) -> bool {
230 1 : if (command.empty()) return false;
231 :
232 1 : boost::filesystem::path path(command);
233 2 : if (path.is_absolute() || path.has_parent_path()) return boost::filesystem::exists(path);
234 :
235 : #ifdef _WIN32
236 : boost::filesystem::path found = boost::process::search_path(path.filename().string());
237 : #else
238 2 : boost::filesystem::path found = boost::process::search_path(command);
239 : #endif
240 1 : return !found.empty();
241 2 : };
242 :
243 1 : const std::string ssh_command = get_env_var("GIT_SSH_COMMAND");
244 1 : if (!ssh_command.empty())
245 : {
246 : // Shell command line: verify the first token looks findable when possible.
247 0 : auto start = ssh_command.find_first_not_of(" \t");
248 0 : if (start == std::string::npos) return true;
249 0 : auto end = ssh_command.find_first_of(" \t", start);
250 0 : const std::string first_token =
251 0 : ssh_command.substr(start, end == std::string::npos ? std::string::npos : end - start);
252 0 : if (first_token.empty()) return true;
253 0 : return command_exists(first_token);
254 0 : }
255 :
256 1 : const std::string ssh = get_env_var("GIT_SSH");
257 1 : if (!ssh.empty()) return command_exists(ssh);
258 :
259 : #ifdef _WIN32
260 : return command_exists("ssh.exe") || command_exists("ssh");
261 : #else
262 1 : return command_exists("ssh");
263 : #endif
264 1 : }
265 :
266 3 : bool Git::do_is_ssh_backend_available(git::SshBackend backend, bool force)
267 : {
268 3 : if (!do_is_ssh_backend_supported(backend)) return false;
269 :
270 3 : if (backend == git::SshBackend::LibSSH2) return true;
271 :
272 2 : if (force) s_exec_probe_done = false;
273 :
274 2 : if (!s_exec_probe_done)
275 : {
276 1 : s_exec_probe_done = true;
277 1 : s_exec_available = probe_openssh_client();
278 : }
279 :
280 2 : return s_exec_available;
281 : }
282 :
283 5 : Result Git::do_set_ssh_backend(git::SshBackend backend)
284 : {
285 5 : if (!do_is_ssh_backend_supported(backend))
286 0 : return ESYSREPO_RESULT(ResultCode::GIT_SSH_BACKEND_NOT_SUPPORTED);
287 :
288 5 : if (backend == git::SshBackend::Exec && !do_is_ssh_backend_available(backend, false))
289 0 : return ESYSREPO_RESULT(ResultCode::GIT_SSH_BACKEND_NOT_SUPPORTED, "OpenSSH client not found");
290 :
291 5 : const char *name = (backend == git::SshBackend::Exec) ? "exec" : "libssh2";
292 5 : int error = git_libgit2_opts(GIT_OPT_SET_SSH_BACKEND, name);
293 5 : if (error < 0) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, error);
294 :
295 5 : return ESYSREPO_RESULT(ResultCode::OK);
296 : }
297 :
298 195 : Result_t<git::SshBackend> Git::do_get_ssh_backend() const
299 : {
300 195 : if (!(git_libgit2_features() & GIT_FEATURE_SSH))
301 0 : return ESYSREPO_RESULT_T(ResultCode::GIT_SSH_BACKEND_NOT_SUPPORTED, git::SshBackend::LibSSH2);
302 :
303 195 : git_buf buf = GIT_BUF_INIT;
304 195 : int error = git_libgit2_opts(GIT_OPT_GET_SSH_BACKEND, &buf);
305 195 : if (error < 0)
306 0 : return ESYSREPO_RESULT_T(ResultCode::GIT_GENERIC_ERROR, git::SshBackend::LibSSH2);
307 :
308 195 : git::SshBackend backend = git::SshBackend::LibSSH2;
309 195 : if (buf.ptr != nullptr && std::string_view(buf.ptr) == "exec") backend = git::SshBackend::Exec;
310 :
311 195 : git_buf_dispose(&buf);
312 390 : return backend;
313 : }
314 :
315 23 : Result Git::merge_analysis(const std::vector<std::string> &refs, git::MergeAnalysisResult &merge_analysis_result,
316 : std::vector<git::CommitHash> &commits)
317 : {
318 23 : return m_impl->merge_analysis(refs, merge_analysis_result, commits);
319 : }
320 :
321 68 : Result Git::fetch(const std::string &remote)
322 : {
323 68 : return m_impl->fetch(remote);
324 : }
325 :
326 2 : Result Git::fetch_all_notes(const std::string &remote)
327 : {
328 2 : return m_impl->fetch_all_notes(remote);
329 : }
330 :
331 4 : Result Git::add_note(git::NoteId ¬e_id, const std::string &reference, const std::string &message, bool overwrite)
332 : {
333 4 : return m_impl->add_note(note_id, reference, message, overwrite);
334 : }
335 :
336 1 : Result Git::remove_note(const git::NoteId ¬e_id)
337 : {
338 1 : return m_impl->remove_note(note_id);
339 : }
340 :
341 0 : Result Git::remove_note(const git::CommitHash &commit_hash, const std::string &reference)
342 : {
343 0 : return m_impl->remove_note(commit_hash, reference);
344 : }
345 :
346 1 : Result Git::remove_note_last_commit(const std::string &reference)
347 : {
348 1 : return m_impl->remove_note_last_commit(reference);
349 : }
350 :
351 2 : Result Git::push(const std::string &remote, const std::string &src_ref, const std::string &dst_ref)
352 : {
353 2 : return m_impl->push(remote, src_ref, dst_ref);
354 : }
355 :
356 1 : Result Git::push_notes(const std::string &remote, const std::string &reference)
357 : {
358 1 : return m_impl->push_notes(remote, reference);
359 : }
360 :
361 15 : Result_t<bool> Git::has_branch(const std::string &name, git::BranchType branch_type)
362 : {
363 15 : return m_impl->has_branch(name, branch_type);
364 : }
365 :
366 27 : Result Git::get_hash(const std::string &revision, std::string &hash, git::BranchType branch_type)
367 : {
368 27 : return m_impl->get_hash(revision, hash, branch_type);
369 : }
370 :
371 2 : Result Git::walk_commits(std::shared_ptr<git::WalkCommit> walk_commit)
372 : {
373 6 : return m_impl->walk_commits(walk_commit);
374 : }
375 :
376 4 : Result Git::diff(const git::CommitHash &commit_hash, std::shared_ptr<git::Diff> diff)
377 : {
378 12 : return m_impl->diff(commit_hash, diff);
379 : }
380 :
381 0 : Result Git::get_ahead_behind(git::AheadBehind &ahead_behind, const git::Branch &local_branch)
382 : {
383 0 : return m_impl->get_ahead_behind(ahead_behind, local_branch);
384 : }
385 :
386 3 : Result Git::get_ahead_behind(git::AheadBehind &ahead_behind, const std::string &first_ref,
387 : const std::string &second_ref)
388 : {
389 3 : return m_impl->get_ahead_behind(ahead_behind, first_ref, second_ref);
390 : }
391 :
392 150 : void Git::set_url(const std::string &url)
393 : {
394 150 : m_url = url;
395 150 : }
396 :
397 0 : const std::string &Git::get_url()
398 : {
399 0 : return m_url;
400 : }
401 :
402 143 : void Git::set_folder(const std::string &folder)
403 : {
404 143 : m_folder = folder;
405 143 : }
406 :
407 0 : const std::string &Git::get_folder()
408 : {
409 0 : return m_folder;
410 : }
411 :
412 0 : const std::string &Git::get_version()
413 : {
414 0 : return m_impl->get_version();
415 : }
416 :
417 0 : const std::string &Git::get_lib_name()
418 : {
419 0 : return m_impl->get_lib_name();
420 : }
421 :
422 990 : void Git::debug(int level, const std::string &msg)
423 : {
424 : // log::ConsoleLockGuard<log::User> lock(this);
425 : //! \TODO must be fixed to allow writing after all console is written
426 :
427 990 : GitBase::debug(level, msg);
428 990 : }
429 :
430 2 : const std::string &Git::s_get_version()
431 : {
432 2 : return GitImpl::s_get_version();
433 : }
434 :
435 2 : const std::string &Git::s_get_lib_name()
436 : {
437 2 : return GitImpl::s_get_lib_name();
438 : }
439 :
440 2 : const std::string &Git::s_get_ssh_version()
441 : {
442 2 : return GitImpl::s_get_ssh_version();
443 : }
444 :
445 2 : const std::string &Git::s_get_ssh_lib_name()
446 : {
447 2 : return GitImpl::s_get_ssh_lib_name();
448 : }
449 :
450 1543 : GitImpl *Git::get_impl() const
451 : {
452 1543 : return m_impl.get();
453 : }
454 :
455 429 : void Git::set_logger_if(std::shared_ptr<log::Logger_if> logger_if)
456 : {
457 858 : get_impl()->set_logger_if(logger_if);
458 429 : }
459 :
460 : } // namespace esys::repo::libgit2
|