A lot of cleanup and small style improvements, currently improving the prose for commits.

This commit is contained in:
Suzanne Soy 2021-06-17 20:10:54 +01:00
parent 94649bd5ed
commit 39fb386e5a
3 changed files with 658 additions and 295 deletions

View File

@ -1,29 +1,44 @@
body { font-size: 1.2rem; text-align:justify; } body { font-size: 1.2rem; text-align:justify; }
article#git-tutorial { max-width: 63rem; position: absolute; right:18.4em; top:0; left:0.5em; transition: right, 0.2s; } article#git-tutorial { position: absolute; top:0; left:0.5em; transition: right, 0.2s; }
#git-tutorial #toc { position: fixed; top: 0; bottom: 0; right:0; width: 17.4em; text-align: left; overflow: scroll; #git-tutorial #toc { position: fixed; top: 0; bottom: 0; width: 17.4em; text-align: left; overflow: scroll;
background: white; border: 1px solid gray; transition: border-width 0.2s, right 0.2s; z-index: 1000; } background: white; border-left: 1px solid gray; transition: border-width 0.2s, right 0.2s; z-index: 1000; }
#git-tutorial #toc:hover { right: 0; transition: border-width 0.4s, right 0.4s; z-index: 3000; } #git-tutorial #toc:hover { right: 0; transition: border-width 0.4s, right 0.4s; z-index: 3000; }
#git-tutorial #lines { position: absolute; z-index: 2000; } #git-tutorial .lines { position: absolute; z-index: 2000; }
#git-tutorial textarea, #git-tutorial .CodeMirror { width: 100%; font-size: 1.2rem; border: thin solid black; } #git-tutorial textarea, #git-tutorial .CodeMirror { width: 100%; font-size: 1.2rem; border: thin solid black; }
#git-tutorial table { table-layout: fixed; width: 100%; font-size: 100%; font-family: monospace; min-width: 41em; } #git-tutorial table { table-layout: fixed; width: 100%; font-size: 100%; font-family: monospace; min-width: 41em; }
#git-tutorial td.cell-contents, #git-tutorial th.cell-contents { font-family: monospace; }
article#git-tutorial p, article#git-tutorial h1 { max-width: 63rem; }
/* td.cell-path { } */ #git-tutorial td, #git-tutorial th { padding-left: 0.3em; padding-right: 0.3em; }
#git-tutorial td.cell-contents { font-family: monospace; width: 36em; } #git-tutorial td.cell-contents, #git-tutorial th.cell-contents { width: 36em; }
article#git-tutorial { left: calc(50% - ( 17.4em / 2 ) - ( 63rem / 2 ) ); right:18.4em; max-width: 63rem; }
article#git-tutorial table { width: 77rem; margin-left: calc( ( ( 63rem - 77rem ) / 2 ) ); }
#git-tutorial #toc { right:0; }
#git-tutorial #toc:hover { border-left: 1px solid gray; }
#git-tutorial td { padding-left: 0.3em; padding-right: 0.3em; } @media (max-width: 100em) {
#git-tutorial td, #git-tutorial th { padding-left: 0.3em; padding-right: 0.3em; }
#git-tutorial td.cell-contents, #git-tutorial th.cell-contents { width: 36em; }
article#git-tutorial { left:0.5em; right:18.4em; max-width: 63rem; }
article#git-tutorial table { width: 100%; margin-left: auto; }
#git-tutorial #toc { right:0; }
#git-tutorial #toc:hover { border-left: 1px solid gray; }
}
@media (max-width: 72em) { @media (max-width: 72em) {
#git-tutorial td { padding-left: 0; padding-right: 0; } #git-tutorial td, #git-tutorial th { padding-left: 0; padding-right: 0; }
#git-tutorial td.cell-contents { width: 34em; } #git-tutorial td.cell-contents, #git-tutorial th.cell-contents { width: 34em; }
article#git-tutorial { right: 7em; } article#git-tutorial { left:0.5em; right: 7em; max-width: 63rem; }
article#git-tutorial table { width: 100%; margin-left: auto; }
#git-tutorial #toc { right: -11em; } #git-tutorial #toc { right: -11em; }
#git-tutorial #toc:hover { border-left: 5px solid gray; } #git-tutorial #toc:hover { border-left: 5px solid gray; }
} }
@media (max-width: 63em) { @media (max-width: 63em) {
#git-tutorial td { padding-left: 0; padding-right: 0; } #git-tutorial td, #git-tutorial th { padding-left: 0; padding-right: 0; }
#git-tutorial td.cell-contents { width: 30em; } #git-tutorial td.cell-contents, #git-tutorial th.cell-contents { width: 30em; }
article#git-tutorial { right:6em; } article#git-tutorial { left:0.5em; right:6em; max-width: 63rem; }
article#git-tutorial table { width: 100%; margin-left: auto; }
#git-tutorial #toc { right: -12em; } #git-tutorial #toc { right: -12em; }
#git-tutorial #toc:hover { border-left: 5px solid gray; } #git-tutorial #toc:hover { border-left: 5px solid gray; }
} }
@ -31,7 +46,11 @@ article#git-tutorial { max-width: 63rem; position: absolute; right:18.4em; top:0
#git-tutorial textarea { display:block; height: 18rem; } #git-tutorial textarea { display:block; height: 18rem; }
#git-tutorial .CodeMirror { height: max-content; } #git-tutorial .CodeMirror { height: max-content; }
#git-tutorial input { display: inline-block; margin-right: 1em; font-size: 1.2rem; } #git-tutorial input { display: inline-block; margin-right: 1em; font-size: 1.2rem; }
#git-tutorial table, #git-tutorial td, #git-tutorial th { border:thin solid black; border-collapse: collapse; } #git-tutorial table, #git-tutorial th { border:thin solid black; border-collapse: collapse; }
#git-tutorial td { opacity: 0.5; border-top:thin solid #aaa; border-left:thin solid #aaa; border-right:thin solid #aaa; border-collapse: collapse; }
#git-tutorial tr:last-child td { border:thin solid #aaa; }
#git-tutorial tr:hover td { opacity: 1; border:thin solid black; }
#git-tutorial tr:last-child.different td, #git-tutorial .different td { border: thin solid black; opacity: 1; background: #f0f6f8; }
#git-tutorial .specialchar { color: red; word-wrap: normal; } #git-tutorial .specialchar { color: red; word-wrap: normal; }
#git-tutorial .hex-prefix { color: lightgrey; } #git-tutorial .hex-prefix { color: lightgrey; }
#git-tutorial .hex { color: brown; } #git-tutorial .hex { color: brown; }
@ -59,9 +78,15 @@ article#git-tutorial .onlytoc { display: none; }
#git-tutorial h1:hover + .permalink, #git-tutorial .permalink:hover { opacity: 1; } #git-tutorial h1:hover + .permalink, #git-tutorial .permalink:hover { opacity: 1; }
#git-tutorial #toc ul { list-style-type: none; padding: 0 !important; /*list-style-type: disc;*/ } #git-tutorial #toc ul { list-style-type: none; padding: 0 !important; /*list-style-type: disc;*/ }
#git-tutorial #toc a { color: #666; } #git-tutorial #toc a { color: #666; }
#git-tutorial #toc .function { color: #00f; }
#git-tutorial #toc .assignment { color: #00f; }
#git-tutorial #toc a:hover { color: #333; } #git-tutorial #toc a:hover { color: #333; }
#git-tutorial .CodeMirror .scrolled-to-line { background: lightcyan; } #git-tutorial .CodeMirror .scrolled-to-line { background: lightcyan; }
#git-tutorial #toc > ol { padding-left: 0.7em; } #git-tutorial #toc > ol { padding-left: 0.7em; }
#git-tutorial #toc ol > li > a { text-decoration: none; } #git-tutorial #toc ol > li > a { text-decoration: none; }
#git-tutorial #toc li { padding-top: 0.4em; } #git-tutorial #toc li { padding-top: 0.4em; }
#git-tutorial #toc ol, #git-tutorial #toc ul { padding-left: 2.3em; } #git-tutorial #toc ol, #git-tutorial #toc ul { padding-left: 2.3em; }
/* Highlight elements when a click on e.g. a hash scrolls to the destination */
#git-tutorial .scroll-destination-hilite, #git-tutorial .scroll-destination-hilite td { transition: background 0.5s linear 0.5s, opacity 0.4s linear 0.5s; background: #ffd3d3 !important; opacity: 1 !important; }
#git-tutorial .scroll-destination-lolite, #git-tutorial .scroll-destination-lolite td { transition: background linear 0.5s, opacity linear 0.5s; }

