From bacc1c866e2c30b4939292056d273289757f1124 Mon Sep 17 00:00:00 2001
From: Suzanne Soy <ligo@suzanne.soy>
Date: Fri, 18 Jun 2021 19:37:26 +0100
Subject: [PATCH] better error messages, empty filesystem, WIP on log

---
 git-tutorial.css |  5 ++++
 git-tutorial.js  | 77 ++++++++++++++++++++++++++++++++++++++++++++----
 index.html       | 75 +++++++++++++++++++++++++++++++++++++++++++---
 3 files changed, 147 insertions(+), 10 deletions(-)

diff --git a/git-tutorial.css b/git-tutorial.css
index dac14bc..1726be5 100644
--- a/git-tutorial.css
+++ b/git-tutorial.css
@@ -51,6 +51,7 @@ article#git-tutorial table { width: 77rem; margin-left: calc( ( ( 63rem - 77rem
 #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 table .empty-filesystem { text-align: center !important; color: #444 !important; opacity: 1 !important; border: thin solid black !important; }
 #git-tutorial .specialchar { color: red; word-wrap: normal; }
 #git-tutorial .hex-prefix { color: lightgrey; }
 #git-tutorial .hex { color: brown; }
@@ -90,3 +91,7 @@ article#git-tutorial .onlytoc { display: none; }
 /* 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; }
+#git-tutorial .trivia { opacity: 80%; border: thin solid slategrey; margin: 1em; padding: 0.3em; }
+#git-tutorial .trivia:before { content: "Trivia"; border-bottom: thin solid slategrey; display: block; text-align: center; }
+#git-tutorial .trivia table { min-width: 90%; max-width: 90%; width: 90%; margin-left: auto; margin-right: auto; }
+/*#git-tutorial .trivia table td.cell-contents, #git-tutorial .trivia table th.cell-contents { width: 30%; }*/
\ No newline at end of file
diff --git a/git-tutorial.js b/git-tutorial.js
index 43a15dd..cb49f1d 100644
--- a/git-tutorial.js
+++ b/git-tutorial.js
@@ -472,6 +472,16 @@ function ___filesystem_to_table(fs, previous_filesystem) {
   for (var i = 0; i < entries.length; i++) {
     tbody.append(___format_entry(previous_filesystem, entries[i]));
   }
+  if (entries.length == 0) {
+    var tr_empty = document.createElement('tr');
+    tbody.append(tr_empty);
+
+    var td_empty = document.createElement('td');
+    tr_empty.append(td_empty);
+    td_empty.setAttribute('colspan', '2');
+    td_empty.classList.add('empty-filesystem');
+    td_empty.innerText = "The filesystem is empty."
+  }
   return table;
 }
 function ___filesystem_to_string(fs, just_table, previous_filesystem) {
@@ -514,21 +524,76 @@ function ___copyprintf_click(id) {
     elem.disabled = true;
   }
 }
-var global_filesystem=false;
+var ___script_log_header = ''
+  + 'var ___log = [];\n'
+  + 'var console = (function(real_console) {\n'
+  + '  return {\n'
+  + '    log: function() {\n'
+  + '      ___log[___log.length] = arguments;\n'
+  + '      real_console.log.apply(console, arguments);\n'
+  + '    },\n'
+  + '    assert: real_console.assert,\n'
+  + '  };\n'
+  + '})(window.console);\n'
+  + '\n';
+
+function ___eval_result_to_string(filesystem, previous_filesystem, log) {
+  return '<pre>' + log.map(function(l) { return l.map(function (x) { return x.toString(); }).join(', '); }).join('\n') + '</pre>'
+    + ___filesystem_to_string(filesystem, false, previous_filesystem);
+}
 function ___git_eval(current) {
   document.getElementById('hide-eval-' + current).style.display = '';
-  var script = '';
+  var script = ___script_log_header;
   for (i = 0; i <= current - 1; i++) {
     script += ___textarea_value(___global_editors[i]);
   }
-  script += "\n var ___previous_filesystem = {}; for (k in filesystem) { ___previous_filesystem[k] = filesystem[k]; }\n";
+  script += '\n'
+  + 'var ___previous_filesystem = {};\n'
+  + 'for (k in filesystem) { ___previous_filesystem[k] = filesystem[k]; }\n'
+  + '___log = [];\n';
   script += ___textarea_value(___global_editors[current]);
-  script += "\n document.getElementById('out' + current).innerHTML = ___filesystem_to_string(filesystem, false, ___previous_filesystem); filesystem;";
+  script += '\n'
+  + '"End of the script";\n'
+  + '\n'
+  + '\n'
+  + 'document.getElementById("out" + current).innerHTML = ___eval_result_to_string(filesystem, ___previous_filesystem, ___log);\n'
+  + 'filesystem;\n';
   try {
-    global_filesystem = eval(script);
+    document.getElementById('debug').innerText = script;
+    eval(script);
   } catch (e) {
+    // Stack traces usually include :line:column
+    var rx = /:([0-9][0-9]*):[0-9][0-9]*/g;
+    var linecol = rx.exec(''+e.stack);
+    var line = null;
+    if (linecol && linecol.length > 0) {
+      line=parseInt(linecol[1]);
+    } else {
+      // Some older versions of Firefox and probably some other browsers use just :line
+      var rx = /:([0-9][0-9]*)*/g;
+      var justline = rx.exec(''+e.stack);
+      if (justline && justline.length > 0) {
+        line=parseInt(justline[1], 10);
+      }
+    }
+    if (typeof(line) == 'number') {
+      var lines = script.split('\n');
+      if (line < lines.length) {
+        var from = Math.max(0, line-2);
+        var to = Math.min(lines.length - 1, line+2+1);
+        var showline = ''
+        + 'Possible location of the error: near line ' + line + '\n'
+        + '\n'
+        + lines.slice(from, to).map(function(l, i) { return '' + (from + i) + ': ' + l; }).join('\n')
+        + '\n'
+        + '\n';
+      }
+    } else {
+      var showline = 'Sorry, this tutorial could not pinpoint precisely\nthe location of the error.\n'
+                   + 'The stacktrace below may contain more information.\n'
+    }
     var error = ___specialchars("" + e + "\n\n" + e.stack);
-    document.getElementById('out' + current).innerHTML = '<pre class="error">' + error + '</pre>';
+    document.getElementById('out' + current).innerHTML = '<pre class="error">' + showline + error + '</pre>';
   }
 }
 
