Luke Clark

Save your commit history to a running Harvest timer

A git hook is a script that will run at certain points in git's execution. They are per repository, and remain local so they do not push to remotes.

Usually they're written in bash or sh. But you can run them in any language you'd prefer. We're going to use a nodejs post-commit hook to post information about our commit to a currently running timer.

I thought what better way to create meaningful timesheet entries than including my actual commit history. Perfect for reflecting on your own efficiency or communicating exactly what happened to clients.

The .git/hooks/post-commit hook will run after a commit has been made.

Check out the annotated source code below

.git/hooks/post-commit
1
#!/usr/bin/env node
2
"use strict";
3
4
// https://lukeclark.com.au/posts/harvest-post-commit-hook-nodejs
5
6
// Assume dependencies are installed at the project, or
7
// required commands have been installed globally
8
var request = require("request");
9
10
// make the logOutput available anywhere
11
var logOutput;
12
13
// Your Havest info
14
var credentials = {
15
account: "HARVESTACCOUNT",
16
username: "HARVESTUSER",
17
password: "HARVESTPASS",
18
};
19
20
var options = {
21
url: "https://" + credentials.account + ".harvestapp.com/daily",
22
auth: {
23
username: credentials.username,
24
password: credentials.password,
25
},
26
headers: {
27
Accept: "application/json",
28
"User-Agent": "git post commit hook",
29
},
30
};
31
32
function harvest(cb) {
33
console.log("[Harvest] Looking for running timer...");
34
request(options, function (error, response, body) {
35
var today = JSON.parse(body);
36
if (error) cb(error, {});
37
if (today.day_entries.length === 0) cb(false, "No running timer found");
38
today.day_entries.forEach(function (entry) {
39
// Check for running timer
40
if (typeof entry.timer_started_at !== "undefined") {
41
// Create a notes variable if there isn't one
42
if (typeof entry.notes === "undefined") entry.notes = "";
43
// Append our commit to the notes
44
entry.notes += "\n" + logOutput;
45
// Modify original request to post an update to the active timer
46
var postParams = Object.assign({ method: "POST" }, options);
47
postParams.url += "/update/" + entry.id;
48
postParams.formData = entry;
49
request(postParams, function (err, resp, body) {
50
if (err) {
51
cb(err, {});
52
} else {
53
cb(false, "Added commit to running timer");
54
}
55
});
56
}
57
});
58
});
59
}
60
61
// Allows us to launch an external shell command
62
var spawn = require("child_process").spawn;
63
// Run our command and save the reference so we can kill it if something bad happens.
64
var child = spawn("git", [
65
"--no-pager", // Print straight away, don't use a pager
66
"log",
67
"--oneline", // first 7 digits of commit, and message on single line
68
"--no-decorate", // Don't include ref names for commits
69
"-1", // Only show the latest commit
70
"HEAD", // From the current branch
71
]);
72
73
child.stdout.on("data", function (data) {
74
// Save our one liner out to a global variable, strip newlines
75
logOutput = data.toString();
76
logOutput = logOutput.replace(/(\r\n|\n|\r)/gm, "");
77
});
78
79
child.on("close", function (code) {
80
if (code !== 0) {
81
// 0 = good, otherwise bad
82
console.error("Something went wrong, code: ", code);
83
process.exit(code);
84
} else {
85
harvest(function (err, cb) {
86
if (err) console.error(err);
87
console.log("[Harvest] " + cb);
88
process.exit(code);
89
});
90
}
91
});
92
93
process.on("uncaughtException", function (err) {
94
console.error("Uncaught Exception: ", err.stack);
95
child.kill("SIGTERM");
96
process.exit(1);
97
});