Compare .mac /.int routines and show differences
Hello,
I'm currently working on a custom version control implementation.
I'd like to show differences between a copy of a previously saved routine as a merge of the ^ROUTINE Global and the current version of that same routine.
I've found legacy Documentation for %RCMP which does the trick in the terminal but I'd like a similar result stored inside a variable.
I also want to show differences, not only see if they are the same or not.
The management portal has that exact feature, so maybe someone can direct me to the function that is being used there.
I've seen solutions with files, and one way to do it would be to write the routines to files, and compare those, but I'd like to avoid that if possible.
Thank you.
Comments
I guess all you need is to redirect the ouput of %RCMP to some %Stream of SPOOL:
In addition, you have %RCMP in your installation and just pick out what you need..png)
Thank you @Robert Cemper for the quick response,
I was hoping to avoid using the %RCMP Routine as it uses GOTO commands.
One of our Senior Developers pointed me in the right direction. He recommended I use:
Class %Library.Routine
query Compare(Nsp1 As %String, RouName1 As %String, Nsp2 As %String, RouName2 As %String)
Selects LineNo1 As %String, Line1 As %String, LineNo2 As %String, Line2 As %String
This query provides a list of all lines that differ between two given routines.
Nsp1 and RouName1 specify the first routine.
Nsp2 and RouName2 specify the second routine.
Nsp1 and RouName2 can be either an explicit or an implied namespace.
I will try this and see if it works. If anyone has further advice feel free to add them.
just checked %Library.Routine
- a sample of many %R* routines
- lots of @ indirections
- DO with . (dot) syntax
- and of course GOTO.
Just to qualify the code inside.
You're right, i should have looked into that aswell.
I got it to work with the Query now, since the %RCMP file was locked for me.
Set statement=##class(%SQL.Statement).%New()
Set status=statement.%PrepareClassQuery("%Library.Routine","Compare")
N result
SET result=##class(%Library.ProcedureContext).%New()
SET result = statement.%Execute(NSP1,ROUTINE1,NSP2,ROUTINE2)
DO result.DumpResults()
>>>>>> "since the %RCMP file was locked"
USER>ZLOAD %RCMP PRINTI've seen solutions with files, and one way to do it would be to write the routines to files, and compare those, but I'd like to avoid that if possible.
I would highly recommend you use a file approach, i.e. git. Write routines to files and compare them using git diff tools.
One of the best tools that I have found for comparing routines is called "Beyond Compare". You can create a small CSP that will serve up the a routine depending on the routine name in the query string of the URL. Then you can put that URL directly in the file open dialog of "Beyond Compare" that it will bring up the routines and display a nice side-by-side compare. It can also save the comparison in an HTML file for sharing with others.
See Studio Compare
With an external program WinMerge also works fine. You can compare any elements (mac, cls, css, js, css, etc.) located both inside the database and in external files.
This is really helpful Vitaliy Serdtsev. Thanks for this information. I set this up with Beyond Compare and it works great. Do you know of anyway to get it to work across namespaces? It would be nice to be able to compare a routine from one namespace to another.
| Compare |
Compares an open file to one that you select with Browse. You must have specified an external compare tool with the Compare setting in Options > Environment > General. To work correctly, the compare tool must be able to accept command line parameters as tool.exe file1 file2. Tested compare tools are Microsoft Windiff and Perforce p4Diff.exe. |
A good question, to which I do not know a beautiful answer. The option with Extended References, unfortunately did not work.
In general, through uploading to a file. If the elements have different names in the databases, then you can use the Mapping a Routine/Package.
This Compare only works when working with the studio, correct?
I was thinking of something for people who don't have direct access to the studio and maybe just want to check out differences made, like a supervisor.
As I said, I managed to implement the query from my answer above, but the code is very old. Just curious if there is a newer version.
This Compare only works when working with the studio, correct?Yes, Studio does the following:
- uploads the first source code to a temporary directory (for example c:\windows\temp)
- uploads the second source code to a temporary directory
- starts WinMerge by passing in the command line links to previously uploaded files: WinMerge Command line
You can do all this yourself from a batch file. In this case, you can compare items not only in different databases, but also on different servers.
Beyond Compare works outside of Studio and can compare files, folders, and it even does an Ok compare of Word files. I have used it to track project changes as a manager to keep track of the changes the developers were making. It worked great and we were able to catch some issues early in the project so that they could be fixed quickly.
Most of the above is true for WinMerge, with one exception - WinMerge is free. WinMerge has plugins for comparing images, PDF, Word, Visio, Excel, PowerPoint, etc.
For us all, the common denominator is Cache/IRIS and we have, as you know, Cache/IRIS for Win, Linux, AIX and MAC platforms. It's nice to know about existing external tools, but for some of us the COS solutions remains as the last resort, especially if those (external) tools do not exists for the OS, one works on (Just My2Cents).
My choice of a comparison tool was determined by the OS on which Studio is running. But you are free to choose something more platform-independent, such as Java or Qt.
By the way, SQL Data Lens (written in Java) already has the ability to view and upload sources to a file. It would be nice to add integration with a third-party tool for comparing items (by analogy with Studio).
I hope @Andreas Schneider will read this wish.
@Vitaliy Serdtsev thanks for this great idea! In the past i have also missed this feature myself from time to time. So far the pain has not been great enough ;-)
But now I know that other users also miss this feature...
Agree, Beyond Compare is a great diff tool. We have interfaced it to Deltanji source control - so it goes direct to code in InterSystems namespaces, rather than exporting the code into files and then diffing the files.
The solo edition (free to use) is available from our website if you're using Studio. Or you can download it as a VS Code extension by installing Serenji.
I'm a fan of beyond compare but it is not always available on some of the sites I log in to. I'm not a fan of RCMP. I've got a Diff program that works in a terminal session that I use when I log on to a Cache version 5 site. Feel free to take this and do what you want with it (.MAC routines can be found on ^rMAC):
Code
ZRDIFF(Master="",Minor="",N1="",N2="",Show=1,Debug=0,Diffs=0,LengthWidth="40;160",UDL=0) ; Poor man's side by side diff
; Knocked up to run a quick diff on an old Caché version 5 system where my nice diff program won't work
; Usage:
; Pass in 1st routine (arg1 is Master) to compare against itself (in another namespace) or against 2nd routine (arg2 is Minor)
; N1 and N2 are optional namespaces
; Alternatively, pass in a pair of arrays to compare in Master and Minor but ensure top node is populated with something.
; To compare classes use ##class(%Compiler.UDL.TextServices).GetTextAsArray(,ClassName,.Array) and do your own thing.
; Call it like $$^ZRDIFF(Routine1,Routine2,Namespace1,Namespace2,0,0,.Diffs) to get it to return a non-zero number indicating there's a difference
; Make Routine1 and Routine2 variables and pass by reference to get back the 2 routines as arrays
; The lines of code that are the same will be lined up
; Show=1 to display to terminal
; ScreenLength and width can be passed in if your terminal doesn't go to 160 columns.
; Pass in "24;80" if you like, it will work.
; Pass in "24;9999" if you are going to use the arrays and pass them to something else that looks after formatting
;
I Master="" Q:$Q "99No routine supplied" Q
I Minor="" M Minor=Master
;
; To compare across namespaces populate N1 and N2 with a valid namespace
; default is current namespace
I N1="" S N1=$ZNSPACE
I N2="" S N2=$ZNSPACE
;
; Written to run on a putty terminal so screen handling is a bit noddy
; Don't like the screen handling? You change it.
; Look for InverseOn and InverseOff as they are the variables used for highlighting differences
; THIS IS FOR A VT100 EMULATION
I Show W $C(27)_"[8;"_LengthWidth_"t" ; screen length(40) and width(160)
N HalfWidth,I,L,Left,R,Right,MASTER,MINOR,MI,RA,RB,R1,R2
S HalfWidth=$P(LengthWidth,";",2)\2
;
; Plan:
; Get 2 routines and normalise each line of code into 2 arrays so that
; all tab characters converted to spaces
; start of line is spaced out to 9 spaces (allows an 8 char tag), longer tags left alone
; Decide which of the two routines has more lines (left or right)
; Use the larger routine as the master routine
; Find all the lines of code in the minor routine that are identical to master and align them to the master
; but don't change the order of the lines, expand the arrays instead so that the similar lines match up
; Then lines broken into 78 character chunks so that the compare can be done side by side on a 160 wide screen
;
S RA=Master,RB=Minor
M R1=Master,R2=Minor
; Obvsiously, not doing this if you passed in a pair of arrays
I $D(R1)<10 K R1 M R1=^[N1]ROUTINE(RA,0)
I $D(R2)<19 K R2 M R2=^[N2]ROUTINE(RB,0)
I '$G(R1(0)) S R1(0)=$O(R1(""),-1) ; number of lines in routine
I '$G(R2(0)) S R2(0)=$O(R2(""),-1)
;
; UDL means you passed in a class or two in UDL format so don't bother normalising it's already done
; If they are not, then don't pass UDL=1 !
I 'UDL {
D Normalise(.R1)
D Normalise(.R2)
}
I R2(0)>R1(0) {
S MASTER="R2",MINOR="R1"
D Compare(.R2,.R1)
}
Else {
S MASTER="R1",MINOR="R2"
D Compare(.R1,.R2)
}
; Getting the odd occurence where a pair of lines shows up next to each other claiming to be different when they are not
; Happens when ^ROUTINE has a missing line in the middle!
; so align them again
S I=0
F {
S I=$O(Master(I)) q:'I
I Master(I)'="",$G(Minor(I))="" {
; they are the same
I $G(Master(I-1))="",$G(Minor(I-1))=Master(I) S Minor(I)=Minor(I-1) K Master(I-1),Minor(I-1) continue
I $G(Master(I+1))="",$G(Minor(I+1))=Master(I) S Minor(I)=Minor(I+1) K Master(I+1),Minor(I+1) continue
}
}
I Show {
S MI=0
W #!,$S(MASTER="R1":$$LJ($$LJ(N1,15)_RA,HalfWidth)_$$LJ(N2,15)_RB,1:$$LJ($$LJ(N2,15)_RB,HalfWidth)_$$LJ(N1,15)_RA)
F {
S MI=$O(Master(MI)) Q:'MI
S Left=$S(MASTER="R1":Master(MI),1:Minor(MI))
S Right=$S(MASTER="R2":Master(MI),1:Minor(MI))
I Left'=Right,$I(Diffs) S Diffs(MI)="" ; lines where something is different
; Convert routine into lines less than 80 characters long
F {
S L=$E(Left,1,HalfWidth-2)
S Left=$E(Left,HalfWidth-1,9999)
S R=$E(Right,1,HalfWidth-2)
S Right=$E(Right,HalfWidth-1,9999)
D Show(L,R)
q:Left_Right=""
}
}
}
; Just in case you passed in arrays by reference and are expecting the left and right hand routines to come back to you...
K Left,Right
I MASTER="R1" {
; don't need to do anything - already the right way round
}
Else {
M Left=Master,Right=Minor
K Master,Minor
M Master=Left,Minor=Right
}
S Master=RA,Minor=RB
q:$Q Diffs q
;
Normalise(ROU) N i,I,Line,N
S i=0
F I=1:1:ROU(0) {
S Line=$G(ROU(I)) ; need the $G because some old systems have missing lines of code! Don't ask me.
; swap tabs for spaces
S Line=$TR(Line,$c(9)," ")
; if first character is a " " make it 9 spaces
I $E(Line)=" " S $E(Line)=" "
; or if it's a tag with no arguments, pad it out to 9 spaces
I $P(Line," ")?1(1"%",1AN).AN {
S $P(Line," ")=$$LJ($P(Line," "),9)
}
// or if it has arguments and is (reasonable guess) less than 8 characters, pad out to 9 spaces, could fail if default values for arguments include spaces or braces
ElseIf $L($P(Line,")"))<8,$P(Line," ")?1(1"%",1AN).AN1"(".e1") " {
S $P(Line," ")=$$LJ($P(Line," "),9)
} // leave everything else alone
;
S N($i(i))=Line
}
; Now the noralised routine is in N() move it to R1() or R2()
S N(0)=ROU(0)
K ROU M ROU=N
Q
R1LinesUpLater(I,i) ; parameter passed by value so will return to orignal value on quit
N OK
S OK=0
F {
S I=$O(ROU1(I)) q:'I
F {
S i=$o(ROU2(i)) Q:'i ; NON-NUMERIC CATCHES THINGS LIKE "LANG"
; 2 consecutive lines the same will have to do
I ROU1(I)=ROU2(i) {
I ($G(ROU1(I+1))=$G(ROU2(i+1))) S OK=1 Q
}
}
Q:OK!'i
}
Q OK
R2LinesUpLater(I,i) ; parameter passed by value so will return to orignal value on quit
N OK
S OK=0
F {
S i=$O(ROU2(i)) q:'i
F {
S I=$o(ROU1(I)) Q:'I ; NON-NUMERIC CATCHES THINGS LIKE "LANG"
; 2 consecutive lines the same will have to do
I ROU1(I)=ROU2(i) {
I ($G(ROU1(I+1))=$G(ROU2(i+1))) S OK=1 Q
}
}
Q:OK!'I
}
Q OK
; left justify
LJ(x,n) q x_$j("",n-$l(x))
;
Part(x,ION,IOFF) w:x'="" ION w x w:x'="" IOFF q ""
Show(Left,Right) ; show the current row and also highlight when different
N InverseOn,InverseOff
S InverseOn=$s(Left=Right:"",1:$c(27,91)_"7m")
S InverseOff=$s(Left=Right:"",1:$c(27,91)_"27m")
W !
W $$Part(Left,InverseOn,InverseOff)
W ?HalfWidth
W $$Part(Right,InverseOn,InverseOff)
q
;
Compare(ROU1,ROU2) N I,i
S (I,i,MI)=0
F {
S I=$O(ROU1(I),1,L) Q:'I ; NON-NUMERIC CATCHES THINGS LIKE "LANG"
F {
S i=$o(ROU2(i),1,R) Q:'i ; NON-NUMERIC CATCHES THINGS LIKE "LANG"
; 2 consecutive lines the same will have to do
I ROU1(I)=ROU2(i) {
; this works for the first line and last line
I ($G(ROU1(I+1))=$G(ROU2(i+1))) || (I=i) || $$R1LinesUpLater(I,i+1) || $$R2LinesUpLater(I+1,i) {
; move to a new array and make them line up
S MI=MI+1
S Master(MI)=ROU1(I),Minor(MI)=ROU2(i)
D:$g(Debug) Show(Master(MI),Minor(MI))
Q ; go get next ROU1(I)
}
; still here?
; Then 2 lines the same but next lines are not and ROU1(I+1) doesn't have a matching line later in ROU2(i+1 onwards)
; Move ROU1(I+1) to Master and increment I
S MI=MI+1
S Master(MI)=ROU1(I),Minor(MI)=ROU2(i)
D:$g(Debug) Show(Master(MI),Minor(MI))
q ;S MI=MI+1,I=I+1,i=i-1 ; was nearly working but Quit works better
I I'>ROU1(0) {
S Master(MI)=ROU1(I),Minor(MI)=""
D:$g(Debug) Show(Master(MI),Minor(MI))
}
Q
}
; got here then they are different
; if they line up later then show the line from ROU2 now
I $$R1LinesUpLater(I,i+1) {
S MI=MI+1
S Master(MI)="",Minor(MI)=ROU2(i)
D:$g(Debug) Show(Master(MI),Minor(MI))
Q ; go get next ROU1(I)
}
; if they line up later then show the line from ROU1 now
I $$R2LinesUpLater(I,i-1) {
S MI=MI+1,i=i-1
S Master(MI)=ROU1(I),Minor(MI)=""
D:$g(Debug) Show(Master(MI),Minor(MI))
Q ; go get next ROU1(I)
}
; Still here?
; Then 2 different lines, show them both
S MI=MI+1
S Master(MI)=ROU1(I),Minor(MI)=""
D:$g(Debug) Show(Master(MI),Minor(MI))
S MI=MI+1
S Master(MI)="",Minor(MI)=ROU2(i)
D:$g(Debug) Show(Master(MI),Minor(MI))
Q
}
}
q
;
TEST k S r1="GT101",r2="GT101"
D ZRDIFF(.r1,.r2,"APEX","TAPEX",1,0,.Diffs) Q