Skip to content

Commit 7288b37

Browse files
committed
First pass at "pull" UI and other related enhancements to simplify configuration/deployment
Fixes #234 Fixes #237 Fixes #33 Fixes #254 Fixes #229
1 parent 18d1a7b commit 7288b37

File tree

9 files changed

+405
-16
lines changed

9 files changed

+405
-16
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.2.0] - Unreleased
9+
10+
### Added
11+
- Page to support deployment (git pull and run pull event handler) with verbose output
12+
- Support for git clone to initialize namespace via Settings page (#234, #237)
13+
- Support for automatically creating SSH keys for use as deploy keys (#33)
14+
15+
### Fixed
16+
- Protect against Favorites links containing control characters (#254)
17+
- Green checks for valid paths shown consistently (#229)
18+
819
## [2.1.1] - 2023-02-24
920

1021
### Fixed

cls/SourceControl/Git/API.cls

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ ClassMethod Configure()
4040
}
4141

4242
/// API for git pull - just wraps Utils
43-
ClassMethod Pull()
43+
ClassMethod Pull(preview As %Boolean = 0)
4444
{
45-
quit ##class(SourceControl.Git.Utils).Pull()
45+
quit ##class(SourceControl.Git.Utils).Pull(,.preview)
4646
}
4747

4848
/// Locks the environment to prevent changes to code other than through git pull.

cls/SourceControl/Git/Extension.cls

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,4 @@ Method AddToSourceControl(InternalName As %String, Description As %String = "")
335335
}
336336

337337
}
338+

cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ Method OnPull() As %Status
3131
}
3232

3333
}
34+

cls/SourceControl/Git/Utils.cls

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,7 @@ ClassMethod UserAction(InternalName As %String, MenuName As %String, ByRef Targe
180180
// cleanup items info
181181
kill @..#Storage@("items")
182182
kill @..#Storage@("TSH")
183-
do ..RunGitCommand("init",.errStream,.outStream)
184-
$$$NewLineIfNonEmptyStream(errStream)
185-
do errStream.OutputToDevice()
186-
$$$NewLineIfNonEmptyStream(outStream)
187-
do outStream.OutputToDevice()
183+
do ..Init()
188184
} else {
189185
set ec = ..MakeError("Unable to create folder "_..TempFolder())
190186
}
@@ -271,6 +267,15 @@ ClassMethod AfterUserAction(Type As %Integer, Name As %String, InternalName As %
271267
quit $$$OK
272268
}
273269

270+
ClassMethod Init()
271+
{
272+
do ..RunGitCommand("init",.errStream,.outStream)
273+
$$$NewLineIfNonEmptyStream(errStream)
274+
do errStream.OutputToDevice()
275+
$$$NewLineIfNonEmptyStream(outStream)
276+
do outStream.OutputToDevice()
277+
}
278+
274279
ClassMethod Revert(InternalName As %String) As %Status
275280
{
276281
set filename = ..FullExternalName(.InternalName)
@@ -342,7 +347,7 @@ ClassMethod Fetch(ByRef diffFiles) As %Status
342347
quit $$$OK
343348
}
344349

345-
ClassMethod Pull(remote As %String = "origin") As %Status
350+
ClassMethod Pull(remote As %String = "origin", preview As %Boolean = 0) As %Status
346351
{
347352
#define Force 1
348353
do ##class(SourceControl.Git.Utils).RunGitCommandWithInput("branch",,.errStream,.outStream,"--show-current")
@@ -376,7 +381,12 @@ ClassMethod Pull(remote As %String = "origin") As %Status
376381
}
377382
if ('$data(files)) {
378383
write !, ?4, "None"
379-
write !, "Already up to date. Git Pull will not be executed."
384+
if preview {
385+
quit $$$OK
386+
}
387+
write !, "Already up to date."
388+
quit $$$OK
389+
} elseif preview {
380390
quit $$$OK
381391
}
382392

@@ -423,6 +433,47 @@ ClassMethod Pull(remote As %String = "origin") As %Status
423433
quit event.OnPull()
424434
}
425435

