From 727e4ab31aa6d1a754711d4cd29dbcefae2e952a Mon Sep 17 00:00:00 2001 From: Kristian Lyngstol Date: Mon, 21 Mar 2016 20:11:55 +0100 Subject: NMS: NMS Public --- web/nms-public.gathering.org/js/nms.js | 674 +++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 web/nms-public.gathering.org/js/nms.js (limited to 'web/nms-public.gathering.org/js/nms.js') diff --git a/web/nms-public.gathering.org/js/nms.js b/web/nms-public.gathering.org/js/nms.js new file mode 100644 index 0000000..fcb8437 --- /dev/null +++ b/web/nms-public.gathering.org/js/nms.js @@ -0,0 +1,674 @@ +"use strict"; +var nms = { + stats:{}, // Various internal stats + get nightMode() { return this._nightMode; }, + set nightMode(val) { if (val != this._nightMode) { this._nightMode = val; setNightMode(val); } }, + /* + * FIXME: This should be slightly smarter. + */ + _now: false, + get now() { return this._now }, + set now(v) { this._now = v; nmsData.now = v; }, + /* + * Various setInterval() handlers. See nmsTimer() for how they are + * used. + * + * FIXME: Should just stop using these. + */ + timers: { + playback:false, + }, + + menuShowing:true, + /* + * This is a list of nms[x] variables that we store in our + * settings-cookie when altered and restore on load. + */ + settingsList:[ + 'nightMode', + 'menuShowing' + ], + keyBindings:{ + '-':toggleMenu, + 'n':toggleNightMode, + '1':setMapModeFromN, + '2':setMapModeFromN, + '3':setMapModeFromN, + '4':setMapModeFromN, + '5':setMapModeFromN, + '6':setMapModeFromN, + '7':setMapModeFromN, + 'h':moveTimeFromKey, + 'j':moveTimeFromKey, + 'k':moveTimeFromKey, + 'l':moveTimeFromKey, + 'p':moveTimeFromKey, + 'r':moveTimeFromKey, + 'Escape':hideWindow, + '?':toggleHelp + }, + /* + * Playback controllers and variables + */ + playback:{ + startTime: false, + stopTime: false, + playing: false, + replayTime: 0, + replayIncrement: 60 * 60 + } +}; + +/* + * Returns a handler object. + * + * This might seem a bit much for 'setInterval()' etc, but it's really more + * about self-documentation and predictable ways of configuring timers. + */ +function nmsTimer(handler, interval, name, description) { + this.handler = handler; + this.handle = false; + this.interval = parseInt(interval); + this.name = name; + this.description = description; + this.start = function() { + if (this.handle) { + this.stop(); + } + this.handle = setInterval(this.handler,this.interval); + }; + this.stop = function() { + if (this.handle) + clearInterval(this.handle); + this.handle = false; + }; + + this.setInterval = function(interval) { + var started = this.handle == false ? false : true; + this.stop(); + this.interval = parseInt(interval); + if (started) + this.start(); + }; +} + + +/* + * Convenience function that doesn't support huge numbers, and it's easier + * to comment than to fix. But not really, but I'm not fixing it anyway. + */ +function byteCount(bytes) { + var units = ['', 'K', 'M', 'G', 'T', 'P']; + var i = 0; + while (bytes > 1024) { + bytes = bytes / 1024; + i++; + } + return bytes.toFixed(1) + units[i]; +} + +/* + * Definitely not a way to toggle night mode. Does something COMPLETELY + * DIFFERENT. + */ +function toggleNightMode() +{ + nms.nightMode = !nms.nightMode; + saveSettings(); +} + +/* + * Parse 'now' from user-input. + * + * Should probably just use stringToEpoch() instead, but alas, not yet. + */ +function parseNow(now) +{ + if (Date.parse(now)) { + // Adjust for timezone when converting from epoch (UTC) to string (local) + var d = new Date(now); + var timezoneOffset = d.getTimezoneOffset() * -60000; + var d = new Date(Date.parse(now) - timezoneOffset); + var str = d.getFullYear() + "-" + ("00" + (parseInt(d.getMonth())+1)).slice(-2) + "-" + ("00" + d.getDate()).slice(-2) + "T"; + str += ("00" + d.getHours()).slice(-2) + ":" + ("00" + d.getMinutes()).slice(-2) + ":" + ("00" + d.getSeconds()).slice(-2); + return str; + + } + if (now == "") + return ""; + return false; +} + +/* + * Convert back and forth between epoch. + * + * There's no particular reason why I use seconds instead of javascript + * microseconds, except to leave the mark of a C coder on this javascript + * project. + */ +function stringToEpoch(t) +{ + var foo = t.toString(); +// foo = foo.replace('T',' '); + var ret = new Date(Date.parse(foo)); + return parseInt(parseInt(ret.valueOf()) / 1000); +} + +/* + * Have to pad with zeroes to avoid "17:5:0" instead of the conventional + * and more readable "17:05:00". I'm sure there's a better way, but this + * works just fine. + */ +function epochToString(t) +{ + // Adjust for timezone when converting from epoch (UTC) to string (local) + var d = new Date(parseInt(t) * parseInt(1000)); + var timezoneOffset = d.getTimezoneOffset() * -60; + t = t - timezoneOffset; + + var d = new Date(parseInt(t) * parseInt(1000)); + var str = d.getFullYear() + "-"; + if (parseInt(d.getMonth()) < 9) + str += "0"; + str += (parseInt(d.getMonth())+1) + "-"; + if (d.getDate() < 10) + str += "0"; + str += d.getDate() + "T"; + if (d.getHours() < 10) + str += "0"; + str += d.getHours() + ":"; + if (d.getMinutes() < 10) + str += "0"; + str += d.getMinutes() + ":"; + if (d.getSeconds() < 10) + str += "0"; + str += d.getSeconds(); + + return str; +} + +function localEpochToString(t) { + var d = new Date(parseInt(t) * parseInt(1000)); + var timezoneOffset = d.getTimezoneOffset() * -60; + t = t + timezoneOffset; + + return epochToString(t); +} + +/* + * Start replaying historical data. + */ +nms.playback.startReplay = function(startTime,stopTime) { + if(!startTime || !stopTime) + return false; + + nms.playback.pause(); + nms.playback.startTime = stringToEpoch(startTime); + nms.playback.stopTime = stringToEpoch(stopTime); + nms.now = epochToString(nms.playback.startTime); + nms.playback.play(); +} + +/* + * Pause playback + */ +nms.playback.pause = function() { + nms.timers.playback.stop(); + nms.playback.playing = false; +} + +/* + * Start playback + */ +nms.playback.play = function() { + nms.playback.tick(); + nms.timers.playback.start(); + nms.playback.playing = true; +} + +/* + * Toggle playback + */ +nms.playback.toggle = function() { + if(nms.playback.playing) { + nms.playback.pause(); + } else { + nms.playback.play(); + } +} + +/* + * Jump to place in time + */ +nms.playback.setNow = function(now) { + var now = parseNow(now); + nms.now = now; + + nms.playback.stopTime = false; + nms.playback.startTime = false; + nms.playback.tick(); +} + +/* + * Step forwards or backwards in timer + */ +nms.playback.stepTime = function(n) +{ + var now = getNowEpoch(); + var newtime = parseInt(now) + parseInt(n); + nms.now = epochToString(parseInt(newtime)); + + if(!nms.playback.playing) + nms.playback.tick(); +} + +/* + * Ticker to trigger updates, and advance time if replaying + * + * This is run on a timer (nms.timers.tick) every second while unpaused + */ +nms.playback.tick = function() +{ + nms.playback.replayTime = getNowEpoch(); + + // If outside start-/stopTime, remove limits and pause playback + if (nms.playback.stopTime && (nms.playback.replayTime >= nms.playback.stopTime || nms.playback.replayTime < nms.playback.startTime)) { + nms.playback.stopTime = false; + nms.playback.startTime = false; + nms.playback.pause(); + return; + } + + // If past actual datetime, go live + if (nms.playback.replayTime > parseInt(Date.now() / 1000)) { + nms.now = false; + } + + // If we are still replaying, advance time + if(nms.now !== false && nms.playback.playing) { + nms.playback.stepTime(nms.playback.replayIncrement); + } +} + +/* + * Helper function for safely getting a valid now-epoch + */ +function getNowEpoch() { + if (nms.now && nms.now != 0) + return stringToEpoch(nms.now); + else + return parseInt(Date.now() / 1000); +} + +/* + * There are 4 legend-bars. This is a helper-function to set the color and + * description/name for each one. Used from handler init-functions. + * + * FIXME: Should be smarter, possibly use a canvas-writer so we can get + * proper text (e.g.: not black text on dark blue). + */ +function setLegend(x,color,name) +{ + var el = document.getElementById("legend-" + x); + el.style.background = color; + el.title = name; + el.textContent = name; +} + +/* + * Change map handler (e.g., change from uplink map to ping map) + */ +function setUpdater(fo) +{ + nmsMap.reset(); + nmsData.unregisterHandlerWildcard("mapHandler"); + try { + fo.init(); + } catch (e) { + /* + * This can happen typically on initial load where the data + * hasn't been retrieved yet. Instead of breaking the + * entire init-process, just bail out here. + */ + console.log("Possibly broken handler: " + fo.name); + console.log(e); + } + var foo = document.getElementById("updater_name"); + foo.innerHTML = fo.name + " "; + document.location.hash = fo.tag; +} + +function toggleLayer(layer) { + var l = document.getElementById(layer); + if (l.style.display == 'none') + l.style.display = ''; + else + l.style.display = 'none'; +} + +function commentInactive(id) +{ + commentChange(id,"inactive"); +} + +function commentPersist(id) +{ + commentChange(id,"persist"); +} + +function commentDelete(id) +{ + var r = confirm("Really delete comment? (Delted comments are still stored in the database, but never displayed)"); + if (r == true) { + commentChange(id,"delete"); + } +} + +/* + * FIXME: Neither of these two handle failures in any way, shape or form. + * Nor do they really give user-feedback. They work, but only by magic. + */ +function commentChange(id,state) +{ + var myData = { + comment:id, + state:state + }; + myData = JSON.stringify(myData); + $.ajax({ + type: "POST", + url: "/api/private/comment-change", + dataType: "text", + data:myData, + success: function (data, textStatus, jqXHR) { + nmsData.invalidate("comments"); + } + }); +} + +function addComment(sw,comment) +{ + var myData = { + switch:sw, + comment:comment + }; + myData = JSON.stringify(myData); + $.ajax({ + type: "POST", + url: "/api/private/comment-add", + dataType: "text", + data:myData, + success: function (data, textStatus, jqXHR) { + nmsData.invalidate("comments"); + } + }); +} + + + +/* + * Returns true if the coordinates (x,y) is inside the box defined by + * box.{x,y,w.h} (e.g.: placement of a switch). + */ +function isIn(box, x, y) +{ + if ((x >= box.x) && (x <= (box.x + box.width)) && (y >= box.y) && (y <= (box.y + box.height))) { + return true; + } + return false; + +} + +/* + * Return the name of the switch found at coordinates (x,y), or 'undefined' + * if none is found. + */ +function findSwitch(x,y) { + x = parseInt(parseInt(x) / nmsMap.scale); + y = parseInt(parseInt(y) / nmsMap.scale); + + for (var v in nmsData.switches.switches) { + if(isIn(nmsData.switches.switches[v]['placement'],x,y)) { + return v; + } + } + return undefined; +} + +/* + * Set night mode to whatever 'toggle' is. + * + * Changes background and nav-bar, then leaves the rest to nmsMap. + */ +function setNightMode(toggle) { + nms.nightMode = toggle; + var body = document.getElementById("body"); + body.style.background = toggle ? "black" : "white"; + var nav = document.getElementsByTagName("nav")[0]; + if (toggle) { + nav.classList.add('navbar-inverse'); + } else { + nav.classList.remove('navbar-inverse'); + } + nmsMap.setNightMode(toggle); +} + +/* + * Boot up "fully fledged" NMS. + * + * This can be re-written to provide different looks and feels but using + * the same framework. Or rather: that's the goal. We're not quite there + * yet. + */ +function initNMS() { + nms.timers.playback = new nmsTimer(nms.playback.tick, 1000, "Playback ticker", "Handler used to advance time"); + + // Public + nmsData.registerSource("ping", "/api/public/ping"); + nmsData.registerSource("switches","/api/public/switches"); + nmsData.registerSource("switchstate","/api/public/switch-state"); + nmsData.registerSource("dhcpsummary","/api/public/dhcp-summary"); + + // This is a magic dummy-source, it's purpose is to give a unified + // way to get ticks every second. It is mainly meant to allow map + // handlers to register for ticks so they will execute without data + // (and thus notice stale data instead of showing a green ping-map + // despite no pings) + nmsData.registerSource("ticker","bananabananbanana"); + + restoreSettings(); + nmsMap.init(); + detectHandler(); + nms.playback.play(); + setupKeyhandler(); +} + +function detectHandler() { + var url = document.URL; + for (var i in handlers) { + if (('#' + handlers[i].tag) == document.location.hash) { + setUpdater(handlers[i]); + return; + } + } + setUpdater(handler_ping); +} + +function setMenu() +{ + var nav = document.getElementsByTagName("nav")[0]; + nav.style.display = nms.menuShowing ? '' : 'none'; +} + +function toggleMenu() +{ + nms.menuShowing = ! nms.menuShowing; + setMenu(); + saveSettings(); +} +function hideWindow(e,key) +{ + nmsInfoBox.hide(); +} +function toggleHelp(e,key) { + toggleLayer('aboutKeybindings'); +} + +function setMapModeFromN(e,key) +{ + switch(key) { + case '1': + setUpdater(handler_ping); + break; + case '2': + setUpdater(handler_uplinks); + break; + case '3': + setUpdater(handler_temp); + break; + case '4': + setUpdater(handler_traffic); + break; + case '5': + setUpdater(handler_comment); + break; + case '6': + setUpdater(handler_traffic_tot); + break; + case '7': + setUpdater(handler_disco); + break; + } + return true; +} + +function moveTimeFromKey(e,key) +{ + switch(key) { + case 'h': + nms.playback.stepTime(-3600); + break; + case 'j': + nms.playback.stepTime(-300); + break; + case 'k': + nms.playback.stepTime(300); + break; + case 'l': + nms.playback.stepTime(3600); + break; + case 'p': + nms.playback.toggle(); + break; + case 'r': + nms.playback.setNow(); + nms.playback.play(); + break; + } + return true; +} + +function keyPressed(e) +{ + if (e.target.nodeName == "INPUT") { + return false; + } + if(e.key) { + var key = e.key; + } else { + var key = e.keyCode; + switch(key) { + case 187: + key = '?'; + break; + case 189: + key = '-'; + break; + case 27: + key = 'Escape'; + break; + default: + key = String.fromCharCode(key); + key = key.toLowerCase(); + break; + } + } + if (nms.keyBindings[key]) + return nms.keyBindings[key](e,key); + if (nms.keyBindings['default']) + return nms.keyBindings['default'](e,key); + return false; +} + +function setupKeyhandler() +{ + var b = document.getElementsByTagName("body")[0]; + $( "body" ).keyup(function(e) { + keyPressed(e); + }); +} + + +function getCookie(cname) { + var name = cname + "="; + var ca = document.cookie.split(';'); + for(var i=0; i Date: Tue, 22 Mar 2016 19:54:00 +0100 Subject: Update nms-public code --- web/nms-public.gathering.org/js/nms.js | 86 ++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 40 deletions(-) (limited to 'web/nms-public.gathering.org/js/nms.js') diff --git a/web/nms-public.gathering.org/js/nms.js b/web/nms-public.gathering.org/js/nms.js index fcb8437..f145177 100644 --- a/web/nms-public.gathering.org/js/nms.js +++ b/web/nms-public.gathering.org/js/nms.js @@ -16,8 +16,8 @@ var nms = { * FIXME: Should just stop using these. */ timers: { - playback:false, - }, + playback:false + }, menuShowing:true, /* @@ -84,7 +84,7 @@ function nmsTimer(handler, interval, name, description) { }; this.setInterval = function(interval) { - var started = this.handle == false ? false : true; + var started = this.handle != false; this.stop(); this.interval = parseInt(interval); if (started) @@ -162,27 +162,27 @@ function stringToEpoch(t) function epochToString(t) { // Adjust for timezone when converting from epoch (UTC) to string (local) - var d = new Date(parseInt(t) * parseInt(1000)); - var timezoneOffset = d.getTimezoneOffset() * -60; + var date = new Date(parseInt(t) * parseInt(1000)); + var timezoneOffset = date.getTimezoneOffset() * -60; t = t - timezoneOffset; - var d = new Date(parseInt(t) * parseInt(1000)); - var str = d.getFullYear() + "-"; - if (parseInt(d.getMonth()) < 9) + date = new Date(parseInt(t) * parseInt(1000)); + var str = date.getFullYear() + "-"; + if (parseInt(date.getMonth()) < 9) str += "0"; - str += (parseInt(d.getMonth())+1) + "-"; - if (d.getDate() < 10) + str += (parseInt(date.getMonth())+1) + "-"; + if (date.getDate() < 10) str += "0"; - str += d.getDate() + "T"; - if (d.getHours() < 10) + str += date.getDate() + "T"; + if (date.getHours() < 10) str += "0"; - str += d.getHours() + ":"; - if (d.getMinutes() < 10) + str += date.getHours() + ":"; + if (date.getMinutes() < 10) str += "0"; - str += d.getMinutes() + ":"; - if (d.getSeconds() < 10) + str += date.getMinutes() + ":"; + if (date.getSeconds() < 10) str += "0"; - str += d.getSeconds(); + str += date.getSeconds(); return str; } @@ -207,7 +207,7 @@ nms.playback.startReplay = function(startTime,stopTime) { nms.playback.stopTime = stringToEpoch(stopTime); nms.now = epochToString(nms.playback.startTime); nms.playback.play(); -} +}; /* * Pause playback @@ -215,7 +215,7 @@ nms.playback.startReplay = function(startTime,stopTime) { nms.playback.pause = function() { nms.timers.playback.stop(); nms.playback.playing = false; -} +}; /* * Start playback @@ -224,7 +224,7 @@ nms.playback.play = function() { nms.playback.tick(); nms.timers.playback.start(); nms.playback.playing = true; -} +}; /* * Toggle playback @@ -235,19 +235,18 @@ nms.playback.toggle = function() { } else { nms.playback.play(); } -} +}; /* * Jump to place in time */ nms.playback.setNow = function(now) { - var now = parseNow(now); - nms.now = now; + nms.now = parseNow(now); nms.playback.stopTime = false; nms.playback.startTime = false; nms.playback.tick(); -} +}; /* * Step forwards or backwards in timer @@ -260,7 +259,7 @@ nms.playback.stepTime = function(n) if(!nms.playback.playing) nms.playback.tick(); -} +}; /* * Ticker to trigger updates, and advance time if replaying @@ -288,7 +287,7 @@ nms.playback.tick = function() if(nms.now !== false && nms.playback.playing) { nms.playback.stepTime(nms.playback.replayIncrement); } -} +}; /* * Helper function for safely getting a valid now-epoch @@ -377,7 +376,7 @@ function commentChange(id,state) myData = JSON.stringify(myData); $.ajax({ type: "POST", - url: "/api/private/comment-change", + url: "/api/write/comment-change", dataType: "text", data:myData, success: function (data, textStatus, jqXHR) { @@ -395,7 +394,7 @@ function addComment(sw,comment) myData = JSON.stringify(myData); $.ajax({ type: "POST", - url: "/api/private/comment-add", + url: "/api/write/comment-add", dataType: "text", data:myData, success: function (data, textStatus, jqXHR) { @@ -412,11 +411,7 @@ function addComment(sw,comment) */ function isIn(box, x, y) { - if ((x >= box.x) && (x <= (box.x + box.width)) && (y >= box.y) && (y <= (box.y + box.height))) { - return true; - } - return false; - + return ((x >= box.x) && (x <= (box.x + box.width)) && (y >= box.y) && (y <= (box.y + box.height))); } /* @@ -468,6 +463,7 @@ function initNMS() { nmsData.registerSource("switches","/api/public/switches"); nmsData.registerSource("switchstate","/api/public/switch-state"); nmsData.registerSource("dhcpsummary","/api/public/dhcp-summary"); + nmsData.registerSource("dhcp","/api/public/dhcp"); // This is a magic dummy-source, it's purpose is to give a unified // way to get ticks every second. It is mainly meant to allow map @@ -481,10 +477,10 @@ function initNMS() { detectHandler(); nms.playback.play(); setupKeyhandler(); + setupSearchKeyHandler(); } function detectHandler() { - var url = document.URL; for (var i in handlers) { if (('#' + handlers[i].tag) == document.location.hash) { setUpdater(handlers[i]); @@ -524,18 +520,21 @@ function setMapModeFromN(e,key) setUpdater(handler_uplinks); break; case '3': - setUpdater(handler_temp); + setUpdater(handler_dhcp); break; case '4': - setUpdater(handler_traffic); + setUpdater(handler_comment); break; case '5': - setUpdater(handler_comment); + setUpdater(handler_temp); break; case '6': - setUpdater(handler_traffic_tot); + setUpdater(handler_traffic); break; case '7': + setUpdater(handler_traffic_tot); + break; + case '9': setUpdater(handler_disco); break; } @@ -608,6 +607,13 @@ function setupKeyhandler() }); } +function setupSearchKeyHandler() +{ + $("#searchbox").keyup(function(e) { + nmsInfoBox._searchKeyListener(e); + }); +} + function getCookie(cname) { var name = cname + "="; @@ -625,8 +631,8 @@ function getCookie(cname) { function saveSettings() { var foo={}; - for (var v in nms.settingsList) { - foo[nms.settingsList[v]] = nms[nms.settingsList[v]]; + for ( var v in nms.settingsList ) { + foo[ nms.settingsList[v] ] = nms[ nms.settingsList[v] ]; } document.cookie = 'nms='+btoa(JSON.stringify(foo)); } -- cgit v1.2.3 From 904196acc73603ab48b7f4d5a8ba276f00d64222 Mon Sep 17 00:00:00 2001 From: Kristian Lyngstol Date: Wed, 23 Mar 2016 17:52:31 +0100 Subject: NMS: Public temp map --- web/nms-public.gathering.org/js/nms.js | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'web/nms-public.gathering.org/js/nms.js') diff --git a/web/nms-public.gathering.org/js/nms.js b/web/nms-public.gathering.org/js/nms.js index f145177..c09fca0 100644 --- a/web/nms-public.gathering.org/js/nms.js +++ b/web/nms-public.gathering.org/js/nms.js @@ -38,6 +38,9 @@ var nms = { '5':setMapModeFromN, '6':setMapModeFromN, '7':setMapModeFromN, + '8':setMapModeFromN, + '9':setMapModeFromN, + 'c':toggleConnect, 'h':moveTimeFromKey, 'j':moveTimeFromKey, 'k':moveTimeFromKey, @@ -345,6 +348,10 @@ function toggleLayer(layer) { l.style.display = 'none'; } +function toggleConnect() { + toggleLayer("linkCanvas"); +} + function commentInactive(id) { commentChange(id,"inactive"); @@ -534,6 +541,9 @@ function setMapModeFromN(e,key) case '7': setUpdater(handler_traffic_tot); break; + case '8': + setUpdater(handler_snmp); + break; case '9': setUpdater(handler_disco); break; -- cgit v1.2.3 From 7ac4551ed94c1f1393bc69e595a90dbe15bc8f6c Mon Sep 17 00:00:00 2001 From: Kristian Lyngstol Date: Thu, 24 Mar 2016 21:15:50 +0100 Subject: NMS: Update public api --- web/nms-public.gathering.org/js/nms.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'web/nms-public.gathering.org/js/nms.js') diff --git a/web/nms-public.gathering.org/js/nms.js b/web/nms-public.gathering.org/js/nms.js index c09fca0..8783844 100644 --- a/web/nms-public.gathering.org/js/nms.js +++ b/web/nms-public.gathering.org/js/nms.js @@ -100,14 +100,16 @@ function nmsTimer(handler, interval, name, description) { * Convenience function that doesn't support huge numbers, and it's easier * to comment than to fix. But not really, but I'm not fixing it anyway. */ -function byteCount(bytes) { +function byteCount(bytes,precision) { + if (precision ==undefined) + precision = 1; var units = ['', 'K', 'M', 'G', 'T', 'P']; var i = 0; while (bytes > 1024) { bytes = bytes / 1024; i++; } - return bytes.toFixed(1) + units[i]; + return bytes.toFixed(precision) + units[i]; } /* -- cgit v1.2.3