1 /*
2 * SessionVCS.cpp
3 *
4 * Copyright (C) 2021 by RStudio, PBC
5 *
6 * Unless you have received this program directly from RStudio pursuant
7 * to the terms of a commercial license agreement with RStudio, then
8 * this program is licensed to you under the terms of version 3 of the
9 * GNU Affero General Public License. This program is distributed WITHOUT
10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13 *
14 */
15
16 #include "SessionVCS.hpp"
17
18 #include <core/Exec.hpp>
19 #include <core/StringUtils.hpp>
20 #include <core/system/Environment.hpp>
21 #include <core/system/Process.hpp>
22 #include <core/system/ShellUtils.hpp>
23
24 #include <session/SessionModuleContext.hpp>
25 #include <session/projects/SessionProjects.hpp>
26 #include <session/SessionConsoleProcess.hpp>
27 #include <session/prefs/UserPrefs.hpp>
28
29 #include "vcs/SessionVCSUtils.hpp"
30
31 #include "SessionSVN.hpp"
32 #include "SessionGit.hpp"
33
34 #include "SessionAskPass.hpp"
35
36 #include "session-config.h"
37
38 #ifdef RSTUDIO_SERVER
39 #include <core/system/Crypto.hpp>
40 #endif
41
42 using namespace rstudio::core;
43
44 namespace rstudio {
45 namespace session {
46
47 namespace {
48 const char * const kVcsIdNone = "none";
49 } // anonymous namespace
50
51 namespace module_context {
52
53 // if we change the name of one of the VCS systems then there will
54 // be persisted versions of the name on disk we need to deal with
55 // migrating. This function can do that migration -- note the initial
56 // default implementation is to return "none" for unrecognized options
normalizeVcsOverride(const std::string & vcsOverride)57 std::string normalizeVcsOverride(const std::string& vcsOverride)
58 {
59 if (vcsOverride == modules::git::kVcsId)
60 return vcsOverride;
61 else if (vcsOverride == modules::svn::kVcsId)
62 return vcsOverride;
63 else if (vcsOverride == kVcsIdNone)
64 return vcsOverride;
65 else
66 return "";
67 }
68
69 } // namespace module_context
70
71 namespace modules {
72 namespace source_control {
73
74 namespace {
75
vcsClone(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)76 Error vcsClone(const json::JsonRpcRequest& request,
77 json::JsonRpcResponse* pResponse)
78 {
79 std::string vcsName;
80 std::string url;
81 std::string username;
82 std::string dirName;
83 std::string parentDir;
84 Error error = json::readObjectParam(request.params, 0,
85 "vcs_name", &vcsName,
86 "repo_url", &url,
87 "username", &username,
88 "directory_name", &dirName,
89 "parent_path", &parentDir);
90 if (error)
91 return error;
92
93 ask_pass::setActiveWindow(request.sourceWindow);
94
95 FilePath parentPath = module_context::resolveAliasedPath(parentDir);
96
97 boost::shared_ptr<console_process::ConsoleProcess> pCP;
98 if (vcsName == git::kVcsId)
99 {
100 Error error = git::clone(url,
101 dirName,
102 parentPath,
103 &pCP);
104 if (error)
105 return error;
106 }
107 else if (vcsName == svn::kVcsId)
108 {
109 Error error = svn::checkout(url,
110 username,
111 dirName,
112 parentPath,
113 &pCP);
114 if (error)
115 return error;
116 }
117 else
118 {
119 return systemError(json::errc::ParamInvalid, ERROR_LOCATION);
120 }
121
122 pResponse->setResult(pCP->toJson(console_process::ClientSerialization));
123
124 return Success();
125 }
126
127 class NullFileDecorationContext : public FileDecorationContext
128 {
decorateFile(const FilePath &,json::Object *)129 void decorateFile(const FilePath&, json::Object*)
130 {
131 }
132 };
133
134 } // anonymous namespace
135
fileDecorationContext(const core::FilePath & rootDir,bool implicit)136 boost::shared_ptr<FileDecorationContext> fileDecorationContext(
137 const core::FilePath& rootDir,
138 bool implicit)
139 {
140 if (implicit && !prefs::userPrefs().vcsAutorefresh())
141 {
142 return boost::shared_ptr<FileDecorationContext>(
143 new NullFileDecorationContext());
144 }
145 else if (git::isWithinGitRoot(rootDir))
146 {
147 return boost::shared_ptr<FileDecorationContext>(
148 new git::GitFileDecorationContext(rootDir));
149 }
150 else if (svn::isSvnEnabled())
151 {
152 return boost::shared_ptr<FileDecorationContext>(
153 new svn::SvnFileDecorationContext(rootDir));
154 }
155 else
156 {
157 return boost::shared_ptr<FileDecorationContext>(
158 new NullFileDecorationContext());
159 }
160 }
161
activeVCS()162 VCS activeVCS()
163 {
164 return git::isGitEnabled() ? VCSGit : VCSNone;
165 }
166
activeVCSName()167 std::string activeVCSName()
168 {
169 if (git::isGitEnabled())
170 return git::kVcsId;
171 else if (svn::isSvnEnabled())
172 return svn::kVcsId;
173 else
174 return std::string();
175 }
176
isGitInstalled()177 bool isGitInstalled()
178 {
179 return git::isGitInstalled();
180 }
181
isSvnInstalled()182 bool isSvnInstalled()
183 {
184 return svn::isSvnInstalled();
185 }
186
getTrueHomeDir()187 FilePath getTrueHomeDir()
188 {
189 #if _WIN32
190 // On Windows, R's idea of "$HOME" is not, by default, the same as
191 // $USERPROFILE, which is what we want for ssh purposes
192 return FilePath(string_utils::systemToUtf8(core::system::getenv("USERPROFILE")));
193 #else
194 return FilePath(string_utils::systemToUtf8(core::system::getenv("HOME")));
195 #endif
196 }
197
defaultSshKeyDir()198 FilePath defaultSshKeyDir()
199 {
200 return getTrueHomeDir().completeChildPath(".ssh");
201 }
202
enqueueRefreshEvent()203 void enqueueRefreshEvent()
204 {
205 vcs_utils::enqueueRefreshEvent();
206 }
207
208
209
initialize()210 core::Error initialize()
211 {
212 git::initialize();
213 svn::initialize();
214
215 // http endpoints
216 using boost::bind;
217 using namespace module_context;
218 ExecBlock initBlock;
219 initBlock.addFunctions()
220 (bind(registerRpcMethod, "vcs_clone", vcsClone));
221 Error error = initBlock.execute();
222 if (error)
223 return error;
224
225 // If VCS is disabled, or we're not in a project, do nothing
226 const projects::ProjectContext& projContext = projects::projectContext();
227 FilePath workingDir = projContext.directory();
228
229 if (!session::options().allowVcs() || !prefs::userPrefs().vcsEnabled() || workingDir.isEmpty())
230 return Success();
231
232
233 // If Git or SVN was explicitly specified, choose it if valid
234 projects::RProjectVcsOptions vcsOptions;
235 if (projContext.hasProject())
236 {
237 Error vcsError = projContext.readVcsOptions(&vcsOptions);
238 if (vcsError)
239 LOG_ERROR(vcsError);
240 }
241
242 if (vcsOptions.vcsOverride == kVcsIdNone)
243 {
244 return Success();
245 }
246 else if (vcsOptions.vcsOverride == git::kVcsId)
247 {
248 if (git::isGitInstalled() && git::isGitDirectory(workingDir))
249 return git::initializeGit(workingDir);
250 return Success();
251 }
252 else if (vcsOptions.vcsOverride == svn::kVcsId)
253 {
254 if (svn::isSvnInstalled() && svn::isSvnDirectory(workingDir))
255 return svn::initializeSvn(workingDir);
256 return Success();
257 }
258
259 if (git::isGitInstalled() && git::isGitDirectory(workingDir))
260 {
261 return git::initializeGit(workingDir);
262 }
263 else if (svn::isSvnInstalled() && svn::isSvnDirectory(workingDir))
264 {
265 return svn::initializeSvn(workingDir);
266 }
267 else
268 {
269 return Success(); // none specified or detected
270 }
271 }
272
273 } // namespace source_control
274 } // namespace modules
275 } // namespace session
276 } // namespace rstudio
277
278 namespace rstudio {
279 namespace session {
280 namespace module_context {
281
vcsContext(const FilePath & workingDir)282 VcsContext vcsContext(const FilePath& workingDir)
283 {
284 using namespace session::modules;
285 using namespace session::modules::source_control;
286
287 // inspect current vcs state (underlying functions execute child
288 // processes so we want to be sure to only call them once)
289 bool gitInstalled = isGitInstalled();
290 bool isGitDirectory = gitInstalled && git::isGitDirectory(workingDir);
291 bool svnInstalled = isSvnInstalled();
292 bool isSvnDirectory = svnInstalled && svn::isSvnDirectory(workingDir);
293
294 // detected vcs
295 VcsContext context;
296 if (isGitDirectory)
297 context.detectedVcs = git::kVcsId;
298 else if (isSvnDirectory)
299 context.detectedVcs = svn::kVcsId;
300 else
301 context.detectedVcs = kVcsIdNone;
302
303 // applicable vcs
304 if (gitInstalled)
305 context.applicableVcs.push_back(git::kVcsId);
306 if (isSvnDirectory)
307 context.applicableVcs.push_back(svn::kVcsId);
308
309 // remote urls
310 if (isGitDirectory)
311 context.gitRemoteOriginUrl = git::remoteOriginUrl(workingDir);
312 if (isSvnDirectory)
313 context.svnRepositoryRoot = svn::repositoryRoot(workingDir);
314
315 return context;
316 }
317
318 } // namespace module_context
319 } // namespace session
320 } // namespace rstudio
321