436+
ClassMethod Clone(remote As %String)
437+
{
438+
set settings = ##class(SourceControl.Git.Settings).%New()
439+
set sc = ..RunGitWithArgs(.errStream, .outStream, "clone", remote, settings.namespaceTemp)
440+
$$$NewLineIfNonEmptyStream(errStream)
441+
do errStream.OutputToDevice()
442+
$$$NewLineIfNonEmptyStream(outStream)
443+
do outStream.OutputToDevice()
444+
}
445+
446+
ClassMethod GenerateSSHKeyPair()
447+
{
448+
set settings = ##class(SourceControl.Git.Settings).%New()
449+
set file = ##class(%Stream.FileCharacter).%New()
450+
set dir = ##class(%File).GetDirectory(settings.privateKeyFile)
451+
if (file = "") {
452+
Throw ##class(%Exception.General).%New("File "_file_" already exists")
453+
}
454+
if ##class(%File).Exists(settings.privateKeyFile) {
455+
Throw ##class(%Exception.General).%New("File "_file_" already exists")
456+
}
457+
do ##class(%File).CreateDirectoryChain(dir)
458+
set outLog = ##class(%Library.File).TempFilename()
459+
set errLog = ##class(%Library.File).TempFilename()
460+
do $zf(-100,"/SHELL /STDOUT="_$$$QUOTE(outLog)_" /STDERR="_$$$QUOTE(errLog)_" /STDIN="_$$$QUOTE(file.Filename),
461+
"ssh-keygen",
462+
"-t","ed25519",
463+
"-C",settings.gitUserEmail,
464+
"-f",settings.privateKeyFile,
465+
"-N","")
466+
467+
set errStream = ##class(%Stream.FileCharacter).%OpenId(errLog,,.sc)
468+
set outStream = ##class(%Stream.FileCharacter).%OpenId(outLog,,.sc)
469+
set outStream.TranslateTable="UTF8"
470+
for stream=errStream,outStream {
471+
set stream.RemoveOnClose = 1
472+
}
473+
do outStream.OutputToDevice()
474+
do errStream.OutputToDevice()
475+
}
476+
426477
ClassMethod IsNamespaceInGit() As %Boolean [ CodeMode = expression ]
427478
{
428479
##class(%File).Exists(..TempFolder()_".git")
@@ -1789,15 +1840,22 @@ ClassMethod ConfigureWeb()
17891840
set installNamespace = $Namespace
17901841
new $Namespace
17911842
set $Namespace = "%SYS"
1792-
write !,"Adding favorite for all users... "
1843+
write !,"Adding favorites for all users:"
17931844
set sql = "insert or update into %SYS_Portal.Users (Username, Page, Data) "_
17941845
"select ID,?,? from Security.Users"
17951846
set caption = "Git: "_installNamespace
17961847
set link = "/isc/studio/usertemplates/gitsourcecontrol/webuidriver.csp/"_installNamespace_"/"
1797-
do ##class(%SQL.Statement).%ExecDirect(,sql,caption,link).%Display()
1848+
write !,"Adding Git favorite... "
1849+
set statement = ##class(%SQL.Statement).%New()
1850+
set statement.%SelectMode = 0
1851+
do ##class(%SQL.Statement).%ExecDirect(statement,sql,caption,link).%Display()
1852+
set caption = "Git Pull: "_installNamespace
1853+
set link = "/isc/studio/usertemplates/gitsourcecontrol/pull.csp?$NAMESPACE="_installNamespace
1854+
write !,"Adding Git Pull favorite... "
1855+
do ##class(%SQL.Statement).%ExecDirect(statement,sql,caption,link).%Display()
17981856
write !,"Setting GroupById to %ISCMgtPortal for /isc/studio/usertemplates... "
17991857
set sql = "update Security.Applications set GroupById='%ISCMgtPortal' where ID = '/isc/studio/usertemplates'"
1800-
do ##class(%SQL.Statement).%ExecDirect(,sql).%Display()
1858+
do ##class(%SQL.Statement).%ExecDirect(statement,sql).%Display()
18011859
}
18021860

18031861
ClassMethod CheckInitialization()

