content/posts/git-remote-hooks.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
--- title: "Using server-side git hooks to auto publish blog posts" date: 2020-10-28T00:20:37+05:30 --- Git hooks are great to automate your software development workflow. They can be used to implement CI/CD workflows. Many PaaS services like [Netlify](https://netlify.com) and [Heroku](https://heroku.com) trigger a build process of your app or website when you push to a remote repository. In this post I want to go through how one could implement that automated process using git hooks for a static blog using just a bash script with nginx serving the files on a VPS. ## Setting up a remote git repository Setting up a remote git repository on the VPS is as easy as doing: ``` $ git init --bare website ``` Usually it's a practice of creating a new user called 'git' on your server to have access to your git repositories through ssh. I keep my repositories in the home directory of the 'git' user on my server. My git clone command would be something like this: ``` $ git clone git@git.prithu.xyz:website ``` There are other ways you can set up a remote git repository - using the git or http protocols. I would suggest one reads the chapter [Git on the Server](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols) from the wonderful book on git - [Pro Git](https://git-scm.com/book/en/v2) to get to know them. If I already have a local repository, then I just push to this new one by adding it as a remote using: ``` $ git remote add origin git@git.prithu.xyz:website $ git push --set-upstream origin master ``` ...Or initialize a new one: ``` $ git init website $ cd website $ git remote add origin git@git.prithu.xyz:website $ cat "A repo for my website" > README.md $ git commit -am 'Initial commit' $ git push origin master ``` ## Understanding hooks A hook is basically code that runs as a result of some event. Here the hooks are basically executable scripts/programs and reside in the `.git/hooks` directory of a git repo. By default this directory has example scripts for a few hooks (go take a look at them). You can read the man page [githooks(5)](https://git-scm.com/docs/githooks) to get a list of all the available git hooks. In git, we have client-side hooks and server-side hooks. Client-side git hooks run on your machine when you perform actions on your local git repo. `pre-commit` is a client-side hook that runs before you commit something, you can use this hook to check for what is being committed and run tests. For example, to check if the formating of the code conforms to a style guide, or perform some sort of static analysis to find bugs and vulnerabilities. `post-merge` runs after a successful merge takes place. The `commit-msg` hook is invoked on `git commit` and gets the name of the file that holds the commit message. Checks can be performed on the commit message to see whether or not it conforms to a standard format. If the hook exits with a non-zero cdoe, the commit is aborted. We also have server-side hooks that are invoked on the remote repository. `pre-recieve` hook runs before refs are updated on the remote, i.e before there are any changes on the remote repository. The script can exit with a non-zero code and the push won't be accepted and the client will be notified of the failure. `post-recieve` on the other hand runs when all the refs are updated on the remote. This hook can be thought of as a hook that runs when there is a 'push'. This hook gets information of what refs were updated - if the master branch was updated then this information is passed on to the script along with the last hash and the new updated hash. This information is passed on to the script through the standard input. For example the `post-recieve` script would have the following line passed to it through the standard input. ``` 689d4729b362e69a27600bb5bc26ca043c67f49f c60d357c48be63ff8ad8a6f94ab2f525332a9cd7 refs/heads/master ``` `689d472` is the old object hash, `c60d357` is the new hash value and `refs/heads/master` is the ref being updated. So the `master` branch is now pointing to `c60d357` A hook script can be written in any language as long as it can be an executable file. ## Writing a hook script I will be using a simple shell script to checkout the latest commit on the `website` repo, build the Hugo site and copy it to a location which will be served by nginx. ``` #!/bin/bash set -o pipefail read ref echo "[`date`] $ref" >> /home/git/.build/logs/git branch=$(echo $ref | cut -d ' ' -f 3) if [ "$branch" = "refs/heads/master" ] then echo "Building Hugo Site" cd /home/git/.build/ [ -d 'website' ] && rm -rf website git clone /home/git/website > /dev/null 2>&1 && cd website logdate="$(date +%Y-%m-%d-%H%M)" hugo | tee "/home/git/.build/logs/$logdate.log" || exit 1 rm -rf /home/git/.build/public mv public /home/git/.build/ echo "Build complete" fi ``` This script builds the Hugo site and then places the resultant static files in the `/home/git/.build/public` directory. It's always a good idea to keep logs and hence I log every build in the `/home/git/.build/logs/` directory. Now, all I need is a webserver serving those files in the `public/` directory. In nginx for example, the following server block will do. ``` server { listen 80 ; listen [::]:80 ipv6only=on; server_name prithu.xyz; root /var/www/website; charset utf-8; location / { try_files $uri $uri/ =404; } } ``` I then create a symbolic link - `/var/www/website` -> `/home/git/.build/public` ``` # ln -s /home/git/.build/public /var/www/website ``` I do this as the 'git' user does not have permissions to write to `/var/www/`; hence a symbolic link is useful here. There might be other housekeeping chores to be done like making sure that the 'git' user has permissions to do things the script will do as the script is run as the 'git' user. Also, another thing to note is - anything the script writes to stdout will be displayed to the client. The above script writes to the stdout a copy of Hugo's output log. ``` $ git commit -m "some changes" $ git push myrepo master Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 4 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 316 bytes | 316.00 KiB/s, done. Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 remote: remote: Building Hugo Site remote: remote: Start building sites … remote: on. remote: WARN 2021/01/15 11:34:13 found no layout file for "HTML" for kind "taxonomy": You should create a template file which matches Hugo Layouts Lookup Rules for this combinati remote: on. remote: WARN 2021/01/15 11:34:13 found no layout file for "HTML" for kind "term": You should create a template file which matches Hugo Layouts Lookup Rules for this combination. remote: remote: | EN remote: -------------------+----- remote: Pages | 21 remote: Paginator pages | 0 remote: Non-page files | 5 remote: Static files | 19 remote: Processed images | 0 remote: Aliases | 0 remote: Sitemaps | 1 remote: Cleaned | 0 remote: remote: Total in 417 ms remote: remote: Build complete remote: To git.prithu.xyz:website 4e7bec0..611ac8c master -> master ``` And that's it! Every time I push to the master branch of this remote, my Hugo site will be built and "deployed".