From f93038fbee1631b95922b0742e0fd00fa8dae02e Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 27 Jan 2017 15:25:26 +0200 Subject: Add notion of phase, enforce --- build2/algorithm | 9 +++++++ build2/algorithm.cxx | 34 ++++++++++++++++++++++++ build2/algorithm.ixx | 12 +++++++++ build2/cc/common.cxx | 5 +++- build2/config/operation.cxx | 3 +++ build2/context | 44 ++++++++++++++++++++++++++++--- build2/context.cxx | 2 ++ build2/dist/operation.cxx | 44 ++++++++++++++++--------------- build2/file.cxx | 9 ++++--- build2/operation.cxx | 36 ++++++++++++++++---------- build2/search.cxx | 27 ++++++++++++++----- build2/target.cxx | 45 +++++++++++++++++--------------- build2/test/common | 2 +- build2/test/common.cxx | 22 ++++++++++------ build2/test/rule.cxx | 6 ++++- build2/test/script/script.cxx | 5 +++- build2/variable | 8 +++++- build2/variable.cxx | 47 +++++++++++++++++++++++++++------- tests/test/config-test/testscript | 6 ++--- unit-tests/function/buildfile | 1 - unit-tests/lexer/buildfile | 8 +++++- unit-tests/scheduler/buildfile | 7 ++++- unit-tests/test/script/lexer/buildfile | 7 ++++- 23 files changed, 292 insertions(+), 97 deletions(-) diff --git a/build2/algorithm b/build2/algorithm index 552ea7f..5cd31fe 100644 --- a/build2/algorithm +++ b/build2/algorithm @@ -65,6 +65,15 @@ namespace build2 target& search (name, scope&); + // As above but only search for an already existing target. Unlike the + // above version, this one can be called during the execute phase. + // + // Note that currently we return NULL for project-qualified names and + // unknown target types. + // + target* + search_existing (const name&, scope&, const dir_path& out = dir_path ()); + // Match and apply a rule to the action/target with ambiguity detection. // Increment the target's dependents count, which means that you should call // this function with the intent to also call execute(). In case of diff --git a/build2/algorithm.cxx b/build2/algorithm.cxx index 6c812f3..1e7a1b8 100644 --- a/build2/algorithm.cxx +++ b/build2/algorithm.cxx @@ -22,6 +22,8 @@ namespace build2 target& search (const prerequisite_key& pk) { + assert (phase == run_phase::search_match); + // If this is a project-qualified prerequisite, then this is import's // business. // @@ -37,6 +39,8 @@ namespace build2 target& search (name n, scope& s) { + assert (phase == run_phase::search_match); + optional ext; const target_type* tt (s.find_target_type (n, ext)); @@ -52,6 +56,36 @@ namespace build2 return search (*tt, n.dir, dir_path (), n.value, ext, &s, n.proj); } + target* + search_existing (const name& cn, scope& s, const dir_path& out) + { + assert (phase == run_phase::search_match || phase == run_phase::execute); + + // We don't handle this for now. + // + if (cn.qualified ()) + return nullptr; + + name n (cn); + optional ext; + const target_type* tt (s.find_target_type (n, ext)); + + // For now we treat an unknown target type as an unknown target. Seems + // logical. + // + if (tt == nullptr) + return nullptr; + + if (!n.dir.empty ()) + n.dir.normalize (false, true); // Current dir collapses to an empty one. + + // @@ OUT: for now we assume the prerequisite's out is undetermined. + // Would need to pass a pair of names. + // + return search_existing_target ( + prerequisite_key {n.proj, {tt, &n.dir, &out, &n.value, ext}, &s}); + } + pair match_impl (slock& ml, action a, target& t, bool apply, const rule* skip) { diff --git a/build2/algorithm.ixx b/build2/algorithm.ixx index 7da189d..ec06ddf 100644 --- a/build2/algorithm.ixx +++ b/build2/algorithm.ixx @@ -11,6 +11,8 @@ namespace build2 inline target& search (prerequisite& p) { + assert (phase == run_phase::search_match); + if (p.target == nullptr) p.target = &search (p.key ()); @@ -56,6 +58,8 @@ namespace build2 inline void match (slock& ml, action a, target& t) { + assert (phase == run_phase::search_match); + if (!t.recipe (a)) match_impl (ml, a, t, true); @@ -72,6 +76,8 @@ namespace build2 { // text << "U " << t << ": " << t.dependents << " " << dependency_count; + assert (phase == run_phase::search_match); + //@@ MT // assert (t.dependents != 0 && dependency_count != 0); @@ -82,6 +88,8 @@ namespace build2 inline void match_only (slock& ml, action a, target& t) { + assert (phase == run_phase::search_match); + if (!t.recipe (a)) match_impl (ml, a, t, false); } @@ -89,6 +97,8 @@ namespace build2 inline pair match_delegate (slock& ml, action a, target& t, const rule& r) { + assert (phase == run_phase::search_match); + auto rp (match_impl (ml, a, t, false, &r)); const match_result& mr (rp.second); return make_pair (rp.first->apply (ml, mr.recipe_action, t), @@ -101,6 +111,8 @@ namespace build2 inline group_view resolve_group_members (slock& ml, action a, target& g) { + assert (phase == run_phase::search_match); + group_view r (g.group_members (a)); return r.members != nullptr ? r : resolve_group_members_impl (ml, a, g); } diff --git a/build2/cc/common.cxx b/build2/cc/common.cxx index b941e6e..aa0a45b 100644 --- a/build2/cc/common.cxx +++ b/build2/cc/common.cxx @@ -402,7 +402,10 @@ namespace build2 // Search for an existing target with this name "as if" it was a // prerequisite. // - xt = &search (move (n), s); + xt = search_existing (n, s); + + if (xt == nullptr) + fail << "unable to find library " << n; } else { diff --git a/build2/config/operation.cxx b/build2/config/operation.cxx index 5339ef9..63e6d9b 100644 --- a/build2/config/operation.cxx +++ b/build2/config/operation.cxx @@ -387,6 +387,7 @@ namespace build2 set_current_oif (*oif); dependency_count = 0; + phase_guard pg (run_phase::search_match); match (ml, action (configure_id, id), t); } @@ -566,6 +567,8 @@ namespace build2 set projects; + // Note: doing everything in the load phase. + // for (void* v: ts) { scope& root (*static_cast (v)); diff --git a/build2/context b/build2/context index 7f533ca..cf5483e 100644 --- a/build2/context +++ b/build2/context @@ -14,14 +14,43 @@ namespace build2 { - // Top-level model (internal state) mutex and its per-thread shared lock. + // In order to perform each operation the build system goes through the + // following phases: // - // - model_lock is NULL during serial execution, all changes are a go - // - model_lock is not NULL during parallel execution, append-only changes - // - parallel execution starts with a shared lock by creating model_slock + // load - load the buildfiles + // search & match - search prerequisites and match rules + // execute - execute the matched rule + // + // The phase can only be changed during serial or exclusive execution + // (see below). + // + extern enum class run_phase {load, search_match, execute} phase; + + // The build system model (internal state) is protected at the top level by + // the model mutex. During serial execution the model mutex is unlocked. // extern shared_mutex model; + // Parallel execution always starts with acquiring a shared model lock (by + // creating model_slock; see below). Pointers to these locks are cached in + // the model_lock TLS variable (which is NULL during serial execution). + // + // The build system starts with a "serial load" phase and then continues + // with parallel search & match and execute. Search & match, however, can be + // interrupted with an "exclusive load" by re-locking the shared lock as + // exclusive, changing the phase, and loading additional buildfiles. + // + // Serial load can perform arbitrary changes to the model. Exclusive load, + // however, can only perform "pure appends". That is, it can create new + // "nodes" (variables, scopes, etc) but not change already existing nodes + // or invalidate any references to such (the idea here is that one should + // be able to load additional buildfiles as long as they don't interfere + // with the existing build state). + // + // @@ MT: do we really have to hold shared lock during execute? + // @@ MT: we can also interrupt load s&m with execute -- neither handled + // nor documented. + // extern #ifdef __cpp_thread_local thread_local @@ -30,6 +59,13 @@ namespace build2 #endif slock* model_lock; + struct phase_guard + { + explicit phase_guard (run_phase p): o (phase) {phase = p;} + ~phase_guard () {phase = o;} + run_phase o; + }; + // A shared model lock. If there is already an instance of model_slock in // this thread, then the new instance simply references it (asserting that // it is locked). diff --git a/build2/context.cxx b/build2/context.cxx index e3f728e..0f13f27 100644 --- a/build2/context.cxx +++ b/build2/context.cxx @@ -23,6 +23,8 @@ using namespace butl; namespace build2 { + run_phase phase = run_phase::load; + shared_mutex model; #ifdef __cpp_thread_local diff --git a/build2/dist/operation.cxx b/build2/dist/operation.cxx index 06df0be..c7fe83b 100644 --- a/build2/dist/operation.cxx +++ b/build2/dist/operation.cxx @@ -115,31 +115,35 @@ namespace build2 if (verb >= 6) dump (a); - scheduler::atomic_count task_count (0); { - model_slock ml; + phase_guard pg (run_phase::search_match); - for (void* v: ts) + scheduler::atomic_count task_count (0); { - target& t (*static_cast (v)); - - if (rs != t.base_scope ().root_scope ()) - fail << "target " << t << " is from a different project" << - info << "one dist() meta-operation can handle one project" << - info << "consider using several dist() meta-operations"; - - l5 ([&]{trace << diag_doing (a, t);}); - - sched.async (task_count, - [a] (target& t) - { - model_slock ml; - build2::match (ml, a, t); // @@ MT exception. - }, - ref (t)); + model_slock ml; + + for (void* v: ts) + { + target& t (*static_cast (v)); + + if (rs != t.base_scope ().root_scope ()) + fail << "target " << t << " is from a different project" << + info << "one dist() meta-operation can handle one project" << + info << "consider using several dist() meta-operations"; + + l5 ([&]{trace << diag_doing (a, t);}); + + sched.async (task_count, + [a] (target& t) + { + model_slock ml; + build2::match (ml, a, t); // @@ MT exception. + }, + ref (t)); + } } + sched.wait (task_count); } - sched.wait (task_count); if (verb >= 6) dump (a); diff --git a/build2/file.cxx b/build2/file.cxx index 9440738..72d2807 100644 --- a/build2/file.cxx +++ b/build2/file.cxx @@ -892,6 +892,8 @@ namespace build2 // over anything that we may discover. In particular, we will prefer it // over any bundled subprojects. // + auto& vp (var_pool.rw (iroot)); + for (;;) // Break-out loop. { string n ("config.import." + proj); @@ -901,8 +903,7 @@ namespace build2 // Note: overridable variable with path auto-completion. // { - const variable& var ( - var_pool.rw (ibase).insert (n, true)); + const variable& var (vp.insert (n, true)); if (auto l = iroot[var]) { @@ -931,9 +932,9 @@ namespace build2 // if (!target.value.empty ()) { - auto lookup = [&iroot, &loc] (string name) -> path + auto lookup = [&iroot, &vp, &loc] (string name) -> path { - const variable& var (var_pool.rw (iroot).insert (name, true)); + const variable& var (vp.insert (name, true)); path r; if (auto l = iroot[var]) diff --git a/build2/operation.cxx b/build2/operation.cxx index 0163e04..0ade4bb 100644 --- a/build2/operation.cxx +++ b/build2/operation.cxx @@ -74,6 +74,8 @@ namespace build2 { tracer trace ("search"); + phase_guard pg (run_phase::search_match); + auto i (targets.find (tk, trace)); if (i == targets.end ()) fail (l) << "unknown target " << tk; @@ -89,25 +91,29 @@ namespace build2 if (verb >= 6) dump (a); - scheduler::atomic_count task_count (0); { - model_slock ml; + phase_guard pg (run_phase::search_match); - for (void* vt: ts) + scheduler::atomic_count task_count (0); { - target& t (*static_cast (vt)); - l5 ([&]{trace << "matching " << t;}); - - sched.async (task_count, - [a] (target& t) - { - model_slock ml; - match (ml, a, t); // @@ MT exception handling. - }, - ref (t)); + model_slock ml; + + for (void* vt: ts) + { + target& t (*static_cast (vt)); + l5 ([&]{trace << "matching " << t;}); + + sched.async (task_count, + [a] (target& t) + { + model_slock ml; + match (ml, a, t); // @@ MT exception handling. + }, + ref (t)); + } } + sched.wait (task_count); } - sched.wait (task_count); if (verb >= 6) dump (a); @@ -118,6 +124,8 @@ namespace build2 { tracer trace ("execute"); + phase_guard pg (run_phase::execute); + // Execute collecting postponed targets (to be re-examined later). // Do it in reverse order if the execution mode is 'last'. // diff --git a/build2/search.cxx b/build2/search.cxx index ef4814d..65eb2fd 100644 --- a/build2/search.cxx +++ b/build2/search.cxx @@ -31,7 +31,7 @@ namespace build2 d = *tk.dir; // Already normalized. else { - d = pk.scope->out_path (); + d = tk.out->empty () ? pk.scope->out_path () : pk.scope->src_path (); if (!tk.dir->empty ()) { @@ -51,12 +51,27 @@ namespace build2 // // relative The out directory was specified using @-syntax as relative (to // the prerequisite's scope) and we need to complete it similar - // to how we complete the relative dir above. This is @@ OUT TODO. - // - // What if we have user-supplied out but it is in-src build. Shouldn't we - // drop it? + // to how we complete the relative dir above. // - auto i (targets.find (*tk.type, d, *tk.out, *tk.name, tk.ext, trace)); + dir_path o; + if (!tk.out->empty ()) + { + if (tk.out->absolute ()) + o = *tk.out; // Already normalized. + else + { + o = pk.scope->out_path (); + o /= *tk.out; + o.normalize (); + } + + // Drop out if it is the same as src (in-src build). + // + if (o == d) + o.clear (); + } + + auto i (targets.find (*tk.type, d, o, *tk.name, tk.ext, trace)); if (i == targets.end ()) return 0; diff --git a/build2/target.cxx b/build2/target.cxx index 81d613f..24bd246 100644 --- a/build2/target.cxx +++ b/build2/target.cxx @@ -625,36 +625,41 @@ namespace build2 // target_set). Also, a buildfile could load from a directory that is // not a subdirectory of out_base. So for now we just assume that this // is so. And so it is. - - // Relock for exclusive access. // - rlock rl (model_lock); - - pair sp (switch_scope (*s.root_scope (), out_base)); - - if (sp.second != nullptr) // Ignore scopes out of any project. + bool retest (false); { - scope& base (sp.first); - scope& root (*sp.second); + // Relock for exclusive access and change to the load phase. + // + rlock rl (model_lock); + phase_guard pg (run_phase::load); - path bf (base.src_path () / "buildfile"); + pair sp (switch_scope (*s.root_scope (), out_base)); - if (exists (bf)) + if (sp.second != nullptr) // Ignore scopes out of any project. { - l5 ([&]{trace << "loading buildfile " << bf << " for " << pk;}); + scope& base (sp.first); + scope& root (*sp.second); - if (source_once (root, base, bf, root)) - { - // If we loaded the buildfile, examine the target again. - // - if (t == nullptr) - t = search_existing_target (pk); + path bf (base.src_path () / "buildfile"); - if (t != nullptr && !t->implied) - return t; + if (exists (bf)) + { + l5 ([&]{trace << "loading buildfile " << bf << " for " << pk;}); + retest = source_once (root, base, bf, root); } } } + + // If we loaded the buildfile, examine the target again. + // + if (retest) + { + if (t == nullptr) + t = search_existing_target (pk); + + if (t != nullptr && !t->implied) + return t; + } } fail << "no explicit target for prerequisite " << pk << diff --git a/build2/test/common b/build2/test/common index e9213b6..d3678c8 100644 --- a/build2/test/common +++ b/build2/test/common @@ -21,7 +21,7 @@ namespace build2 const names* test_ = nullptr; // The config.test value if any. scope* root_ = nullptr; // The root scope for target resolution. - // Return true if the specified alias target should pass-through to it + // Return true if the specified alias target should pass-through to its // prerequisites. // bool diff --git a/build2/test/common.cxx b/build2/test/common.cxx index 66be9d8..1b4c194 100644 --- a/build2/test/common.cxx +++ b/build2/test/common.cxx @@ -136,14 +136,19 @@ namespace build2 r = d.sub (n->dir); else // First quickly and cheaply weed out names that cannot possibly - // match. Only then search for a target as if it was a prerequisite, - // which can be expensive. + // match. Only then search for a target (as if it was a + // prerequisite), which can be expensive. + // + // We cannot specify an src target in config.test since we used + // the pair separator for ids. As a result, we search for both + // out and src targets. // r = - t.name == n->value && // Name matches. - tt.name == n->type && // Target type matches. - d == n->dir && // Directory matches. - search (*n, *root_) == t; + t.name == n->value && // Name matches. + tt.name == n->type && // Target type matches. + d == n->dir && // Directory matches. + (search_existing (*n, *root_) == &t || + search_existing (*n, *root_, d) == &t); if (r) break; @@ -189,8 +194,9 @@ namespace build2 r = t.name == n->value && tt.name == n->type && - d == n->dir && - search (*n, *root_) == t; + d == n->dir && + (search_existing (*n, *root_) == &t || + search_existing (*n, *root_, d) == &t); if (!r) continue; // Not our target. diff --git a/build2/test/rule.cxx b/build2/test/rule.cxx index 17821a4..6b06dca 100644 --- a/build2/test/rule.cxx +++ b/build2/test/rule.cxx @@ -589,7 +589,11 @@ namespace build2 // // @@ OUT: what if this is a @-qualified pair or names? // - t = &search (*n, tt.base_scope ()); + t = search_existing (*n, tt.base_scope ()); + + if (t == nullptr) + fail << "invalid test executable override: unknown target: '" + << n << "'"; } } else diff --git a/build2/test/script/script.cxx b/build2/test/script/script.cxx index 342cae9..bd13845 100644 --- a/build2/test/script/script.cxx +++ b/build2/test/script/script.cxx @@ -549,7 +549,10 @@ namespace build2 // // @@ OUT: what if this is a @-qualified pair or names? // - t = &search (*n, tt.base_scope ()); + t = search_existing (*n, tt.base_scope ()); + + if (t == nullptr) + fail << "unknown target '" << n << "' in test variable"; } } else diff --git a/build2/variable b/build2/variable index e296e37..d31d20f 100644 --- a/build2/variable +++ b/build2/variable @@ -781,7 +781,7 @@ namespace build2 // Variable pool. // - // Protected by the model mutex. + // The global version is protected by the model mutex. // class variable_pool { @@ -856,6 +856,8 @@ namespace build2 void clear () {map_.clear ();} + variable_pool (): variable_pool (false) {} + // Proof of lock for RW access. // variable_pool& @@ -902,6 +904,10 @@ namespace build2 return r; } + explicit + variable_pool (bool global): global_ (global) {} + + bool global_; map map_; }; diff --git a/build2/variable.cxx b/build2/variable.cxx index ec72fdc..bbe08d0 100644 --- a/build2/variable.cxx +++ b/build2/variable.cxx @@ -6,6 +6,7 @@ #include // memcmp() +#include #include using namespace std; @@ -908,6 +909,8 @@ namespace build2 const variable& variable_pool:: insert (string n) { + assert (!global_ || phase == run_phase::load); + // We are not overriding anything so skip the insert_() checks. // auto p ( @@ -928,6 +931,8 @@ namespace build2 const variable_visibility* v, const bool* o) { + assert (!global_ || phase == run_phase::load); + auto p ( insert ( variable { @@ -940,9 +945,39 @@ namespace build2 if (!p.second) { + // Check overridability (all overrides, if any, should already have + // been entered (see context.cxx:reset()). + // + if (r.override != nullptr && (o == nullptr || !*o)) + fail << "variable " << r.name << " cannot be overridden"; + + bool ut (t != nullptr && r.type != t); + bool uv (v != nullptr && r.visibility != *v); + + // In the global pool existing variables can only be updated during + // serial load. + // + /* + @@ MT + + if (global_) + { + //assert (!(ut || uv) || model_lock == nullptr); + + if (model_lock != nullptr) + { + if (ut) + text << r.name << " type update during exclusive load"; + + if (uv) + text << r.name << " visibility update during exclusive load"; + } + } + */ + // Update type? // - if (t != nullptr && r.type != t) + if (ut) { assert (r.type == nullptr); const_cast (r).type = t; // Not changing the key. @@ -953,23 +988,17 @@ namespace build2 // were set, in which case the variable will be entered with the // default visibility. // - if (v != nullptr && r.visibility != *v) + if (uv) { assert (r.visibility == variable_visibility::normal); // Default. const_cast (r).visibility = *v; // Not changing the key. } - - // Check overridability (all overrides, if any, should already have - // been entered (see context.cxx:reset()). - // - if (r.override != nullptr && (o == nullptr || !*o)) - fail << "variable " << r.name << " cannot be overridden"; } return r; } - variable_pool variable_pool::instance; + variable_pool variable_pool::instance (true); const variable_pool& variable_pool::cinstance = variable_pool::instance; const variable_pool& var_pool = variable_pool::cinstance; diff --git a/tests/test/config-test/testscript b/tests/test/config-test/testscript index 2c262b7..833a33a 100644 --- a/tests/test/config-test/testscript +++ b/tests/test/config-test/testscript @@ -21,7 +21,7 @@ EOI +cat <=proj/buildfile d = tests/ units/ ./: $d -include $d +#include $d EOI # tests/ - as a subproject @@ -37,7 +37,7 @@ EOI +cat <=proj/tests/buildfile d = script/ ./: $d -include $d +#include $d EOI # tests/script - scripted test @@ -66,7 +66,7 @@ EOI +cat <=proj/units/buildfile d = simple/ script/ ./: $d test{testscript} -include $d +#include $d EOI +cat <=proj/units/testscript echo 'units' >| diff --git a/unit-tests/function/buildfile b/unit-tests/function/buildfile index 3631958..4d2862c 100644 --- a/unit-tests/function/buildfile +++ b/unit-tests/function/buildfile @@ -13,7 +13,6 @@ functions-builtin functions-path functions-process-path functions-string \ functions-target-triplet algorithm search dump filesystem scheduler \ config/{utility init operation} - exe{driver}: cxx{driver} ../../build2/cxx{$src} $libs test{call syntax} include ../../build2/ diff --git a/unit-tests/lexer/buildfile b/unit-tests/lexer/buildfile index 3ad0ac6..f37c7cf 100644 --- a/unit-tests/lexer/buildfile +++ b/unit-tests/lexer/buildfile @@ -4,8 +4,14 @@ #@@ Temporary until we get utility library support. # +if ($cxx.target.class != "windows") + cxx.libs += -lpthread import libs = libbutl%lib{butl} -src = token lexer diagnostics utility variable name b-options types-parsers +src = token lexer diagnostics utility variable name b-options types-parsers \ +context scope parser target operation rule prerequisite file module function \ +functions-builtin functions-path functions-process-path functions-string \ +functions-target-triplet algorithm search dump filesystem scheduler \ +config/{utility init operation} exe{driver}: cxx{driver} ../../build2/cxx{$src} $libs \ test{comment eval quoting} diff --git a/unit-tests/scheduler/buildfile b/unit-tests/scheduler/buildfile index 2e79c16..056930e 100644 --- a/unit-tests/scheduler/buildfile +++ b/unit-tests/scheduler/buildfile @@ -7,7 +7,12 @@ if ($cxx.target.class != "windows") cxx.libs += -lpthread import libs = libbutl%lib{butl} -src = scheduler diagnostics utility variable name b-options types-parsers +src = token lexer diagnostics utility variable name b-options types-parsers \ +context scope parser target operation rule prerequisite file module function \ +functions-builtin functions-path functions-process-path functions-string \ +functions-target-triplet algorithm search dump filesystem scheduler \ +config/{utility init operation} + exe{driver}: cxx{driver} ../../build2/cxx{$src} $libs diff --git a/unit-tests/test/script/lexer/buildfile b/unit-tests/test/script/lexer/buildfile index 43df2e3..aaeadc9 100644 --- a/unit-tests/test/script/lexer/buildfile +++ b/unit-tests/test/script/lexer/buildfile @@ -4,9 +4,14 @@ #@@ Temporary until we get utility library support. # +if ($cxx.target.class != "windows") + cxx.libs += -lpthread import libs = libbutl%lib{butl} src = token lexer diagnostics utility variable name b-options types-parsers \ -test/script/{token lexer} +context scope parser target operation rule prerequisite file module function \ +functions-builtin functions-path functions-process-path functions-string \ +functions-target-triplet algorithm search dump filesystem scheduler \ +config/{utility init operation} test/script/{token lexer} exe{driver}: cxx{driver} ../../../../build2/cxx{$src} $libs \ test{command-line first-token second-token command-expansion variable-line \ -- cgit v1.1