/**
This module evaluates to the an object which contains
utility functions for calculating differences between
checkin versions.
*/
(proc(){
/**
Internal impl of DeckDiff.calcCheckinChangeInfo().
See that function (below) for the main docs and the
structure of the returned object.
deck1 and deck2 must be Manifest objects loaded
with Fossil.Context.loadManifest(..., true) (so that
each Manifest contains its whole file list, even if it's
a delta manifest).
*/
const calcDeckDiffs = proc(deck1, deck2){
var isManifest = proc(v){
return v && (v.uuid && v.rid && v.type)
}
assert isManifest(deck1)
assert isManifest(deck2)
unset isManifest
const rc = object{
v1: deck1.uuid,
v2: deck2.uuid,
changes:array[]
}
const f1 = deck1.F ? deck1.F : array[];
const f2 = deck2.F ? deck2.F : array[];
var f1len = f1.length()
var f2len = f2.length();
const n1 = api.Hash( f1len * 2 )
const n2 = api.Hash( f2len * 2 )
/**
The basic strategy:
- put all names from f1 into hashtable n1.
- iterate over f2 and calculate changes based on existence
of f2.(N).name's existence in n1.
*/
f1.eachIndex(proc(v,i){ n1.insert(v.name, v) }.importSymbols('n1'))
$f2.eachIndex proc(fc2,i){
n2.insert(fc2.name, fc2)
var fc1 = n1.search(fc2.name)
if(!fc1){
fc2.priorName && (fc1 = n1.search(fc2.priorName))
var row = object{
name: fc2.name,
uuid: fc2.uuid,
perm: fc2.perm
}
if(fc1){
n1.remove(fc2.priorName)
row.status = 'renamed'
row.priorName = fc1.name
row.priorContent = fc1.uuid
}else{
row.status = 'added'
}
//row.sortProperties()
ch.push(row)
return
}
n1.remove(fc2.name)
(fc1.uuid === fc2.uuid) && return
const row = object{
name: fc2.name,
uuid: fc2.uuid,
perm: fc2.perm,
priorContent: fc1.uuid
}
ch.push(row)
if( !fc2.uuid ){
row.status = 'removed'
const p1 = n1.search(fc1.name)
assert p1
row.priorContent = p1.uuid
if(p1.name !== fc1.name){
throw {i do not think this is actually possible}
fc1.priorName = p1.name
}
}else if(fc2.priorName){
row.status = 'renamed'
row.priorName = fc2.priorName
//const p1 = n1.search(fc2.priorName)
//assert p1
}else{
row.status = 'modified'
}
//row.sortProperties()
}.importSymbols('n1', 'n2', ((const ch = rc.changes), 'ch'))
// Pick up any entries in the parent but not the child...
$n1.eachEntry proc(name,fc1){
var fc2 = n2.search(fc1.name)
if(!fc2){
const row = object{
name: fc1.name,
priorContent: fc1.uuid,
status: 'removed'
}
ch.push(row)
return
}
}.importSymbols('n2', ((const ch = rc.changes), 'ch'))
rc.changes.sort(proc(l,r){
return l.name.compare(r.name)
})
return rc
}
const uuidStr = proc(u){ return u.substr(0,12) }
const dlLink = proc(fn, uuid){
const CGI = api.cgi
const url = CGI.cgiRoot.concat(
'download/uuid/', uuid,
'?name=', CGI.urlencode(fn))
return CGI.html.createAnchor(url, uuidStr(uuid))
}.importSymbols('uuidStr')
const DeckDiff = object{
__typename: 'DeckDiff',
out: api.io.output,
/**
Calculates file-level changes between two versions of
a checkin.
Requires f to be a Fossil.Context instance and v1/v2
to be checkin versions to calculate file diffs for. v1 is
the "older" version and v2 the newer.
v1 and v2 may be symbolic names, RIDs (integers), or
manifests loaded via Fossil.Context.loadManifest(). If
they are Manifest objects and are delta manifests, they
_must_ have been loaded with their baseline files
included in their own F-card list (the 2nd parameter to
Fossil.Context.loadManifest() must be true) or else the
diffs will probably be somewhat unexpected! Proper
traversal of files between a delta manifest and its
baseline is intricate, and this routine does not do it -
it assumes that any decks passed to it have their F-card
list populated as if they were not delta manifests.
If v1 is provided and v2 is not then the diff is calculated
based on (v2,v1), where v2 is the parent checkin of v2.
Example: to get a diff of the current and previous versions,
pass (f,'current') to this function. Note that the special
'current' and 'prev' symbolic names rely on an opened
checkout.
Returns an Object with this structure:
{
v1: "full UUID of v1",
v2: "full UUID of v2",
changes: array[
{
name: "filename",
uuid: "UUID of v2 file content.",
priorContent: "UUID of v1 file content", // if any
priorName: "oldName", // only if renamed
status: "added|renamed|modified|removed",
perm: "x|l|f" // eXecuable, symLink, or File
},
...one for each change...
]
}
The changes array holds the list of files (sorted
alphabetically by file name)
changed/added/removed/renamed between the two versions.
If a file is both renamed a modified, its status will be
"renamed" and its priorContent will hold the UUID of its
old content. A renamed file without a priorContent UUID
(or where priorContent==uuid) was renamed without its
contents being modified in the same checkin.
*/
calcCheckinChangeInfo: proc(f, v1, v2){
assert f inherits Fossil.Context
var d1, d2
if('object'===typename v1){
d1 = v1
}else if(v1){
d1 = f.loadManifest(v1, true, false)
}else{
throw {Expecting v1 argument to be a version value or manifest object.}
}
if(!v2 && d1.P && d1.P.0){
v2 = d1
d1 = f.loadManifest(d1.P.0, true, false)
}
if('object'===typename v2){
d2 = v2
}else if(v2){
d2 = f.loadManifest(v2, true, false)
}else {
throw {Expecting v2 argument to be a version value or manifest object.}
}
return (d2 && d2) ? calcDeckDiffs(d1, d2) : undefined
}.importSymbols('calcDeckDiffs'),
/**
Parameters:
f: a Fossil.Context instance
changes: the result of calling this.calcCheckinChangeInfo()
Requires api.cgi.
diffOpt: If diffOpt is-a Object then it may contain
properties to control the output of the diff. If it is a
non-object value then defaults are used. Any properties
not specified in diffOpt get defaults, mostly taken from
CGI-provided variables:
{
// Number of lines of context around changes:
contextLines: +api.cgi.getVar('contextLines', 5),
// Disables HTML output, generating a plain text diff
text: +api.cgi.getVar('text', 0),
// Specifies the column width for TEXT-mode side-by-side diffs.
// Has no effect in HTML mode!
sbsWidth: +api.cgi.getVar('sbsWidth', 80),
// Swaps the left/right sides of the diffs (but not the labels!)
invert: +api.cgi.getVar('invert'),
// Disables side-by-side (sbs) mode
inline: +api.cgi.getVar('inline', 0)
}
*/
showFileDiffsFromChangeInfo: proc(f,changes, diffOpt){
assert f inherits Fossil.Context
assert 'object' === typename changes
const cha = changes.changes
assert 'array' === typename cha
if(0){
$out "Raw change data:<br/>"
$out {<textarea rows="30" cols="80" readonly>}
CGI.printJSON(changes)
$out {</textarea>}
}
cha.length() || return
const out = this.out
const CGI = api.cgi
const defaultDiffOpt = object {
html: 1,
text: +CGI.getVar('text', 0),
sbsWidth: +CGI.getVar('sbsWidth', 80), // has no effect in HTML mode!
contextLines: +CGI.getVar('contextLines', 7),
invert: +CGI.getVar('invert'),
inline: +CGI.getVar('inline', 0)
}
diffOpt = Fossil.extendProperties(
defaultDiffOpt,
('object'===typename diffOpt) ? diffOpt : object{}
)
diffOpt.text && unset diffOpt.html
diffOpt.inline && unset diffOpt.sbsWidth
//diffOpt.sortProperties(); print(diffOpt)
$cha.eachIndex proc(v,i){
$out {<div>}
$out {<h2>}
if(v.priorContent){
$out {[<span class='uuid'>} dlLink(v.priorName || v.name, v.priorContent) {</span>]} ' → '
}
if(v.uuid){
$out ' ' {[<span class='uuid'>} dlLink(v.name, v.uuid) {</span>]}
}
$out ' ' {<span class='filename strong'>} v.name {</span>}
$out {</h2>}
if(v.priorName){
$out '<div>Renamed from: ' {<span class='filename'>} v.priorName {</span></div>}
}
//$out ' ' {<span class='filename strong'>} v.name {</span>}
if(!v.uuid){
$out "Removed."
}else{
if(v.priorContent){
$out {<div class='artifact-diff'>}
const diff = catch {
toss f.artifactDiff(v.priorContent, v.uuid, diffOpt)
}
if(diff inherits api.Exception){
$out "Error generating diff. "
if(Fossil.rc.TYPE === diff.code){
$out "Perhaps it's binary content."
}else{
$out CGI.toJSONString(CGI.scrubException(diff))
}
}else{
if(diffOpt.html){
$out diff
}else{
$out CGI.htmlEscape(diff)
}
}
$out {</div>}
}else{
$out "New file. Content: <span class='uuid'>" dlLink(v.name, v.uuid) "</span>"
}
}
$out {</div>}
}/*showFileDiffsFromChangeInfo()*/.importSymbols('f', 'CGI', 'diffOpt', 'out', 'dlLink', 'uuidStr')
}.importSymbols('dlLink', 'uuidStr'),
/**
Lists an overview of changes made between a checkin and
its parent. Requires api.cgi.
Params:
f: a Fossil.Context instance from which...
deck: a Manifest "deck" loaded via f.loadManifest() OR
a symbolic name/UUID of a checkin version.
diffOpt: if a falsy value, no file-level diffs are shown. If
a truthy value, see showFileDiffsFromChangeInfo() for
the details.
*/
showDeckFileChangeList: proc(f, deck, diffOpt = false){
assert f inherits Fossil.Context
(deck.P && deck.P.0) || return
const out = this.out
const CGI = api.cgi
if(deck.B){
deck = deck.uuid /* have to re-load to ensure that all (incl. inherited) F-cards are loaded */
}
const changes = this.calcCheckinChangeInfo(F, deck)
(changes.changes && changes.changes.length()) || return
const statMap = object {
added: '+',
modified: '~',
removed: '-'
renamed: '→'
}
$out {<div>File changes: <span class='small-text'>(legend:}
statMap.sortProperties()
var counter = 0
$statMap.eachProperty proc(k,v){
counter && out(',')
out(' ',v,' = ',k)
counter += 1
}.importSymbols('out')
$out {)</span></div>}
$out {<div class='manifest-file-change-list'>}
Fossil.sorter(changes.changes, true)
$changes.changes.eachIndex proc(v){
$out {<div>}
var pre = statMap.(v.status)
pre && $out {<span class='monospace'>} pre {</span>} ' '
unset pre
if(v.uuid){
$out {[<span class='uuid'>}
const url = CGI.cgiRoot.concat(
'download/uuid/', v.uuid,
'?name=', CGI.urlencode(v.name))
$out CGI.html.createAnchor(url, uuidStr(v.uuid))
$out {</span>]}
}
$out ' ' {<span class='filename'}
$out " title='" v.uuid "'>"
if('renamed'===v.status){
assert v.priorName
$out v.priorName ' → '
}
$out v.name
$out {</span>}
$out {</div>}
}.importSymbols('statMap', 'out', 'CGI', 'dlLink', 'uuidStr')
$out {</div><!-- .manifest-file-change-list -->}
diffOpt && this.showFileDiffsFromChangeInfo(f, changes, diffOpt)
//showFileDiffs(changes)
return
/*$out "Raw change data:<br/>"
$out {<textarea rows="30" cols="80" readonly>}
CGI.printJSON(changes)
$out {</textarea>}*/
}.importSymbols('dlLink', 'uuidStr')/*showDeckFileChangeList()*/
}/*DeckDiff*/
0 && (!api.cgi) && scope { /* test code */
api.import('fossil-extend')
const F = Fossil.createContext()
//print(typename dd)
var v1, v2
//v1 = 'd73e9a278e' v2 = '9b886baf47'
//v1 = '684229ae618e' v2 = '22d1ad1a3b6d'
//v1 = 'f0620d926e4b' v2 = '68fbc6aeb3fb'
//v1 = '64c47b71d0c11ffef' v2 = '9b886baf47a29e'
//v1 = '8d1ab6166d' v2 = '1f304f5f35'
v1 = '5fbaf6df0a4a73e56' v2 = 'de941e7bf373cfab1'
//v1 = 'prev' v2 = 'current'
const x = DeckDiff.calcCheckinChangeInfo(F, v1, v2)
//d73e9a278ee4ff2f439b303c75b55c64a42f076b
//9b886baf47a29e0336e16d39d05731f6bcc0bb29
//Fossil.sorter(x, true)
print("%1$-1J".applyFormat(x))
}
return DeckDiff
})()