csp/gitprojectsettings.csp

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,34 @@ body {
147147
<div class="form-group row mb-3">
148148
<div class="col-sm-1"></div>
149149
<label for="namespaceTemp" class="col-sm-3 col-form-label" data-toggle="tooltip" data-placement="top" title="Absolute path to you project">Temp folder for this namespace<br/></label>
150+
<server>
151+
set dir = ##class(%File).NormalizeDirectory(settings.namespaceTemp)
152+
if (settings.namespaceTemp '= "") && ##class(%File).DirectoryExists(dir_".git") {
153+
set class = "form-control is-valid"
154+
} else {
155+
set class = "form-control"
156+
}
157+
</server>
150158
<div class="col-sm-7">
151-
<input type="text" class="form-control" id="namespaceTemp" name="namespaceTemp" value='#(..EscapeHTML(settings.namespaceTemp))#' placeholder="e.g. C:\someproj\"/>
159+
<input type="text" class="#(class)#" id="namespaceTemp" name="namespaceTemp" value='#(..EscapeHTML(settings.namespaceTemp))#' placeholder="e.g. C:\someproj\"/>
152160
</div>
153161
</div>
154-
162+
<server>
163+
set dir = ##class(%File).NormalizeDirectory(settings.namespaceTemp)
164+
if (settings.namespaceTemp '= "") && '##class(%File).DirectoryExists(dir_".git") {
165+
&html<
166+
<div class="form-group row mb-3">
167+
<div class="col-sm-4"></div>
168+
<div class="col-sm-8 neutral-feedback">
169+
<span id="initMsg">Git has not been not initialized for folder.</span>
170+
<button id="initBtn" onclick="init(); return false;" class="btn btn-sm btn-outline-dark">Initialize</button>
171+
<button id="cloneBtn" onclick="clone(); return false;" class="btn btn-sm btn-outline-dark">Clone...</button>
172+
<pre id="initOutput"></pre>
173+
</div>
174+
</div>
175+
>
176+
}
177+
</server>
155178
<div class="form-group row mb-3">
156179
<div class="col-sm-1"></div>
157180
<label for="privateKeyFile" class="col-sm-3 col-form-label" data-toggle="tooltip" data-placement="top" title="Absolute path to your private SSH key file">SSH Private Key File</label>
@@ -169,16 +192,44 @@ body {
169192
} else {
170193
set class = "form-control is-invalid"
171194
set divClass = "invalid-feedback"
172-
set feedbackText = "File not found"
195+
set feedbackText = "<button class='btn btn-sm btn-outline-dark' onclick='genSSH(); return false;'>"_
196+
"File not found - generate a new key pair?"_
197+
"</button>"
173198
}
174199
</server>
175200
<input type="text" class="#(class)#" id="privateKeyFile" name="privateKeyFile" value='#(..EscapeHTML(settings.privateKeyFile))#' placeholder="C:\Users\ExampleUser\.ssh\id_rsa"/>
176201
<div class = "#(divClass)#">
177202
#(feedbackText)#
203+
<pre id="sshOutput"></pre>
178204
</div>
179205
</div>
180206
</div>
181207

208+
<server>
209+
if (settings.privateKeyFile '= "") && fileExists {
210+
set pubKeyName = settings.privateKeyFile_".pub"
211+
if ##class(%File).Exists(pubKeyName) {
212+
set pubStream = ##class(%Stream.FileCharacter).%OpenId(pubKeyName)
213+
set pubKeyText = ""
214+
while 'pubStream.AtEnd {
215+
set pubKeyText = pubKeyText_pubStream.Read()
216+
}
217+
&html<
218+
<div class="form-group row mb-3">
219+
<div class="col-sm-1"></div>
220+
<div class="col-sm-3">Public key:</div>
221+
<div class="col-sm-5">
222+
<pre id="publicKey">#(pubKeyText)#</pre>
223+
</div>
224+
<div class="col-sm-1">
225+
<button class='btn btn-sm btn-outline-dark' onclick="copyToClipboard('publicKey'); return false;">Copy</button>
226+
</div>
227+
</div>
228+
>
229+
}
230+
}
231+
</server>
232+
182233
<div class="form-group row mb-3">
183234
<div class="col-sm-1"></div>
184235
<label for="pullEventClass" class="col-sm-3 col-form-label" data-toggle="tooltip" data-placement="top" title="Handler class for git pull">Pull Event Class</label>
@@ -332,6 +383,50 @@ body {
332383
<script src="js/popper.min.js"></script>
333384
<script src="js/bootstrap.min.js"></script>
334385
<script type="text/javascript">
386+
function getSocket(urlPostfix) {
387+
var socketURL = window.location.href.replace('http','ws').replace('gitprojectsettings.csp','socket.csp') + "&" + urlPostfix;
388+
return new WebSocket(socketURL);
389+
}
390+
391+
function init() {
392+
disableActionButtons();
393+
var ws = getSocket("method=init");
394+
ws.onmessage = showOutput('initOutput');
395+
}
396+
397+
function clone() {
398+
var remote = prompt("Enter URL for repository to clone:");
399+
if ((remote == null) || (remote == "")) {
400+
return;
401+
}
402+
disableActionButtons();
403+
var ws = getSocket("method=clone&remote=" + encodeURIComponent(remote));
404+
ws.onmessage = showOutput('initOutput');
405+
}
406+
407+
function genSSH() {
408+
var ws = getSocket("method=sshkeygen");
409+
ws.onmessage = showOutput('sshOutput');
410+
}
411+
412+
function copyToClipboard(elementId) {
413+
navigator.clipboard.writeText(document.getElementById(elementId).innerText);
414+
}
415+
416+
function disableActionButtons()
417+
{
418+
document.getElementById('initBtn').disabled = true;
419+
document.getElementById('cloneBtn').disabled = true;
420+
}
421+
422+
function showOutput(target) {
423+
return function(event) {
424+
// IE retains the newlines that $System.Encryption.Base64Encode adds after every 76 characters;
425+
// these are therefore removed.
426+
var msg = JSON.parse(atob(event.data.split('\r\n').join('')));
427+
document.getElementById(target).innerHTML += msg.content;
428+
}
429+
}
335430

336431
$(function () {
337432
$('[data-toggle="tooltip"]').tooltip({

csp/pull.csp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<title>Git Fetch, Git Pull, and Load to IRIS</title>
8+
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css" />
9+
<link rel="stylesheet" type="text/css" href="css/git-webui.css" />
10+
</head>
11+
<body>
12+
<pre id="preview" style="white-space: pre-wrap;">
13+
</pre>
14+
<button id="execute" onclick="execute()" disabled="true">Pull and Load Changes</button>
15+
<pre id="pull" style="white-space: pre-wrap;">
16+
</pre>
17+
<script type="text/javascript">
18+
function preview() {
19+
var socketURL = window.location.href.replace('http','ws').replace('pull.csp','socket.csp');
20+
socketURL += "&method=preview"
21+
var ws = new WebSocket(socketURL);
22+
ws.onmessage = function(event) {
23+
// IE retains the newlines that $System.Encryption.Base64Encode adds after every 76 characters;
24+
// these are therefore removed.
25+
var msg = JSON.parse(atob(event.data.split('\r\n').join('')));
26+
document.getElementById('preview').innerHTML += msg.content;
27+
}
28+
ws.onclose = function(event) {
29+
document.getElementById('execute').disabled = false;
30+
}
31+
}
32+
33+
function execute() {
34+
var socketURL = window.location.href.replace('http','ws').replace('pull.csp','socket.csp');
35+
socketURL += "&method=pull"
36+
var ws = new WebSocket(socketURL);
37+
ws.onmessage = function(event) {
38+
// IE retains the newlines that $System.Encryption.Base64Encode adds after every 76 characters;
39+
// these are therefore removed.
40+
var msg = JSON.parse(atob(event.data.split('\r\n').join('')));
41+
document.getElementById('pull').innerHTML += msg.content;
42+
}
43+
ws.onclose = function(event) {
44+
// TODO: detect errors
45+
}
46+
}
47+
48+
preview();
49+
</script>
50+
</body>
51+
</html>

0 commit comments

Comments
 (0)