A lot of cleanup and small style improvements, currently improving the prose for commits.
This commit is contained in:
parent
94649bd5ed
commit
39fb386e5a
|
@ -1,29 +1,44 @@
|
|||
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; }
|
||||
#git-tutorial #toc { position: fixed; top: 0; bottom: 0; right: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; }
|
||||
article#git-tutorial { position: absolute; top:0; left:0.5em; transition: right, 0.2s; }
|
||||
#git-tutorial #toc { position: fixed; top: 0; bottom: 0; width: 17.4em; text-align: left; overflow: scroll;
|
||||
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 #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 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.cell-contents { font-family: monospace; width: 36em; }
|
||||
#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: 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) {
|
||||
#git-tutorial td { padding-left: 0; padding-right: 0; }
|
||||
#git-tutorial td.cell-contents { width: 34em; }
|
||||
article#git-tutorial { right: 7em; }
|
||||
#git-tutorial td, #git-tutorial th { padding-left: 0; padding-right: 0; }
|
||||
#git-tutorial td.cell-contents, #git-tutorial th.cell-contents { width: 34em; }
|
||||
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:hover { border-left: 5px solid gray; }
|
||||
}
|
||||
|
||||
@media (max-width: 63em) {
|
||||
#git-tutorial td { padding-left: 0; padding-right: 0; }
|
||||
#git-tutorial td.cell-contents { width: 30em; }
|
||||
article#git-tutorial { right:6em; }
|
||||
#git-tutorial td, #git-tutorial th { padding-left: 0; padding-right: 0; }
|
||||
#git-tutorial td.cell-contents, #git-tutorial th.cell-contents { width: 30em; }
|
||||
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: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 .CodeMirror { height: max-content; }
|
||||
#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 .hex-prefix { color: lightgrey; }
|
||||
#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 #toc ul { list-style-type: none; padding: 0 !important; /*list-style-type: disc;*/ }
|
||||
#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 .CodeMirror .scrolled-to-line { background: lightcyan; }
|
||||
#git-tutorial #toc > ol { padding-left: 0.7em; }
|
||||
#git-tutorial #toc ol > li > a { text-decoration: none; }
|
||||
#git-tutorial #toc li { padding-top: 0.4em; }
|
||||
#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; }
|
||||
|
|
559
git-tutorial.js
559
git-tutorial.js
|
@ -22,6 +22,15 @@ function ___to_hex(s) {
|
|||
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.
|
||||
sha1 = function(s) { return Sha1.hash(___to_hex(s), { msgFormat: 'hex-bytes', outFormat: 'hex' }); };
|
||||
deflate = function(s) { return ___uint8ArrayToString(pako.deflate(___stringToUint8Array(s))); }
|
||||
|
@ -75,7 +84,7 @@ function ___getOffset(elt) {
|
|||
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() {
|
||||
if (global_current_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++) {
|
||||
global_current_hilite.dests[d].classList.remove('hilite-dest');
|
||||
}
|
||||
global_current_hilite = { src: false, dests: [] };
|
||||
document.getElementById('lines').innerHTML = '';
|
||||
if (global_current_hilite.lines) {
|
||||
global_current_hilite.lines.innerHTML = '';
|
||||
}
|
||||
global_current_hilite = { src: false, dests: [], srcid: false, destclass: false, lines: false };
|
||||
}
|
||||
function ___hilite(src, dest) {
|
||||
___hilite_off();
|
||||
var src = document.getElementById(src);
|
||||
function ___scroll_to_dest(srcid, destclass) {
|
||||
var src = document.getElementById(srcid);
|
||||
var wrapper = src;
|
||||
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');
|
||||
var lines = document.getElementById('lines');
|
||||
src.setAttribute('onclick', 'event.stopPropagation(); ___scroll_to_dest("'+srcid+'", "'+destclass+'")');
|
||||
lines.innerHTML = '';
|
||||
for (var d = 0; d < dests.length; d++) {
|
||||
dests[d].classList.add('hilite-dest');
|
||||
|
@ -117,12 +163,22 @@ function ___hilite(src, dest) {
|
|||
lines.appendChild(l3);
|
||||
l3.style.position = 'absolute';
|
||||
|
||||
var ar = document.createElement('div');
|
||||
lines.appendChild(ar);
|
||||
ar.style.position = 'absolute';
|
||||
|
||||
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 ya = Math.floor(osrc.top - op.top + src.offsetHeight / 2);
|
||||
var xb = Math.floor(otr.left - op.left + tr.offsetWidth);
|
||||
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));
|
||||
if (ya > yb) {
|
||||
var tmpx = xa;
|
||||
|
@ -138,8 +194,6 @@ function ___hilite(src, dest) {
|
|||
var p3 = { left: x, top: yb };
|
||||
var p4 = { left: xb, top: yb };
|
||||
|
||||
var thickness = 3;
|
||||
|
||||
// line 1
|
||||
l1.style.width = p2.left-p1.left;
|
||||
l1.style.height = thickness + 'px';
|
||||
|
@ -153,15 +207,36 @@ function ___hilite(src, dest) {
|
|||
l2.style.top = p2.top;
|
||||
l2.style.left = p2.left;
|
||||
// line 3
|
||||
l3.style.width = p3.left-p4.left;
|
||||
l3.style.width = (p3.left-p4.left)+'px';
|
||||
l3.style.height = thickness+'px';
|
||||
l3.style.backgroundColor = 'red';
|
||||
l3.style.top = p4.top;
|
||||
l3.style.left = p4.left;
|
||||
l3.style.top = p4.top+'px';
|
||||
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) {
|
||||
// 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) {
|
||||
var id = ___global_unique_id++;
|
||||
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) {
|
||||
var id=___global_unique_id++;
|
||||
return '<span class="deflate-toggle" onClick="___deflated_click('+id+')">'
|
||||
+ '<span id="deflated'+id+'-pretty">'
|
||||
return {
|
||||
html:
|
||||
'<span id="deflated'+id+'-pretty">'
|
||||
+ '<span class="deflated">deflated:</span>'
|
||||
+ ___specialchars_and_colour_and_hex(___uint8ArrayToString(inflated))
|
||||
+ '</span>'
|
||||
+ '<span id="deflated'+id+'-raw" style="display:none">'
|
||||
+ ___specialchars_and_colour_and_hex(s)
|
||||
+ '</span>'
|
||||
+ '</span>';
|
||||
+ '</span>',
|
||||
td: function(td) { td.classList.add('deflate-toggle'); td.setAttribute('onclick', '___deflated_click('+id+')'); }
|
||||
};
|
||||
} 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) {
|
||||
|
@ -300,7 +377,7 @@ function ___bytestring_to_printf(bs, trailing_x) {
|
|||
}) + (trailing_x ? 'x' : '');
|
||||
}
|
||||
function ___filesystem_to_printf(fs) {
|
||||
var entries = Object.entries(fs)
|
||||
var entries = ___sort_filesystem_entries(fs)
|
||||
.map(function (x) {
|
||||
if (x[1] === null) {
|
||||
return 'd="$('+___bytestring_to_printf(x[0], true)+')"; mkdir "${d%x}";';
|
||||
|
@ -339,29 +416,81 @@ function ___format_filepath(x) {
|
|||
return ___specialchars_and_colour(x);
|
||||
}
|
||||
}
|
||||
function ___format_entry(x) {
|
||||
return '<tr><td class="cell-path"><code>'
|
||||
+ ___format_filepath(x[0])
|
||||
+ '</code></td><td class="cell-contents">'
|
||||
+ (x[1] === null
|
||||
? '<span class="directory">Directory</span>'
|
||||
: ("<code>" + ___specialchars_and_colour_and_hex_and_zlib(x[1]) + "</code>"))
|
||||
+ "</td></tr>";
|
||||
function ___format_entry(previous_filesystem, x) {
|
||||
var previous_filesystem = previous_filesystem || {};
|
||||
var tr = document.createElement('tr');
|
||||
if (! (previous_filesystem.hasOwnProperty(x[0]) && previous_filesystem[x[0]] == x[1])) {
|
||||
tr.classList.add('different');
|
||||
}
|
||||
|
||||
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) {
|
||||
var entries = Object.entries(fs)
|
||||
.sort((a,b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0))
|
||||
.map(___format_entry);
|
||||
function ___sort_filesystem_entries(fs) {
|
||||
return Object.entries(fs)
|
||||
.sort((a,b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0));
|
||||
}
|
||||
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++;
|
||||
return '<div class="hilite-wrapper">Filesystem contents: ' + entries.length + " files and directories. "
|
||||
+ '<a href="javascript: ___copyprintf_click(\'elem-'+id+'\');">'
|
||||
+ "Copy commands to recreate in *nix terminal"
|
||||
+ "</a>."
|
||||
+ "<br />"
|
||||
+ '<textarea id="elem-'+id+'" disabled="disabled" style="display:none">'
|
||||
+ ___specialchars(___filesystem_to_printf(fs) || 'echo "Empty filesystem."')
|
||||
+ '</textarea>'
|
||||
+ "<table><thead><tr><th>Path</th><th>Contents</th></tr></thead><tbody>" + entries.join('') + "</tbody></table></div>";
|
||||
var html = '<div class="hilite-wrapper">';
|
||||
if (! just_table) {
|
||||
html += 'Filesystem contents: ' + entries.length + " files and directories. "
|
||||
+ '<a href="javascript: ___copyprintf_click(\'elem-'+id+'\');">'
|
||||
+ "Copy commands to recreate in *nix terminal"
|
||||
+ "</a>."
|
||||
+ "<br />"
|
||||
+ '<textarea id="elem-'+id+'" disabled="disabled" style="display:none">'
|
||||
+ ___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) {
|
||||
if (elem.getValue) {
|
||||
|
@ -389,10 +518,12 @@ var global_filesystem=false;
|
|||
function ___git_eval(current) {
|
||||
document.getElementById('hide-eval-' + current).style.display = '';
|
||||
var script = '';
|
||||
for (i = 0; i <= current; i++) {
|
||||
for (i = 0; i <= current - 1; 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 {
|
||||
global_filesystem = eval(script);
|
||||
} catch (e) {
|
||||
|
@ -402,190 +533,188 @@ function ___git_eval(current) {
|
|||
}
|
||||
|
||||
function ___level(s) {
|
||||
if (s) {
|
||||
return (s.tagName == 'SECTION' ? 1 : 0) + ___level(s.parentElement);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if (s) {
|
||||
return (s.tagName == 'SECTION' ? 1 : 0) + ___level(s.parentElement);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
function ___process_elements() {
|
||||
var sections = document.getElementsByTagName('section');
|
||||
var stack = [[]];
|
||||
var previousLevel = 1;
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var level = ___level(sections[i]);
|
||||
while (level < 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: [] });
|
||||
}
|
||||
function ___process_elements() {
|
||||
var sections = document.getElementsByTagName('section');
|
||||
var stack = [[]];
|
||||
var previousLevel = 1;
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var level = ___level(sections[i]);
|
||||
while (level < previousLevel) {
|
||||
var p = stack.pop();
|
||||
previousLevel--;
|
||||
}
|
||||
var nested = stack[0];
|
||||
document.getElementById('toc').appendChild(___sections_to_html(nested));
|
||||
}
|
||||
function ___sections_to_html(sections) {
|
||||
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));
|
||||
while (level > previousLevel) {
|
||||
var top_of_stack = stack[stack.length-1];
|
||||
stack.push(top_of_stack[top_of_stack.length-1].subsections);
|
||||
previousLevel++;
|
||||
}
|
||||
return ol;
|
||||
stack[stack.length-1].push({ s: sections[i], subsections: [] });
|
||||
}
|
||||
function ___insertAfter(elt, ref) {
|
||||
ref.parentElement.insertBefore(elt, ref.nextSibling);
|
||||
}
|
||||
function ___ancestor(elem, tag) {
|
||||
if (! elem) {
|
||||
return false;
|
||||
var nested = stack[0];
|
||||
document.getElementById('toc').appendChild(___sections_to_html(nested));
|
||||
}
|
||||
function ___sections_to_html(sections) {
|
||||
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 = "🔗"
|
||||
}
|
||||
if (elem.tagName.toLowerCase() == tag) {
|
||||
return elem;
|
||||
}
|
||||
return ___ancestor(elem.parentElement, tag);
|
||||
li.appendChild(___functions_to_html(sections[i].s));
|
||||
li.appendChild(___sections_to_html(sections[i].subsections));
|
||||
}
|
||||
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 fun = lines[i].match(/^function\s+([a-zA-Z_][a-zA-Z0-9_]*)/);
|
||||
if (fun) { text = fun[1] + '()'; }
|
||||
var v = lines[i].match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=/);
|
||||
if (v) { text = v[1]; }
|
||||
|
||||
if (text) {
|
||||
var li = document.createElement('li');
|
||||
var a = document.createElement('a');
|
||||
a.setAttribute('href', 'javascript: ___scrollToLine(___global_editors['+(editor_id)+'], '+i+'); void(0);');
|
||||
var code = document.createElement('code');
|
||||
return ol;
|
||||
}
|
||||
function ___insertAfter(elt, ref) {
|
||||
ref.parentElement.insertBefore(elt, ref.nextSibling);
|
||||
}
|
||||
function ___ancestor(elem, tag) {
|
||||
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 fun = lines[i].match(/^function\s+([a-zA-Z_][a-zA-Z0-9_]*)/);
|
||||
if (fun) { text = fun[1]; }
|
||||
var v = lines[i].match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=/);
|
||||
if (v) { text = v[1]; }
|
||||
if (text) {
|
||||
var li = document.createElement('li');
|
||||
var a = document.createElement('a');
|
||||
a.setAttribute('href', 'javascript: ___scrollToLine(___global_editors['+(editor_id)+'], '+i+'); void(0);');
|
||||
var code = document.createElement('code');
|
||||
if (fun) {
|
||||
var spanFunction = document.createElement('span');
|
||||
spanFunction.className = 'function';
|
||||
spanFunction.innerText = text;
|
||||
var parens = document.createTextNode('()');
|
||||
code.appendChild(spanFunction);
|
||||
code.appendChild(parens);
|
||||
} else {
|
||||
code.className = 'assignment';
|
||||
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;
|
||||
function ___clearScrolledToLine() {
|
||||
var current = ___global_current_highlighted_editor_and_line;
|
||||
if (current) {
|
||||
current.editor.removeLineClass(current.line, 'background', 'scrolled-to-line');
|
||||
}
|
||||
___global_current_highlighted_editor_and_line = false;
|
||||
return ul;
|
||||
}
|
||||
var ___global_current_highlighted_editor_and_line = false;
|
||||
function ___clearScrolledToLine() {
|
||||
var current = ___global_current_highlighted_editor_and_line;
|
||||
if (current) {
|
||||
current.editor.removeLineClass(current.line, 'background', 'scrolled-to-line');
|
||||
}
|
||||
function ___scrollToLine(editor, line) {
|
||||
___clearScrolledToLine();
|
||||
___global_current_highlighted_editor_and_line = { editor: editor, line: line };
|
||||
|
||||
editor.addLineClass(line, 'background', 'scrolled-to-line');
|
||||
|
||||
var editorOffset = ___getOffset(editor.getScrollerElement()).top;
|
||||
var lineOffset = editor.charCoords({line: line, ch: 0}, "local").top;
|
||||
document.body.scrollTo(0, editorOffset + lineOffset - window.innerHeight/2);
|
||||
___global_current_highlighted_editor_and_line = false;
|
||||
}
|
||||
function ___scrollToLine(editor, line) {
|
||||
___clearScrolledToLine();
|
||||
___global_current_highlighted_editor_and_line = { editor: editor, line: line };
|
||||
editor.addLineClass(line, 'background', 'scrolled-to-line');
|
||||
var editorOffset = ___getOffset(editor.getScrollerElement()).top;
|
||||
var lineOffset = editor.charCoords({line: line, ch: 0}, "local").top;
|
||||
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) {
|
||||
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 };
|
||||
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;
|
||||
}
|
||||
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() {
|
||||
var srclines = ___get_all_code().split('\n');
|
||||
|
@ -600,4 +729,8 @@ function ___loc_count() {
|
|||
lct[i].innerText = lctv;
|
||||
}
|
||||
}
|
||||
___loc_count();
|
||||
|
||||
function ___git_tutorial_onload() {
|
||||
___process_elements();
|
||||
___loc_count();
|
||||
}
|
341
index.html
341
index.html
|
@ -9,12 +9,31 @@
|
|||
<script src="pako.min.js"></script>
|
||||
<link rel="stylesheet" href="codemirror-5.60.0/lib/codemirror.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>
|
||||
<body>
|
||||
|
||||
<article id="git-tutorial">
|
||||
<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>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>
|
||||
</ul>
|
||||
|
||||
<div id="lines"></div>
|
||||
|
||||
<section id="introduction">
|
||||
<h1>Introduction</h1>
|
||||
<p>
|
||||
|
@ -44,7 +61,7 @@ excluded, <span class="loc-count-total">a few more</span> in total.</span>
|
|||
|
||||
<section id="os-filesystem-model">
|
||||
<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
|
||||
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
|
||||
|
@ -124,21 +141,70 @@ function join_paths(a, b) {
|
|||
return (a == "") ? b : (a + "/" + b);
|
||||
}
|
||||
|
||||
// git init (partial implementation: create the .git directory)
|
||||
function git_init_mkdir() {
|
||||
mkdir(join_paths(current_directory, '.git'));
|
||||
}
|
||||
|
||||
git_init_mkdir();
|
||||
</textarea>
|
||||
<p>Click on the <em>eval</em> button to see the files and directories that were
|
||||
created so far.</p>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<p>The most basic element of a GIT repository is an object. It is a
|
||||
copy of a file that is stored in GIT's database. That copy is
|
||||
stored under a unique name. The unique name is obtained by hashing the
|
||||
contents of the file.</p>
|
||||
<p>The most basic element of a GIT repository is an <em>object</em>. Objects have a type which can be
|
||||
<code>blob</code> (individual files), <code>tree</code> (directories),
|
||||
<code>commit</code> (pointers to a specific version of the root directory,
|
||||
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'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">
|
||||
// git hash-object [-w] -t <type> [--stdin] [path]
|
||||
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));
|
||||
|
||||
|
@ -170,16 +236,12 @@ a <em>blob</em> object, i.e. the contents of a file.</p>
|
|||
// git hash-object -w -t blob README
|
||||
hash_object(true, 'blob', false, 'README');
|
||||
</textarea>
|
||||
<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 file contents to toggle between this
|
||||
pretty-printed view and the raw compressed data.
|
||||
</p>
|
||||
<p>Click on the <em>eval</em> button to see the file that was
|
||||
created by this call.</p>
|
||||
|
||||
<p>You will notice that the database does not contain the name of the
|
||||
file, only its contents, stored under a unique identifier which is
|
||||
derived by hashing its contents. Let's add the second user file
|
||||
<p>You can notice that the database does not contain the name of the
|
||||
original file, only its content, stored under a unique identifier which is
|
||||
derived by hashing that content. Let's add the second user file
|
||||
to the database.</p>
|
||||
<textarea id="in7">
|
||||
// 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">
|
||||
<h1><code>zlib</code> compression</h1>
|
||||
<p>GIT compresses objects with zlib. To
|
||||
view a zlib-compressed object in your terminal, simply write this
|
||||
declaration in your shell, and then call e.g. <code>unzlib
|
||||
.git/objects/95/d318ae78cee607a77c453ead4db344fc1221b7</code></p>
|
||||
|
||||
<p>GIT compresses objects with zlib. The <code>deflate()</code> function used in
|
||||
the script above comes from the <a href="https://github.com/nodeca/pako">pako 2.0.3</a> library.
|
||||
To view a zlib-compressed object in your *nix terminal, simply write this
|
||||
declaration in your shell.</p>
|
||||
<pre>
|
||||
unzlib() {
|
||||
python -c \
|
||||
|
@ -203,17 +264,35 @@ unzlib() {
|
|||
"$1"
|
||||
}
|
||||
</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 id="storing-trees">
|
||||
<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.
|
||||
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>
|
||||
|
||||
<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">
|
||||
// base_directory is a string
|
||||
// filenames is a list of strings
|
||||
|
@ -222,15 +301,15 @@ function store_tree(base_directory, filenames, subtrees) {
|
|||
function get_file_hash(filename) {
|
||||
var path = join_paths(base_directory, filename);
|
||||
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) {
|
||||
return "100644 " + filename + "\0" + get_file_hash(filename)
|
||||
return "100644 " + filename + "\0" + get_file_hash(filename);
|
||||
});
|
||||
|
||||
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
|
||||
|
@ -241,9 +320,9 @@ function store_tree(base_directory, filenames, subtrees) {
|
|||
}
|
||||
</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">
|
||||
function hex_to_bin(hex) {
|
||||
function hex_to_raw_bytes(hex) {
|
||||
var hex = String(hex);
|
||||
var str = ""
|
||||
for (var i = 0; i < hex.length; i+=2) {
|
||||
|
@ -254,41 +333,73 @@ function hex_to_bin(hex) {
|
|||
</textarea>
|
||||
|
||||
<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">
|
||||
//hash_src_tree = store_tree("src", ["main.scm"], []);
|
||||
//hash_root_tree = store_tree("", ["README"], [{name:"src", hash:hash_src_tree}]);
|
||||
</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 id="store-tree-from-paths">
|
||||
<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">
|
||||
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: [] };
|
||||
|
||||
// This splits the input paths on occurrences of "/",
|
||||
// and inserts them into the "hierarchy" object.
|
||||
for (var i = 0; i < paths.length; i++) {
|
||||
var path_components = paths[i].split('/');
|
||||
var h = hierarchy;
|
||||
for (var j = 0; j < path_components.length - 1; 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.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 subtrees = [];
|
||||
for (var i in hierarchy.subfolders) {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
a commit can be created. A commit contains</p>
|
||||
<ul>
|
||||
<li>a pointer to the tree</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>information about the author (the person who initially wrote the code)</li>
|
||||
<li>the hash of the tree object,</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 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
|
||||
e.g. when someone else makes changes to the history or applies a patch recieved
|
||||
by e-mail)</li>
|
||||
<li>a description</li>
|
||||
e.g. when someone else rewrites the history with a rebase or applies a patch recieved
|
||||
by e-mail),</li>
|
||||
<li>and a description.</li>
|
||||
</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>
|
||||
<ul>
|
||||
<li>the person's name</li>
|
||||
<li>the person's email</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>the person's name,</li>
|
||||
<li>the person's email,</li>
|
||||
<li>the *nix timestamp at which the version was authored or committed,</li>
|
||||
<li>and the <a href="https://www.youtube.com/watch?v=q2nNzNo_Xps">timezone for that timestamp</a>.</li>
|
||||
</ul>
|
||||
<textarea id="in12">
|
||||
function store_commit(tree, parents, author, committer, message) {
|
||||
|
@ -373,7 +504,61 @@ initial_commit = store_commit(
|
|||
|
||||
<section id="resolving-references">
|
||||
<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>
|
||||
// Removes the newline at the end of a string, if present.
|
||||
function trim_newline(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,
|
||||
but using it should be a rare occurrence.</p>
|
||||
<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/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");
|
||||
return false;
|
||||
} else {
|
||||
|
@ -643,7 +828,7 @@ function parse_tree_entry(entry) {
|
|||
}
|
||||
</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">
|
||||
function to_hex(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>
|
||||
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>
|
||||
<textarea id="index-binary-utils">
|
||||
function binary(val, bytes) {
|
||||
return hex_to_bin(left_pad(val.toString(16), '0', bytes*2));
|
||||
<textarea id="index-raw-bytes-utils">
|
||||
function raw_bytes(val, bytes) {
|
||||
return hex_to_raw_bytes(left_pad(val.toString(16), '0', bytes*2));
|
||||
}
|
||||
|
||||
function binary16(val) { return binary(val, 2); }
|
||||
function binary32(val) { return binary(val, 4); }
|
||||
function binary64(val) { return binary(val, 8); }
|
||||
function raw_bytes16(val) { return raw_bytes(val, 2); }
|
||||
function raw_bytes32(val) { return raw_bytes(val, 4); }
|
||||
function raw_bytes64(val) { return raw_bytes(val, 8); }
|
||||
</textarea>
|
||||
|
||||
<textarea id="make-index">
|
||||
function store_index(paths) {
|
||||
var magic = 'DIRC' // DIRectory Cache
|
||||
var version = binary32(2);
|
||||
var entries = binary32(paths.length);
|
||||
var version = raw_bytes32(2);
|
||||
var entries = raw_bytes32(paths.length);
|
||||
var header = magic + version + entries;
|
||||
|
||||
index = header;
|
||||
|
||||
for (var i = 0; i < paths.length; i++) {
|
||||
var ctime = binary64(0);
|
||||
var mtime = binary64(0);
|
||||
var device = binary32(0);
|
||||
var inode = binary32(0);
|
||||
var ctime = raw_bytes64(0);
|
||||
var mtime = raw_bytes64(0);
|
||||
var device = raw_bytes32(0);
|
||||
var inode = raw_bytes32(0);
|
||||
// default permissions for files, in octal.
|
||||
var mode = binary32(0100644);
|
||||
var uid = binary32(0);
|
||||
var gid = binary32(0);
|
||||
var size = binary32(read(join_paths(current_directory, paths[i])).length);
|
||||
var hash = hex_to_bin(hash_object(true, 'blob', false, paths[i]));
|
||||
var mode = raw_bytes32(0100644);
|
||||
var uid = raw_bytes32(0);
|
||||
var gid = raw_bytes32(0);
|
||||
var size = raw_bytes32(read(join_paths(current_directory, paths[i])).length);
|
||||
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.
|
||||
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';
|
||||
entry = ctime + mtime + device + inode + mode + uid + gid + size
|
||||
+ hash + flags_and_file_path_length + file_path;
|
||||
|
@ -802,7 +987,7 @@ function store_index(paths) {
|
|||
index += entry;
|
||||
}
|
||||
|
||||
index += hex_to_bin(sha1(index));
|
||||
index += hex_to_raw_bytes(sha1(index));
|
||||
|
||||
write(join_paths(current_directory, '.git/index'), index)
|
||||
}
|
||||
|
@ -844,7 +1029,7 @@ store_index(['README', 'src/main.scm']);
|
|||
</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
|
||||
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>
|
||||
</section>
|
||||
|
||||
|
@ -869,6 +1054,26 @@ commands.</p>
|
|||
<div id="toc"></div>
|
||||
</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>
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue
Block a user