View File

@ -22,6 +22,15 @@ function ___to_hex(s) {
return hex; return hex;
} }
function ___hex_to_bin(hex) {
var hex = String(hex);
var str = ""
for (var i = 0; i < hex.length; i+=2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return str;
}
// These three functions are accessible in the user scripts. // These three functions are accessible in the user scripts.
sha1 = function(s) { return Sha1.hash(___to_hex(s), { msgFormat: 'hex-bytes', outFormat: 'hex' }); }; sha1 = function(s) { return Sha1.hash(___to_hex(s), { msgFormat: 'hex-bytes', outFormat: 'hex' }); };
deflate = function(s) { return ___uint8ArrayToString(pako.deflate(___stringToUint8Array(s))); } deflate = function(s) { return ___uint8ArrayToString(pako.deflate(___stringToUint8Array(s))); }
@ -75,7 +84,7 @@ function ___getOffset(elt) {
return { left: 0, top: 0 }; return { left: 0, top: 0 };
} }
} }
var global_current_hilite = { src: false, dests: [] }; var global_current_hilite = { src: false, dests: [], srcid: false, destclass: false, lines: false };
function ___hilite_off() { function ___hilite_off() {
if (global_current_hilite.src) { if (global_current_hilite.src) {
global_current_hilite.src.classList.remove('hilite-src'); global_current_hilite.src.classList.remove('hilite-src');
@ -83,20 +92,57 @@ function ___hilite_off() {
for (var d = 0; d < global_current_hilite.dests.length; d++) { for (var d = 0; d < global_current_hilite.dests.length; d++) {
global_current_hilite.dests[d].classList.remove('hilite-dest'); global_current_hilite.dests[d].classList.remove('hilite-dest');
} }
global_current_hilite = { src: false, dests: [] }; if (global_current_hilite.lines) {
document.getElementById('lines').innerHTML = ''; global_current_hilite.lines.innerHTML = '';
}
global_current_hilite = { src: false, dests: [], srcid: false, destclass: false, lines: false };
} }
function ___hilite(src, dest) { function ___scroll_to_dest(srcid, destclass) {
___hilite_off(); var src = document.getElementById(srcid);
var src = document.getElementById(src);
var wrapper = src; var wrapper = src;
while (wrapper && !wrapper.classList.contains('hilite-wrapper')) { wrapper = wrapper.parentElement; } while (wrapper && !wrapper.classList.contains('hilite-wrapper')) { wrapper = wrapper.parentElement; }
var dests = (wrapper || document).getElementsByClassName(dest); var dests = (wrapper || document).getElementsByClassName(destclass);
if (dests.length > 0) {
dest = dests[dests.length - 1];
while (dest && dest.tagName.toLowerCase() != 'tr') { dest = dest.parentElement; }
if (dest) {
dest.scrollIntoView({ behavior: 'smooth', block: 'center' });
dest.classList.add('scroll-destination-hilite');
window.setTimeout(function() {
dest.classList.add('scroll-destination-lolite');
dest.classList.remove('scroll-destination-hilite');
window.setTimeout(function() {
dest.classList.remove('scroll-destination-lolite');
}, 600);
}, 1100);
}
}
return false;
}
function ___hilite(srcid, destclass) {
___hilite_off();
var src = document.getElementById(srcid);
var wrapper = src;
while (wrapper && !wrapper.classList.contains('hilite-wrapper')) { wrapper = wrapper.parentElement; }
var dests = (wrapper || document).getElementsByClassName(destclass);
global_current_hilite = { src, dests }; // circumvent glitch where the codemirror areas seem to resize themselves
// which causes the arrow to be misaligned. Instead of using a global container for lines:
// var lines = document.getElementById('lines');
// we use a different container for each hilite-wrapper, positionned within it.
var lines = wrapper.getElementsByClassName('lines');
if (lines.length < 1) {
lines = document.createElement('div');
lines.className = 'lines';
wrapper.insertBefore(lines, wrapper.firstChild);
} else {
lines = lines[0];
}
global_current_hilite = { src: src, dests: dests, srcid: srcid, destclass: destclass, lines: lines };
src.classList.add('hilite-src'); src.classList.add('hilite-src');
var lines = document.getElementById('lines'); src.setAttribute('onclick', 'event.stopPropagation(); ___scroll_to_dest("'+srcid+'", "'+destclass+'")');
lines.innerHTML = ''; lines.innerHTML = '';
for (var d = 0; d < dests.length; d++) { for (var d = 0; d < dests.length; d++) {
dests[d].classList.add('hilite-dest'); dests[d].classList.add('hilite-dest');
@ -117,12 +163,22 @@ function ___hilite(src, dest) {
lines.appendChild(l3); lines.appendChild(l3);
l3.style.position = 'absolute'; l3.style.position = 'absolute';
var ar = document.createElement('div');
lines.appendChild(ar);
ar.style.position = 'absolute';
var op = ___getOffset(l1.offsetParent); var op = ___getOffset(l1.offsetParent);
var arrowWidth = 15;
var arrowHeight = 8;
var thickness = 3;
var xa = Math.floor(osrc.left - op.left + src.offsetWidth); var xa = Math.floor(osrc.left - op.left + src.offsetWidth);
var ya = Math.floor(osrc.top - op.top + src.offsetHeight / 2); var ya = Math.floor(osrc.top - op.top + src.offsetHeight / 2);
var xb = Math.floor(otr.left - op.left + tr.offsetWidth); var xb = Math.floor(otr.left - op.left + tr.offsetWidth);
var yb = Math.floor(otr.top - op.top + tr.offsetHeight / 2); var yb = Math.floor(otr.top - op.top + tr.offsetHeight / 2);
var pdest = { left: xb, top: yb };
xb += arrowWidth - 1;
var x = Math.max(xa, xb) + (50 * (d+1)); var x = Math.max(xa, xb) + (50 * (d+1));
if (ya > yb) { if (ya > yb) {
var tmpx = xa; var tmpx = xa;
@ -138,8 +194,6 @@ function ___hilite(src, dest) {
var p3 = { left: x, top: yb }; var p3 = { left: x, top: yb };
var p4 = { left: xb, top: yb }; var p4 = { left: xb, top: yb };
var thickness = 3;
// line 1 // line 1
l1.style.width = p2.left-p1.left; l1.style.width = p2.left-p1.left;
l1.style.height = thickness + 'px'; l1.style.height = thickness + 'px';
@ -153,15 +207,36 @@ function ___hilite(src, dest) {
l2.style.top = p2.top; l2.style.top = p2.top;
l2.style.left = p2.left; l2.style.left = p2.left;
// line 3 // line 3
l3.style.width = p3.left-p4.left; l3.style.width = (p3.left-p4.left)+'px';
l3.style.height = thickness+'px'; l3.style.height = thickness+'px';
l3.style.backgroundColor = 'red'; l3.style.backgroundColor = 'red';
l3.style.top = p4.top; l3.style.top = p4.top+'px';
l3.style.left = p4.left; l3.style.left = p4.left+'px';
// arrow
ar.style.width = '0px';
ar.style.height = '0px';
ar.style.borderLeft = arrowWidth+'px solid transparent';
ar.style.borderTop = arrowHeight+'px solid transparent';
ar.style.borderRight = arrowWidth+'px solid red';
ar.style.borderBottom = arrowHeight+'px solid transparent';
ar.style.top = (pdest.top - arrowHeight + thickness/2)+'px';
ar.style.left = (pdest.left - arrowWidth)+'px';
} }
} }
function ___lolite(src, dest) { function ___lolite(src, dest) {
// For now, keep the highlight onmouseout, to help with scrolling while looking for the target of an arrow.
} }
(function() {
var oldresize = window.onresize;
window.onresize = function () {
if (global_current_hilite.srcid && global_current_hilite.destclass) {
var srcid = global_current_hilite.srcid;
var destclass = global_current_hilite.destclass;
___hilite(srcid, destclass);
}
if (oldresize) { oldresize(); }
}
})();
function ___hex_hash(s) { function ___hex_hash(s) {
var id = ___global_unique_id++; var id = ___global_unique_id++;
var hash = "object-hash-"+___to_hex(s.substr(0,20)); var hash = "object-hash-"+___to_hex(s.substr(0,20));
@ -281,17 +356,19 @@ function ___specialchars_and_colour_and_hex_and_zlib(s) {
} }
if (inflated) { if (inflated) {
var id=___global_unique_id++; var id=___global_unique_id++;
return '<span class="deflate-toggle" onClick="___deflated_click('+id+')">' return {
+ '<span id="deflated'+id+'-pretty">' html:
'<span id="deflated'+id+'-pretty">'
+ '<span class="deflated">deflated:</span>' + '<span class="deflated">deflated:</span>'
+ ___specialchars_and_colour_and_hex(___uint8ArrayToString(inflated)) + ___specialchars_and_colour_and_hex(___uint8ArrayToString(inflated))
+ '</span>' + '</span>'
+ '<span id="deflated'+id+'-raw" style="display:none">' + '<span id="deflated'+id+'-raw" style="display:none">'
+ ___specialchars_and_colour_and_hex(s) + ___specialchars_and_colour_and_hex(s)
+ '</span>' + '</span>',
+ '</span>'; td: function(td) { td.classList.add('deflate-toggle'); td.setAttribute('onclick', '___deflated_click('+id+')'); }
};
} else { } else {
return ___specialchars_and_colour_and_hex(s); return { html: ___specialchars_and_colour_and_hex(s), td: function() {} };
} }
} }
function ___bytestring_to_printf(bs, trailing_x) { function ___bytestring_to_printf(bs, trailing_x) {
@ -300,7 +377,7 @@ function ___bytestring_to_printf(bs, trailing_x) {
}) + (trailing_x ? 'x' : ''); }) + (trailing_x ? 'x' : '');
} }
function ___filesystem_to_printf(fs) { function ___filesystem_to_printf(fs) {
var entries = Object.entries(fs) var entries = ___sort_filesystem_entries(fs)
.map(function (x) { .map(function (x) {
if (x[1] === null) { if (x[1] === null) {
return 'd="$('+___bytestring_to_printf(x[0], true)+')"; mkdir "${d%x}";'; return 'd="$('+___bytestring_to_printf(x[0], true)+')"; mkdir "${d%x}";';
@ -339,29 +416,81 @@ function ___format_filepath(x) {
return ___specialchars_and_colour(x); return ___specialchars_and_colour(x);
} }
} }
function ___format_entry(x) { function ___format_entry(previous_filesystem, x) {
return '<tr><td class="cell-path"><code>' var previous_filesystem = previous_filesystem || {};
+ ___format_filepath(x[0]) var tr = document.createElement('tr');
+ '</code></td><td class="cell-contents">' if (! (previous_filesystem.hasOwnProperty(x[0]) && previous_filesystem[x[0]] == x[1])) {
+ (x[1] === null tr.classList.add('different');
? '<span class="directory">Directory</span>' }
: ("<code>" + ___specialchars_and_colour_and_hex_and_zlib(x[1]) + "</code>"))
+ "</td></tr>"; var td_path = document.createElement('td');
tr.appendChild(td_path);
td_path.classList.add('cell-path');
var td_path_code = document.createElement('code');
td_path.appendChild(td_path_code);
td_path_code.innerHTML = ___format_filepath(x[0]);
var td_contents = document.createElement('td');
tr.appendChild(td_contents);
td_contents.classList.add('cell-contents');
if (x[1] === null) {
td_contents.innerHTML = '<span class="directory">Directory</span>';
} else {
var specials = ___specialchars_and_colour_and_hex_and_zlib(x[1]);
td_contents.innerHTML = '<code>' + specials.html + '</code>';
specials.td(td_contents);
}
return tr;
} }
function ___filesystem_to_string(fs) { function ___sort_filesystem_entries(fs) {
var entries = Object.entries(fs) return Object.entries(fs)
.sort((a,b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0)) .sort((a,b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0));
.map(___format_entry); }
function ___filesystem_to_table(fs, previous_filesystem) {
var table = document.createElement('table');
var thead = document.createElement('thead');
table.appendChild(thead);
var thead_tr = document.createElement('tr');
thead.appendChild(thead_tr);
var thead_tr_th_path = document.createElement('th');
thead_tr.appendChild(thead_tr_th_path);
thead_tr_th_path.innerText = 'Path';
var thead_tr_th_contents = document.createElement('th');
thead_tr.appendChild(thead_tr_th_contents);
thead_tr_th_contents.classList.add('cell-contents');
thead_tr_th_contents.innerText = 'Contents';
var tbody = document.createElement('tbody');
table.appendChild(tbody);
var entries = ___sort_filesystem_entries(fs);
for (var i = 0; i < entries.length; i++) {
tbody.append(___format_entry(previous_filesystem, entries[i]));
}
return table;
}
function ___filesystem_to_string(fs, just_table, previous_filesystem) {
var entries = ___sort_filesystem_entries(fs);
var id = ___global_unique_id++; var id = ___global_unique_id++;
return '<div class="hilite-wrapper">Filesystem contents: ' + entries.length + " files and directories. " var html = '<div class="hilite-wrapper">';
+ '<a href="javascript: ___copyprintf_click(\'elem-'+id+'\');">' if (! just_table) {
+ "Copy commands to recreate in *nix terminal" html += 'Filesystem contents: ' + entries.length + " files and directories. "
+ "</a>." + '<a href="javascript: ___copyprintf_click(\'elem-'+id+'\');">'
+ "<br />" + "Copy commands to recreate in *nix terminal"
+ '<textarea id="elem-'+id+'" disabled="disabled" style="display:none">' + "</a>."
+ ___specialchars(___filesystem_to_printf(fs) || 'echo "Empty filesystem."') + "<br />"
+ '</textarea>' + '<textarea id="elem-'+id+'" disabled="disabled" style="display:none">'
+ "<table><thead><tr><th>Path</th><th>Contents</th></tr></thead><tbody>" + entries.join('') + "</tbody></table></div>"; + ___specialchars(___filesystem_to_printf(fs) || 'echo "Empty filesystem."')
+ '</textarea>';
}
html += ___filesystem_to_table(fs, previous_filesystem).outerHTML // TODO: use DOM primitives instead.
+ '</div>';
return html;
} }
function ___textarea_value(elem) { function ___textarea_value(elem) {
if (elem.getValue) { if (elem.getValue) {
@ -389,10 +518,12 @@ var global_filesystem=false;
function ___git_eval(current) { function ___git_eval(current) {
document.getElementById('hide-eval-' + current).style.display = ''; document.getElementById('hide-eval-' + current).style.display = '';
var script = ''; var script = '';
for (i = 0; i <= current; i++) { for (i = 0; i <= current - 1; i++) {
script += ___textarea_value(___global_editors[i]); script += ___textarea_value(___global_editors[i]);
} }
script += "\n document.getElementById('out' + current).innerHTML = ___filesystem_to_string(filesystem); filesystem;"; script += "\n var ___previous_filesystem = {}; for (k in filesystem) { ___previous_filesystem[k] = filesystem[k]; }\n";
script += ___textarea_value(___global_editors[current]);
script += "\n document.getElementById('out' + current).innerHTML = ___filesystem_to_string(filesystem, false, ___previous_filesystem); filesystem;";
try { try {
global_filesystem = eval(script); global_filesystem = eval(script);
} catch (e) { } catch (e) {
@ -402,190 +533,188 @@ function ___git_eval(current) {
} }
function ___level(s) { function ___level(s) {
if (s) { if (s) {
return (s.tagName == 'SECTION' ? 1 : 0) + ___level(s.parentElement); return (s.tagName == 'SECTION' ? 1 : 0) + ___level(s.parentElement);
} else { } else {
return 0; return 0;
}
} }
function ___process_elements() { }
var sections = document.getElementsByTagName('section'); function ___process_elements() {
var stack = [[]]; var sections = document.getElementsByTagName('section');
var previousLevel = 1; var stack = [[]];
for (var i = 0; i < sections.length; i++) { var previousLevel = 1;
var level = ___level(sections[i]); for (var i = 0; i < sections.length; i++) {
while (level < previousLevel) { var level = ___level(sections[i]);
var p = stack.pop(); while (level < previousLevel) {
previousLevel--; var p = stack.pop();
} previousLevel--;
while (level > previousLevel) {
var top_of_stack = stack[stack.length-1];
stack.push(top_of_stack[top_of_stack.length-1].subsections);
previousLevel++;
}
stack[stack.length-1].push({ s: sections[i], subsections: [] });
} }
var nested = stack[0]; while (level > previousLevel) {
document.getElementById('toc').appendChild(___sections_to_html(nested)); var top_of_stack = stack[stack.length-1];
} stack.push(top_of_stack[top_of_stack.length-1].subsections);
function ___sections_to_html(sections) { previousLevel++;
var ol = document.createElement('ol');
for (var i = 0; i < sections.length; i++) {
var li = document.createElement('li');
ol.appendChild(li);
var headers = sections[i].s.getElementsByTagName('h1');
console.assert(!headers || headers.length >= 1)
var target = sections[i].s.getAttribute('id');
var a = document.createElement('a');
li.appendChild(a);
a.innerHTML = headers[0].innerHTML;
if (target) { a.setAttribute('href', '#' + target); }
if (target) {
var a2 = document.createElement('a');
___insertAfter(a2, headers[0]);
a2.className = "permalink"
a2.setAttribute('href', '#' + target);
a2.innerText = "🔗"
}
li.appendChild(___functions_to_html(sections[i].s));
li.appendChild(___sections_to_html(sections[i].subsections));
} }
return ol; stack[stack.length-1].push({ s: sections[i], subsections: [] });
} }
function ___insertAfter(elt, ref) { var nested = stack[0];
ref.parentElement.insertBefore(elt, ref.nextSibling); document.getElementById('toc').appendChild(___sections_to_html(nested));
} }
function ___ancestor(elem, tag) { function ___sections_to_html(sections) {
if (! elem) { var ol = document.createElement('ol');
return false; for (var i = 0; i < sections.length; i++) {
var li = document.createElement('li');
ol.appendChild(li);
var headers = sections[i].s.getElementsByTagName('h1');
console.assert(!headers || headers.length >= 1)
var target = sections[i].s.getAttribute('id');
var a = document.createElement('a');
li.appendChild(a);
a.innerHTML = headers[0].innerHTML;
if (target) { a.setAttribute('href', '#' + target); }
if (target) {
var a2 = document.createElement('a');
___insertAfter(a2, headers[0]);
a2.className = "permalink"
a2.setAttribute('href', '#' + target);
a2.innerText = "🔗"
} }
if (elem.tagName.toLowerCase() == tag) { li.appendChild(___functions_to_html(sections[i].s));
return elem; li.appendChild(___sections_to_html(sections[i].subsections));
}
return ___ancestor(elem.parentElement, tag);
} }
var ___global_editors = []; return ol;
function ___functions_to_html(section) { }
var ul = document.createElement('ul'); function ___insertAfter(elt, ref) {
var ta = section.getElementsByTagName('textarea'); ref.parentElement.insertBefore(elt, ref.nextSibling);
for (var j = 0; j < ta.length; j++) { }
if (___ancestor(ta[j], 'section') == section) { function ___ancestor(elem, tag) {
var lines = ta[j].value.split('\n'); if (! elem) {
return false;
}
if (elem.tagName.toLowerCase() == tag) {
return elem;
}
return ___ancestor(elem.parentElement, tag);
}
var ___global_editors = [];
function ___functions_to_html(section) {
var ul = document.createElement('ul');
var ta = section.getElementsByTagName('textarea');
for (var j = 0; j < ta.length; j++) {
if (___ancestor(ta[j], 'section') == section) {
var lines = ta[j].value.split('\n');
var ret = ___toCodeMirror(ta[j]);
var editor = ret.editor;
var editor_id = ret.editor_id;
editor.on('keydown', ___clearScrolledToLine);
for (var i = 0; i < lines.length; i++) {
var text = false;
var ret = ___toCodeMirror(ta[j]); var fun = lines[i].match(/^function\s+([a-zA-Z_][a-zA-Z0-9_]*)/);
var editor = ret.editor; if (fun) { text = fun[1]; }
var editor_id = ret.editor_id; var v = lines[i].match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=/);
if (v) { text = v[1]; }
editor.on('keydown', ___clearScrolledToLine); if (text) {
var li = document.createElement('li');
for (var i = 0; i < lines.length; i++) { var a = document.createElement('a');
var text = false; a.setAttribute('href', 'javascript: ___scrollToLine(___global_editors['+(editor_id)+'], '+i+'); void(0);');
var code = document.createElement('code');
var fun = lines[i].match(/^function\s+([a-zA-Z_][a-zA-Z0-9_]*)/); if (fun) {
if (fun) { text = fun[1] + '()'; } var spanFunction = document.createElement('span');
var v = lines[i].match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=/); spanFunction.className = 'function';
if (v) { text = v[1]; } spanFunction.innerText = text;
var parens = document.createTextNode('()');
if (text) { code.appendChild(spanFunction);
var li = document.createElement('li'); code.appendChild(parens);
var a = document.createElement('a'); } else {
a.setAttribute('href', 'javascript: ___scrollToLine(___global_editors['+(editor_id)+'], '+i+'); void(0);'); code.className = 'assignment';
var code = document.createElement('code');
code.innerText = text; code.innerText = text;
a.appendChild(code);
li.appendChild(a);
ul.appendChild(li);
} }
a.appendChild(code);
li.appendChild(a);
ul.appendChild(li);
} }
} }
} }
return ul;
} }
var ___global_current_highlighted_editor_and_line = false; return ul;
function ___clearScrolledToLine() { }
var current = ___global_current_highlighted_editor_and_line; var ___global_current_highlighted_editor_and_line = false;
if (current) { function ___clearScrolledToLine() {
current.editor.removeLineClass(current.line, 'background', 'scrolled-to-line'); var current = ___global_current_highlighted_editor_and_line;
} if (current) {
___global_current_highlighted_editor_and_line = false; current.editor.removeLineClass(current.line, 'background', 'scrolled-to-line');
} }
function ___scrollToLine(editor, line) { ___global_current_highlighted_editor_and_line = false;
___clearScrolledToLine(); }
___global_current_highlighted_editor_and_line = { editor: editor, line: line }; function ___scrollToLine(editor, line) {
___clearScrolledToLine();
editor.addLineClass(line, 'background', 'scrolled-to-line'); ___global_current_highlighted_editor_and_line = { editor: editor, line: line };
editor.addLineClass(line, 'background', 'scrolled-to-line');
var editorOffset = ___getOffset(editor.getScrollerElement()).top; var editorOffset = ___getOffset(editor.getScrollerElement()).top;
var lineOffset = editor.charCoords({line: line, ch: 0}, "local").top; var lineOffset = editor.charCoords({line: line, ch: 0}, "local").top;
document.body.scrollTo(0, editorOffset + lineOffset - window.innerHeight/2); document.body.scrollTo(0, editorOffset + lineOffset - window.innerHeight/2);
}
function ___toCodeMirror(ta) {
var editor = CodeMirror.fromTextArea(ta, {
mode: 'javascript',
lineNumbers: true,
viewportMargin: Infinity
});
var id = ta.getAttribute('id');
ta.remove();
var wrapper = editor.getWrapperElement();
wrapper.setAttribute('id', id);
var editor_id = ___global_editors.length;
___global_editors[editor_id] = editor;
var eval_button = document.createElement('input');
eval_button.setAttribute('type', 'button');
eval_button.setAttribute('value', 'eval');
eval_button.setAttribute('onclick', '___git_eval('+editor_id+')');
___insertAfter(eval_button, wrapper);
var hide_eval_button = document.createElement('input');
hide_eval_button.setAttribute('id', 'hide-eval-' + editor_id);
hide_eval_button.setAttribute('type', 'button');
hide_eval_button.setAttribute('value', 'hide output');
hide_eval_button.setAttribute('onclick', '___hide_eval('+editor_id+')');
hide_eval_button.style.display = 'none';
___insertAfter(hide_eval_button, eval_button);
var out_div = document.createElement('div');
out_div.setAttribute('id', 'out' + editor_id);
___insertAfter(out_div, hide_eval_button);
return { editor: editor, editor_id: editor_id };
}
function ___hide_eval(editor_id) {
document.getElementById('out' + editor_id).innerHTML = '';
document.getElementById('hide-eval-' + editor_id).style.display = 'none';
___hilite_off();
}
function ___get_all_code() {
var all = '';
for (var i = 0; i < ___global_editors.length; i++) {
var val = ___global_editors[i].getValue()
all += val + (val.endsWith('\n') ? '' : '\n') + (val.endsWith('\n\n') ? '' : '\n');
} }
function ___toCodeMirror(ta) { return all.substr(0, all.length-1/*remove last newline in the last \n\n*/);
var editor = CodeMirror.fromTextArea(ta, { }
mode: 'javascript', function ___copy_all_code() {
lineNumbers: true, var elem = document.getElementById('copy-all-code');
viewportMargin: Infinity if (elem.style.display != "none") {
}); elem.style.display = "none";
var id = ta.getAttribute('id'); } else {
ta.remove(); elem.style.display = '';
var wrapper = editor.getWrapperElement(); var elem2 = document.createElement('textarea');
wrapper.setAttribute('id', id); elem.innerHTML = '';
elem.appendChild(elem2);
var editor_id = ___global_editors.length; var all_code = ___get_all_code();
___global_editors[editor_id] = editor; elem2.value = all_code
elem2.focus();
var eval_button = document.createElement('input'); elem2.disabled = false;
eval_button.setAttribute('type', 'button'); elem2.select();
eval_button.setAttribute('value', 'eval'); elem2.setSelectionRange(0, elem2.value.length * 10); // for mobile devices?
eval_button.setAttribute('onclick', '___git_eval('+editor_id+')'); document.execCommand('copy');
___insertAfter(eval_button, wrapper); elem2.disabled = true;
var hide_eval_button = document.createElement('input');
hide_eval_button.setAttribute('id', 'hide-eval-' + editor_id);
hide_eval_button.setAttribute('type', 'button');
hide_eval_button.setAttribute('value', 'hide output');
hide_eval_button.setAttribute('onclick', '___hide_eval('+editor_id+')');
hide_eval_button.style.display = 'none';
___insertAfter(hide_eval_button, eval_button);
var out_div = document.createElement('div');
out_div.setAttribute('id', 'out' + editor_id);
___insertAfter(out_div, hide_eval_button);
return { editor: editor, editor_id: editor_id };
} }
function ___hide_eval(editor_id) { }
document.getElementById('out' + editor_id).innerHTML = '';
document.getElementById('hide-eval-' + editor_id).style.display = 'none';
___hilite_off();
}
function ___get_all_code() {
var all = '';
for (var i = 0; i < ___global_editors.length; i++) {
var val = ___global_editors[i].getValue()
all += val + (val.endsWith('\n') ? '' : '\n') + (val.endsWith('\n\n') ? '' : '\n');
}
return all.substr(0, all.length-1/*remove last newline in the last \n\n*/);
}
function ___copy_all_code() {
var elem = document.getElementById('copy-all-code');
if (elem.style.display != "none") {
elem.style.display = "none";
} else {
elem.style.display = '';
var elem2 = document.createElement('textarea');
elem.innerHTML = '';
elem.appendChild(elem2);
var all_code = ___get_all_code();
elem2.value = all_code
elem2.focus();
elem2.disabled = false;
elem2.select();
elem2.setSelectionRange(0, elem2.value.length * 10); // for mobile devices?
document.execCommand('copy');
elem2.disabled = true;
}
}
___process_elements();
function ___loc_count() { function ___loc_count() {
var srclines = ___get_all_code().split('\n'); var srclines = ___get_all_code().split('\n');
@ -600,4 +729,8 @@ function ___loc_count() {
lct[i].innerText = lctv; lct[i].innerText = lctv;
} }
} }
___loc_count();
function ___git_tutorial_onload() {
___process_elements();
___loc_count();
}

View File

@ -9,12 +9,31 @@
<script src="pako.min.js"></script> <script src="pako.min.js"></script>
<link rel="stylesheet" href="codemirror-5.60.0/lib/codemirror.css"> <link rel="stylesheet" href="codemirror-5.60.0/lib/codemirror.css">
<link rel="stylesheet" href="git-tutorial.css"> <link rel="stylesheet" href="git-tutorial.css">
<script src="git-tutorial.js"></script>
<script class="example">
var examples=[];
function ___h2f(hash) { return 'proj/.git/objects/'+hash.substr(0,2)+'/'+hash.substr(2); }
function ___example(id, f) {
examples.push(function () {
var result = f();
var fs = {};
for (var i = 0; i < result.names.length; i++) {
fs[result.names[i]] = filesystem[result.names[i]];
}
var previous_fs = {};
for (var i = 0; i < result.previous_names.length; i++) {
previous_fs[result.previous_names[i]] = filesystem[result.previous_names[i]];
}
document.getElementById(id).innerHTML = ___filesystem_to_string(fs, true, previous_fs);
});
}
</script>
</head> </head>
<body> <body>
<article id="git-tutorial">
<h1>Under construction</h1> <h1>Under construction</h1>
<article id="git-tutorial">
<p>The main reference for this tutorial is the <a href="https://git-scm.com/book/en/v2/Git-Internals-Git-Objects">Pro Git book</a> section on GIT internals.</p> <p>The main reference for this tutorial is the <a href="https://git-scm.com/book/en/v2/Git-Internals-Git-Objects">Pro Git book</a> section on GIT internals.</p>
<p>This tutorial uses three libraries:</p> <p>This tutorial uses three libraries:</p>
@ -24,8 +43,6 @@
<li><a href="https://github.com/nodeca/pako">pako 2.0.3</a>, released under the MIT and Zlib licenses, see the project page for details.</li> <li><a href="https://github.com/nodeca/pako">pako 2.0.3</a>, released under the MIT and Zlib licenses, see the project page for details.</li>
</ul> </ul>
<div id="lines"></div>
<section id="introduction"> <section id="introduction">
<h1>Introduction</h1> <h1>Introduction</h1>
<p> <p>
@ -44,7 +61,7 @@ excluded, <span class="loc-count-total">a few more</span> in total.</span>
<section id="os-filesystem-model"> <section id="os-filesystem-model">
<h1>Model of the filesystem</h1> <h1>Model of the filesystem</h1>
<p>We will simulate the Operating System's filesystem with a very <p>The Operating System's filesystem will be simulated by a very
simple key-value store. In this very simple filesystem, directories simple key-value store. In this very simple filesystem, directories
are entries mapped to <code>null</code> and files are entries mapped are entries mapped to <code>null</code> and files are entries mapped
to strings. The path to the current directory is stored in a separate to strings. The path to the current directory is stored in a separate
@ -124,21 +141,70 @@ function join_paths(a, b) {
return (a == "") ? b : (a + "/" + b); return (a == "") ? b : (a + "/" + b);
} }
// git init (partial implementation: create the .git directory)
function git_init_mkdir() { function git_init_mkdir() {
mkdir(join_paths(current_directory, '.git')); mkdir(join_paths(current_directory, '.git'));
} }
git_init_mkdir(); git_init_mkdir();
</textarea> </textarea>
<p>Click on the <em>eval</em> button to see the files and directories that were
created so far.</p>
</section> </section>
<section id="git-hash-object"> <section id="git-hash-object">
<h1><code>git hash-object</code><span class="notoc"> (storing a copy of a file in <code>.git</code>)</span></h1> <h1><code>git hash-object</code><span class="notoc"> (storing a copy of a file in <code>.git</code>)</span></h1>
<p>The most basic element of a GIT repository is an object. It is a <p>The most basic element of a GIT repository is an <em>object</em>. Objects have a type which can be
copy of a file that is stored in GIT&apos;s database. That copy is <code>blob</code> (individual files), <code>tree</code> (directories),
stored under a unique name. The unique name is obtained by hashing the <code>commit</code> (pointers to a specific version of the root directory,
contents of the file.</p> with a description and some metadata) and <code>tag</code> (named pointers to a specific commit,
with a description and some metadata).
When a file is added to the git repostitory, a compressed copy is stored in GIT&apos;s database,
in the <code>.git/objects/</code> folder. This copy is a <em>blob</em> object.</p>
<p>The compressed copy is given a unique filename, which is obtained by hashing the contents of the original file.
Some filesystems have poor performance when a single directory contains a large number of files, and some filesystems
have a limit on the number of files that a directory may contain. To circumvent these issues, the first two characters
of the hash are used as the name of an intermediate directory: if a file's hash is <code>0a1bd…</code>, its compressed
copy will be stored in <code>.git/objects/0a/1bd…</code></p>
<p>This function creates a file that looks like this:</p>
<div id="example-blob-object-template"></div>
<script class="example">
___example('example-blob-object-template', function() {
var object_contents = 'type length\000Contents of path_or_data';
var hash = sha1(object_contents);
var path = ___h2f(hash);
write(path, deflate(object_contents));
return { filesystem: filesystem, names: [path], previous_names: [] };
});
</script>
<p>The objects stored in the GIT database are compressed with zlib
(using the "deflate" compression method). The filesystem view shows
the marker <span class="deflated">deflated:</span> followed by the
uncompressed data. Click on the (un)compressed data to toggle between
this pretty-printed view and the raw compressed data.</p>
<p>When creating some <code>blob</code> objects, the result could be, for example:</p>
<div id="example-blob-objects"></div>
<script class="example">
___example('example-blob-objects', function() {
var names = [
___h2f(hash_object(true, 'blob', false, 'src/main.scm')),
___h2f(hash_object(true, 'blob', false, 'README')),
];
return { filesystem: filesystem, names: names, previous_names: [] };
});
</script>
<p>This function reproduces faithfully the behaviour of (a subset of the options of)
the <code>git hash-object</code> command which can be called on a real git command-line.</p>
<textarea id="in5"> <textarea id="in5">
// git hash-object [-w] -t <type> [--stdin] [path]
function hash_object(must_write, type, is_data, path_or_data) { function hash_object(must_write, type, is_data, path_or_data) {
var data = is_data ? path_or_data : read(join_paths(current_directory, path_or_data)); var data = is_data ? path_or_data : read(join_paths(current_directory, path_or_data));
@ -170,16 +236,12 @@ a <em>blob</em> object, i.e. the contents of a file.</p>
// git hash-object -w -t blob README // git hash-object -w -t blob README
hash_object(true, 'blob', false, 'README'); hash_object(true, 'blob', false, 'README');
</textarea> </textarea>
<p>The objects stored in the GIT database are compressed with zlib <p>Click on the <em>eval</em> button to see the file that was
(using the "deflate" compression method). The filesystem view shows created by this call.</p>
the marker <span class="deflated">deflated:</span> followed by the
uncompressed data. Click on the file contents to toggle between this
pretty-printed view and the raw compressed data.
</p>
<p>You will notice that the database does not contain the name of the <p>You can notice that the database does not contain the name of the
file, only its contents, stored under a unique identifier which is original file, only its content, stored under a unique identifier which is
derived by hashing its contents. Let&apos;s add the second user file derived by hashing that content. Let&apos;s add the second user file
to the database.</p> to the database.</p>
<textarea id="in7"> <textarea id="in7">
// git hash-object -w -t blob src/main.scm // git hash-object -w -t blob src/main.scm
@ -190,11 +252,10 @@ hash_object(true, 'blob', false, 'src/main.scm');
<section id="zlib-compression-note"> <section id="zlib-compression-note">
<h1><code>zlib</code> compression</h1> <h1><code>zlib</code> compression</h1>
<p>GIT compresses objects with zlib. To <p>GIT compresses objects with zlib. The <code>deflate()</code> function used in
view a zlib-compressed object in your terminal, simply write this the script above comes from the <a href="https://github.com/nodeca/pako">pako 2.0.3</a> library.
declaration in your shell, and then call e.g. <code>unzlib To view a zlib-compressed object in your *nix terminal, simply write this
.git/objects/95/d318ae78cee607a77c453ead4db344fc1221b7</code></p> declaration in your shell.</p>
<pre> <pre>
unzlib() { unzlib() {
python -c \ python -c \
@ -203,17 +264,35 @@ unzlib() {
"$1" "$1"
} }
</pre> </pre>
<p>You can then inspect git objects as follows, using <code>hexdump</code> to view the null bytes and other non-printable bytes.</p>
<pre>unzlib .git/objects/95/d318ae78cee607a77c453ead4db344fc1221b7 | hexdump -Cv</pre>
</section> </section>
<section id="storing-trees"> <section id="storing-trees">
<h1>Storing trees (list of hashed files and subtrees)</h1> <h1>Storing trees (list of hashed files and subtrees)</h1>
<p>Now GIT knows about the contents of both of the user's <p>At this point GIT knows about the contents of both of the user's
files, but it would be nice to also store the filenames. files, but it would be nice to also store the filenames.
This is done by creating a <em>tree</em> object</p> This is done by creating a <em>tree</em> object</p>
<p>A tree object can contain files (by associating the file's blob to its name), or directories (by associating the hash of other subtrees to their name). <p>A tree object can contain files (by associating the blob's hash to its name), or directories (by associating the hash of other subtrees to their name).
The mode (<code>100644</code> for the file and <code>40000</code> for the folder) incidates the permissions, and is given in octal using <a href="https://unix.stackexchange.com/a/145118/19059">the values used by *nix</a></p> The mode (<code>100644</code> for the file and <code>40000</code> for the folder) incidates the permissions, and is given in octal using <a href="https://unix.stackexchange.com/a/145118/19059">the values used by *nix</a></p>
<div id="example-tree-objects"></div>
<script class="example">
___example('example-tree-objects', function() {
var main = ___h2f(hash_object(true, 'blob', false, 'src/main.scm'));
var readme = ___h2f(hash_object(true, 'blob', false, 'README'));
var src = ___h2f(store_tree("src", ["main.scm"], []));
var proj = ___h2f(paths_to_tree(["README", "src/main.scm"]));
var previous_names = [ main, readme ];
var names = [ main, readme, src, proj ];
return { filesystem: filesystem, names: names, previous_names: previous_names };
});
</script>
<p>In the contents of a tree, subdirectories (trees) are listed before files (blobs);
within each group the entries are ordered alphabetically.</p>
<textarea id="in8"> <textarea id="in8">
// base_directory is a string // base_directory is a string
// filenames is a list of strings // filenames is a list of strings
@ -222,15 +301,15 @@ function store_tree(base_directory, filenames, subtrees) {
function get_file_hash(filename) { function get_file_hash(filename) {
var path = join_paths(base_directory, filename); var path = join_paths(base_directory, filename);
var hash = hash_object(true, 'blob', false, path) var hash = hash_object(true, 'blob', false, path)
return hex_to_bin(hash); return hex_to_raw_bytes(hash);
} }
var blobs = filenames.map(function (filename) { var blobs = filenames.map(function (filename) {
return "100644 " + filename + "\0" + get_file_hash(filename) return "100644 " + filename + "\0" + get_file_hash(filename);
}); });
var trees = subtrees.map(function (subtree) { var trees = subtrees.map(function (subtree) {
return "40000 " + subtree.name + "\0" + hex_to_bin(subtree.hash); return "40000 " + subtree.name + "\0" + hex_to_raw_bytes(subtree.hash);
}); });
// blobs are listed before subtrees // blobs are listed before subtrees
@ -241,9 +320,9 @@ function store_tree(base_directory, filenames, subtrees) {
} }
</textarea> </textarea>
<p>This function needs a small utility to convert hashes encoded in hexadecimal to a binary form.</p> <p>This function needs a small utility to convert hashes encoded in hexadecimal to raw bytes.</p>
<textarea id="in9"> <textarea id="in9">
function hex_to_bin(hex) { function hex_to_raw_bytes(hex) {
var hex = String(hex); var hex = String(hex);
var str = "" var str = ""
for (var i = 0; i < hex.length; i+=2) { for (var i = 0; i < hex.length; i+=2) {
@ -254,41 +333,73 @@ function hex_to_bin(hex) {
</textarea> </textarea>
<section id="store-tree-example"> <section id="store-tree-example">
<h1>Example use of store_tree</h1> <h1>Example use of <code>store_tree()</code></h1>
<p>The following code, once uncommented, stores into the GIT database the trees for <code>src</code>
and for the root directory of the GIT project.</p>
<textarea id="in10"> <textarea id="in10">
//hash_src_tree = store_tree("src", ["main.scm"], []); //hash_src_tree = store_tree("src", ["main.scm"], []);
//hash_root_tree = store_tree("", ["README"], [{name:"src", hash:hash_src_tree}]); //hash_root_tree = store_tree("", ["README"], [{name:"src", hash:hash_src_tree}]);
</textarea> </textarea>
<p>The <code>store_tree()</code> function needs to be called for the contents of subdirectories
first, and that result can be used to store the trees of upper directories. In the next section,
we will write a function which takes a list of paths, constructs an internal representation of
the hierarchy, and stores the corresponding trees bottom-up.</p>
</section> </section>
<section id="store-tree-from-paths"> <section id="store-tree-from-paths">
<h1>Storing a tree from a list of paths</h1> <h1>Storing a tree from a list of paths</h1>
<p>Making trees out of the subfolders one by one is cumbersome. Here's a utility function which takes a list of paths, and builds a tree from those.</p> <p>Making trees out of the subfolders one by one is cumbersome.
The following utility function takes a list of paths, and builds
a tree from those.</p>
<textarea id="in11"> <textarea id="in11">
function paths_to_tree(paths) { function paths_to_tree(paths) {
// This temporary mutable object will store a hierarchy of
// subfolders and files, e.g.
// {
// subfolders: { src: { subfolders: [], files: ['main.scm'] } }
// files: ['README']
// }
var hierarchy = { subfolders: {}, files: [] }; var hierarchy = { subfolders: {}, files: [] };
// This splits the input paths on occurrences of "/",
// and inserts them into the "hierarchy" object.
for (var i = 0; i < paths.length; i++) { for (var i = 0; i < paths.length; i++) {
var path_components = paths[i].split('/'); var path_components = paths[i].split('/');
var h = hierarchy; var h = hierarchy;
for (var j = 0; j < path_components.length - 1; j++) { for (var j = 0; j < path_components.length - 1; j++) {
if (! h.subfolders.hasOwnProperty(path_components[j])) { if (! h.subfolders.hasOwnProperty(path_components[j])) {
h.subfolders[path_components[j]] = { subfolders: {}, files: [] }; h.subfolders[path_components[j]] = {
subfolders: {},
files: []
};
} }
h = h.subfolders[path_components[j]]; h = h.subfolders[path_components[j]];
} }
h.files.push(path_components[i]); h.files[h.files.length] = path_components[path_components.length - 1];
} }
// This function takes the path to a directory, e.g. "src",
// and a hierarchy object e.g. { subfolders: [], files: ['main.scm'] }.
// It recursively stores the tree object for that directory into
// GIT's database.
var to_tree = function(base_directory, hierarchy) { var to_tree = function(base_directory, hierarchy) {
var subtrees = []; var subtrees = [];
for (var i in hierarchy.subfolders) { for (var i in hierarchy.subfolders) {
if (hierarchy.subfolders.hasOwnProperty(i)) { if (hierarchy.subfolders.hasOwnProperty(i)) {
subtrees.push({ name: i, hash: to_tree(join_paths(base_directory, i), hierarchy.subfolders[i]) }); subtrees[subtrees.length] = {
name: i,
hash: to_tree(join_paths(base_directory, i), hierarchy.subfolders[i])
};
} }
} }
return store_tree(base_directory, hierarchy.files, subtrees); return store_tree(base_directory, hierarchy.files, subtrees);
} }
// Store the trees for the whole hierarchy, starting from the
// root directory of the GIT repository (which is represented
// as an empty path "")
return to_tree("", hierarchy); return to_tree("", hierarchy);
} }
@ -303,21 +414,41 @@ paths_to_tree(["README", "src/main.scm"]);
<p>Now that the GIT database contains the entire tree for the current version, <p>Now that the GIT database contains the entire tree for the current version,
a commit can be created. A commit contains</p> a commit can be created. A commit contains</p>
<ul> <ul>
<li>a pointer to the tree</li> <li>the hash of the tree object,</li>
<li>a pointer to the previous ("parent") commit (or to multiple parent commits merging them, or no parents for the initial commit)</li> <li>the hash of the previous commit, which is dubbed the <code>parent</code> (merge commits have two or more parents, and the initial commit has no parent commit),</li>
<li>information about the author (the person who initially wrote the code)</li> <li>information about the author (the person who initially wrote the code),</li>
<li>information about the committer (the person who adds the code to the GIT <li>information about the committer (the person who adds the code to the GIT
database, often the same person as the author, but it can be a different person database, often the same person as the author, but it can be a different person
e.g. when someone else makes changes to the history or applies a patch recieved e.g. when someone else rewrites the history with a rebase or applies a patch recieved
by e-mail)</li> by e-mail),</li>
<li>a description</li> <li>and a description.</li>
</ul> </ul>
<div id="example-commit-object"></div>
<script class="example">
___example('example-commit-object', function() {
var main = ___h2f(hash_object(true, 'blob', false, 'src/main.scm'));
var readme = ___h2f(hash_object(true, 'blob', false, 'README'));
var src = ___h2f(store_tree("src", ["main.scm"], []));
var proj = ___h2f(paths_to_tree(["README", "src/main.scm"]));
var initial_commit = ___h2f(store_commit(
paths_to_tree(["README", "src/main.scm"]),
[],
{name:'Ada Lovelace', email:'ada@analyti.cal', date:new Date(1617120803000), timezoneMinutes: +60},
{name:'Ada Lovelace', email:'ada@analyti.cal', date:new Date(1617120803000), timezoneMinutes: +60},
'Initial commit'));
var previous_names = [ main, readme, src, proj ];
var names = [ main, readme, src, proj, initial_commit ];
return { filesystem: filesystem, names: names, previous_names: previous_names };
});
</script>
<p>The author and committer information contain</p> <p>The author and committer information contain</p>
<ul> <ul>
<li>the person's name</li> <li>the person's name,</li>
<li>the person's email</li> <li>the person's email,</li>
<li>the *nix timestamp at which the version was authored or committed</li> <li>the *nix timestamp at which the version was authored or committed,</li>
<li>the <a href="https://www.youtube.com/watch?v=q2nNzNo_Xps">timezone for that timestamp</a></li> <li>and the <a href="https://www.youtube.com/watch?v=q2nNzNo_Xps">timezone for that timestamp</a>.</li>
</ul> </ul>
<textarea id="in12"> <textarea id="in12">
function store_commit(tree, parents, author, committer, message) { function store_commit(tree, parents, author, committer, message) {
@ -373,7 +504,61 @@ initial_commit = store_commit(
<section id="resolving-references"> <section id="resolving-references">
<h1>resolving references</h1> <h1>resolving references</h1>
<p>The next few sections will introduce <em>symbolic references</em>
like branch names, the special name <code>HEAD</code> or tag names.</p>
<p>Symbolic references are nothing more than regular files containing a hexadecimal
hash or a string of the form <code>ref: path/to/other/symbolic/reference</code>.
The <code>HEAD</code> reference is stored in <code>.git/HEAD</code>, and can point
directly to a commit hash like
<span id="example-reference-head-hash">0123456789abcdef0123456789abcdef01234567</span>,
or can point to another symbolic reference, in which case the <code>.git/HEAD</code> file
will contain e.g. <code>refs/heads/main</code>.</p>
<p>Branches are simple files stored in <code>.git/refs/heads/name-of-the-branch</code></p>
<div id="example-reference"></div>
<script class="example">
___example('example-reference', function() {
var h2f = function(hash) { return 'proj/.git/objects/'+hash.substr(0,2)+'/'+hash.substr(2); }
var main = h2f(hash_object(true, 'blob', false, 'src/main.scm'));
var readme = h2f(hash_object(true, 'blob', false, 'README'));
var src = h2f(store_tree("src", ["main.scm"], []));
var proj = h2f(paths_to_tree(["README", "src/main.scm"]));
var initial_commit_hash = store_commit(
paths_to_tree(["README", "src/main.scm"]),
[],
{name:'Ada Lovelace', email:'ada@analyti.cal', date:new Date(1617120803000), timezoneMinutes: +60},
{name:'Ada Lovelace', email:'ada@analyti.cal', date:new Date(1617120803000), timezoneMinutes: +60},
'Initial commit');
var initial_commit = h2f(initial_commit_hash);
git_branch('main', initial_commit_hash, true);
var main_branch = 'proj/.git/refs/heads/main';
git_tag('v1.0', initial_commit_hash, true);
var v1_0_tag = 'proj/.git/refs/tags/v1.0';
git_init_head();
var head = 'proj/.git/HEAD';
document.getElementById('example-reference-head-hash').innerText = initial_commit_hash;
var previous_names = [ main, readme, src, proj, initial_commit ];
var names = [ main, readme, src, proj, initial_commit, main_branch, v1_0_tag, head ];
return { filesystem: filesystem, names: names, previous_names: previous_names }
});
</script>
<p>We'll start with a small utility to remove the newline at the end of a string.
GIT references are usually files containing a hexadecimal hash, and following
*NIX tradition these files finish with a newline byte. When reading these
references, we need to get rid of the newline first.</p>
<textarea> <textarea>
// Removes the newline at the end of a string, if present.
function trim_newline(s) { function trim_newline(s) {
if (s.endsWith('\n')) { return s.substr(0, s.length-1); } else { return s; } if (s.endsWith('\n')) { return s.substr(0, s.length-1); } else { return s; }
} }
@ -526,10 +711,10 @@ var second_commit = git_commit(['README', 'src/main.scm'], 'Some updates');
<p>GIT does offer a <code>git tag -f existing-tag new-hash</code> command, <p>GIT does offer a <code>git tag -f existing-tag new-hash</code> command,
but using it should be a rare occurrence.</p> but using it should be a rare occurrence.</p>
<textarea id="in17"> <textarea id="in17">
function git_tag(tag_name, commit_hash) { function git_tag(tag_name, commit_hash, force) {
mkdir(join_paths(current_directory, '.git/refs')); mkdir(join_paths(current_directory, '.git/refs'));
mkdir(join_paths(current_directory, '.git/refs/tags')); mkdir(join_paths(current_directory, '.git/refs/tags'));
if (exists(join_paths(current_directory, '.git/refs/tags/' + tag_name))) { if (!force && exists(join_paths(current_directory, '.git/refs/tags/' + tag_name))) {
alert("tag already exists"); alert("tag already exists");
return false; return false;
} else { } else {
@ -643,7 +828,7 @@ function parse_tree_entry(entry) {
} }
</textarea> </textarea>
<p>The <code>parse_tree</code> function above needs a small utility to convert hashes in binary form to a hexadecimal representation.</p> <p>The <code>parse_tree</code> function above needs a small utility to convert hashes represented using raw bytes to a hexadecimal representation.</p>
<textarea id="in19"> <textarea id="in19">
function to_hex(bin) { function to_hex(bin) {
var bin = String(bin); var bin = String(bin);
@ -758,39 +943,39 @@ function git_init() {
The mock filesystem used here lacks most of these pieces of information, so thr value <code>0</code> The mock filesystem used here lacks most of these pieces of information, so thr value <code>0</code>
will be used for most fields. See <a href="https://mincong.io/2018/04/28/git-index/">this blog post</a> will be used for most fields. See <a href="https://mincong.io/2018/04/28/git-index/">this blog post</a>
for a more in-depth study of the index.</p> for a more in-depth study of the index.</p>
<textarea id="index-binary-utils"> <textarea id="index-raw-bytes-utils">
function binary(val, bytes) { function raw_bytes(val, bytes) {
return hex_to_bin(left_pad(val.toString(16), '0', bytes*2)); return hex_to_raw_bytes(left_pad(val.toString(16), '0', bytes*2));
} }
function binary16(val) { return binary(val, 2); } function raw_bytes16(val) { return raw_bytes(val, 2); }
function binary32(val) { return binary(val, 4); } function raw_bytes32(val) { return raw_bytes(val, 4); }
function binary64(val) { return binary(val, 8); } function raw_bytes64(val) { return raw_bytes(val, 8); }
</textarea> </textarea>
<textarea id="make-index"> <textarea id="make-index">
function store_index(paths) { function store_index(paths) {
var magic = 'DIRC' // DIRectory Cache var magic = 'DIRC' // DIRectory Cache
var version = binary32(2); var version = raw_bytes32(2);
var entries = binary32(paths.length); var entries = raw_bytes32(paths.length);
var header = magic + version + entries; var header = magic + version + entries;
index = header; index = header;
for (var i = 0; i < paths.length; i++) { for (var i = 0; i < paths.length; i++) {
var ctime = binary64(0); var ctime = raw_bytes64(0);
var mtime = binary64(0); var mtime = raw_bytes64(0);
var device = binary32(0); var device = raw_bytes32(0);
var inode = binary32(0); var inode = raw_bytes32(0);
// default permissions for files, in octal. // default permissions for files, in octal.
var mode = binary32(0100644); var mode = raw_bytes32(0100644);
var uid = binary32(0); var uid = raw_bytes32(0);
var gid = binary32(0); var gid = raw_bytes32(0);
var size = binary32(read(join_paths(current_directory, paths[i])).length); var size = raw_bytes32(read(join_paths(current_directory, paths[i])).length);
var hash = hex_to_bin(hash_object(true, 'blob', false, paths[i])); var hash = hex_to_raw_bytes(hash_object(true, 'blob', false, paths[i]));
// for this simple index, the flags (the 4 higher bits) are 0. // for this simple index, the flags (the 4 higher bits) are 0.
assert(paths[i].length < 0xfff) assert(paths[i].length < 0xfff)
var flags_and_file_path_length = binary16(paths[i].length) var flags_and_file_path_length = raw_bytes16(paths[i].length)
var file_path = paths[i] + '\0'; var file_path = paths[i] + '\0';
entry = ctime + mtime + device + inode + mode + uid + gid + size entry = ctime + mtime + device + inode + mode + uid + gid + size
+ hash + flags_and_file_path_length + file_path; + hash + flags_and_file_path_length + file_path;
@ -802,7 +987,7 @@ function store_index(paths) {
index += entry; index += entry;
} }
index += hex_to_bin(sha1(index)); index += hex_to_raw_bytes(sha1(index));
write(join_paths(current_directory, '.git/index'), index) write(join_paths(current_directory, '.git/index'), index)
} }
@ -844,7 +1029,7 @@ store_index(['README', 'src/main.scm']);
</textarea> </textarea>
<p>By clicking on "Copy commands to recreate in *nix terminal.", it is possible to copy a series of <code>mkdir …</code> and <code>printf … > …</code> commands that, when executed, will recreate the virtual filesystem on a real system. The resulting <p>By clicking on "Copy commands to recreate in *nix terminal.", it is possible to copy a series of <code>mkdir …</code> and <code>printf … > …</code> commands that, when executed, will recreate the virtual filesystem on a real system. The resulting
folder is binary-compatible with the official <code>git log</code>, <code>git status</code>, <code>git checkout</code> etc. folder is bit-compatible with the official <code>git log</code>, <code>git status</code>, <code>git checkout</code> etc.
commands.</p> commands.</p>
</section> </section>
@ -869,6 +1054,26 @@ commands.</p>
<div id="toc"></div> <div id="toc"></div>
</article> </article>
<script src="git-tutorial.js"></script> <script>
(function() {
var script = '';
var ta = document.getElementsByTagName('textarea');
for (var j = 0; j < ta.length; j++) {
if (ta[j] == document.getElementById('playground-reset')) {
break;
}
script += ta[j].value + "\n\n";
}
var js = document.getElementsByTagName('script');
for (var j = 0; j < js.length; j++) {
if (js[j].className.indexOf('example') != -1) {
script += js[j].innerText;
}
}
script += '\nfor (var i = 0; i < examples.length; i++) { examples[i](); }';
eval(script);
})();
___git_tutorial_onload()
</script>
</body> </body>
</html> </html>