diff --git a/index.html b/index.html
index d53b867..fd9fe40 100644
--- a/index.html
+++ b/index.html
@@ -516,7 +516,16 @@ directly to a commit hash like
 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>
+<p>Branches are simple files stored in <code>.git/refs/heads/name-of-the-branch</code>
+and usually contain a hash like
+<span id="example-reference-branch-hash">0123456789abcdef0123456789abcdef01234567</span>.</p>
+
+<p>Tags are identical to branches in terms of representation. It seems that the only difference
+between tags and branches is the behaviour of <code>git checkout</code> and similar commands.
+These commands, as explained in <a href="git-checkout">the section about <code>git checkout</code></a> below,
+normally write <code>ref: refs/heads/name-of-branch</code> in <code>.git/HEAD</code> when
+checking out a branch, but write the hash of the target commit when checking out a tag or
+any other non-branch reference.</p>
 
 <div id="example-reference"></div>
 <script class="example">
@@ -545,6 +554,7 @@ will contain e.g. <code>refs/heads/main</code>.</p>
     var head = 'proj/.git/HEAD';
 
     document.getElementById('example-reference-head-hash').innerText = initial_commit_hash;
+    document.getElementById('example-reference-branch-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 ];
@@ -566,21 +576,77 @@ function trim_newline(s) {
 
 <section id="git-symbolic-ref">
 <h1><code>git symbolic-ref</code></h1>
+<p><code>git symbolic-ref</code> is a low-level command which reads
+(and in the official GIT implementation also writes and updates)
+symbolic references given a path relative to <code>.git/</code>.
+For example, <code>git symbolic-ref HEAD</code> will read the
+contents of the file <code>.git/HEAD</code>, and if that file starts
+with <code>ref: </code>, the rest of the line will be returned.</p>
+
 <textarea>
 function git_symbolic_ref(ref) {
   var ref_file = join_paths(current_directory, '.git/' + ref);
   if (exists(ref_file) && read(ref_file).startsWith('ref: ')) {
-    return trim_newline(read(ref_file)).substr('ref: '.length);
+    var result = trim_newline(read(ref_file)).substr('ref: '.length);
+    var recursive = git_symbolic_ref(result);
+    return recursive || result;
   } else {
     return false;
   }
 }
 </textarea>
+<div class="trivia">
+<p>The official implementation of GIT follows references recursively
+  and returns the <code>path/to/file</code> of the last file of the
+  form <code>ref: path/to/file</code>. In the example below,
+  <code>git symbolic-ref HEAD</code> would
+  <ul>
+    <li>read the file <code>proj/.git/HEAD</code> which contains <code>ref: refs/heads/main</code>,</li>
+    <li>follow that indirection and read the file <code>proj/.git/refs/heads/main</code> which contains <code>ref: refs/heads/other</code></li>
+    <li>follow that indirection and read the file <code>proj/.git/refs/heads/other</code> which contains a hash</li>
+    <li>return the last file path that contained a <code>ref:</code>, i.e. return the string <code>refs/heads/other</code></li>
+  </ul>
+<div id="example-recursive-ref"></div>
+<script class="example">
+  ___example('example-recursive-ref', 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', email:'ada@...', date:new Date(1617120803000), timezoneMinutes: +60},
+      {name:'Ada', email:'ada@...', date:new Date(1617120803000), timezoneMinutes: +60},
+      'Initial commit');
+    var initial_commit = h2f(initial_commit_hash);
+    
+    write('proj/.git/refs/heads/main', 'ref: refs/heads/other\n');
+    var main_branch = 'proj/.git/refs/heads/main';
+
+    git_branch('other', initial_commit_hash, true);
+    var other_branch = 'proj/.git/refs/heads/other';
+
+    git_init_head();
+    var head = 'proj/.git/HEAD';
+
+    document.getElementById('example-reference-head-hash').innerText = initial_commit_hash;
+    document.getElementById('example-reference-branch-hash').innerText = initial_commit_hash;
+
+    var previous_names = [ initial_commit ];
+    var names          = [ initial_commit, main_branch, other_branch, head ];
+    return { filesystem: filesystem, names: names, previous_names: previous_names }
+  });
+</script>
+</div>
 </section>
   
 <section id="git-rev-parse">
-<h1><code>git rev-parse</code></h1>  
+<h1><code>git rev-parse</code></h1>
 <textarea>
+console.log('hello', 'world', 3);
 function git_rev_parse(ref) {
   var symbolic_ref_target = git_symbolic_ref(ref);
   if (symbolic_ref_target) {
@@ -1052,11 +1118,12 @@ commands.</p>
 </section>
 
 <div id="toc"></div>
+<pre id="debug"></pre>
 </article>
 
 <script>
   (function() {
-      var script = '';
+      var script = ___script_log_header;
       var ta = document.getElementsByTagName('textarea');
       for (var j = 0; j < ta.length; j++) {
         if (ta[j] == document.getElementById('playground-reset')) {