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