CodeReview--提高程式碼健壯性

菩提樹下的煮茶小童子發表於2018-02-26

前言

沒實習之前,不怎麼用到版本管理工具。頂多就是用Git把寫的一些程式碼上傳到github上,基本上不會涉及到某一個版本怎麼怎麼地的。年前到唱吧實習的這段時間,倒是接觸了不少相關的內容。由於歷史原因,後臺開發使用的是SVN。在提交程式碼的時候通過COMMIT資訊來實現CodeReview或者真正的程式碼提交。當時對這塊特別好奇,到底是怎麼實現的,今天也想著自己做一個簡易的版本出來,目的嘛,就當是學習新知識了。

程式碼提交路線

一般來說,程式碼提交可能會有這麼幾個階段。

  • 隨意篇: 自己管理,自己維護,commit就完了,這個階段基本上不會涉及版本控制。

  • 自測篇: 這個階段開發成員可能會大於一個人。大家各自開發自己的模組,功能完成後直接提交。最後進行模組間的整合。一般而言,這個階段開發風險有點大,某個成員離職了,文件不規範等,都會給後續的整合帶來困難。一旦出了問題,線上程式碼回滾就可以了,所以這個階段版本控制起到了很大的作用。

  • 監督篇: 這個階段下,一般團隊開發人員已經有一定的規模了。這個階段下的公司更在意程式碼的質量,而不是簡單的追求功能完成了。因此,給團隊其他成員進行程式碼review會進一步提高程式碼的健壯性,減少出錯的機率。類似於小黃鴨測試法,review你的程式碼的人或許可以輕鬆的發現你怎麼也不會想到的錯誤。

對我而言,是直接從**隨意篇蹦到了監督篇**了。記得當初寫唱吧APP首頁的心願牆介面的時候,程式碼被review了11次。最後一版不管是從簡潔性還是效率上逗比第一版好了太多。這也許就是程式碼review的好處了吧。可以從優秀的開發工程師身上學到更多實用的經驗。

也許有人會覺得,寫個程式碼要先被別人看下,還要浪費這麼多時間,我自己測試完事不就行了嘛,何必搞得這麼麻煩。其實,一開始我也是這麼想的,後來就慢慢的離不開codereview了,老話不是有一句磨刀不誤砍柴工嗎,被review了的程式碼在一定程度上會有更高的準確性和執行效率。從長遠角度看,後期維護和優化的時間也會少很多。

Code Review簡易實現

說了這麼多,都是鋪墊用的。下面開始一點點的著手進行實現這個簡易版本的codereview吧。

Hooks

在SVN倉庫的根目錄下有一個hooks目錄,裡面有很多鉤子模板。這些鉤子的作用就像我們提交一個表單的時候會使用JavaScript做下欄位的校驗。不同的鉤子(檔名去掉tmpl,並且chmod 755許可權才可以被正確執行)對應了不同的功能,而對於實現codereview的話,pre-commit鉤子就夠了。

svn倉庫下的hooks

鉤子的工作原理就是,在開發人員發起一次svn commit 到中央倉庫的時候,會被pre-commit攔截下來。這樣便於程式碼管理員做一些操作。同樣程式碼提交後,同樣會被post-commit(如果放開了的話)攔截,做一些通知類的處理邏輯。

SVNLOOK

# svnlook --help
general usage: svnlook SUBCOMMAND REPOS_PATH [ARGS & OPTIONS ...]
Subversion repository inspection tool.
Type 'svnlook help <subcommand>' for help on a specific subcommand.
Type 'svnlook --version' to see the program version and FS modules.
Note: any subcommand which takes the '--revision' and '--transaction'
      options will, if invoked without one of those options, act on
      the repository's youngest revision.

Available subcommands:
   author
   cat
   changed
   date
   diff
   dirs-changed
   filesize
   help (?, h)
   history
   info
   lock
   log
   propget (pget, pg)
   proplist (plist, pl)
   tree
   uuid
   youngest

複製程式碼

其中可以通過子命令author(獲取程式碼提交者名字), diff(獲取程式碼有改動的地方), log(獲取提交程式碼的註釋資訊)。用法大同小異。可以參考下面的命令學習不同的使用技巧。

# svnlook author --help
author: usage: svnlook author REPOS_PATH

Print the author.

Valid options:
  -r [--revision] ARG      : specify revision number ARG
  -t [--transaction] ARG   : specify transaction name ARG


複製程式碼

通用模板如下

svnlook subcommander -t "$TXN" "$REPOS" | ...
複製程式碼

思路

一開始我想的是,模仿公司的做法,將獲取到的diff檔案內容,通過github的gist服務,直接生成一個方便轉發閱讀的連結。但是國內現在沒法訪問這個服務了,很遺憾。然後就打算自己做一個(其實還是太天真了,一點也不優雅)。

因為diff內容可能會很大,因此需要使用post方式,在shell下使用curl傳送post本來是一個很隨意的事。

URL="http://ip/codereciew/addreview.php"
curl -i -X POST -H "Content-Type: application/json" -d '{"param1": "value1", "param2": "'$VALUE2'"}' $URL
複製程式碼

但是不管我怎麼測試,都獲取不到對應的請求體。很奇怪,沒辦法,轉換下思路。pre-commit把資料持久化到某一個地方,然後通過Python指令碼簡介將內容POST到對應的新增review的服務上。然後就可以通過getreview被別人看到。

真的是一波三折啊

程式碼部分

pre-commit*

cat pre-commit
#!/bin/bash

# PRE-COMMIT HOOK
#
# The pre-commit hook is invoked before a Subversion txn is
# committed.  Subversion runs this hook by invoking a program
# (script, executable, binary, etc.) named 'pre-commit' (for which
# this file is a template), with the following ordered arguments:
#
#   [1] REPOS-PATH   (the path to this repository)
#   [2] TXN-NAME     (the name of the txn about to be committed)
#
#   [STDIN] LOCK-TOKENS ** the lock tokens are passed via STDIN.
#
#   If STDIN contains the line "LOCK-TOKENS:\n" (the "\n" denotes a
#   single newline), the lines following it are the lock tokens for
#   this commit.  The end of the list is marked by a line containing
#   only a newline character.
#
#   Each lock token line consists of a URI-escaped path, followed
#   by the separator character '|', followed by the lock token string,
#   followed by a newline.
#
# If the hook program exits with success, the txn is committed; but
# if it exits with failure (non-zero), the txn is aborted, no commit
# takes place, and STDERR is returned to the client.   The hook
# program can use the 'svnlook' utility to help it examine the txn.
#
#   ***  NOTE: THE HOOK PROGRAM MUST NOT MODIFY THE TXN, EXCEPT  ***
#   ***  FOR REVISION PROPERTIES (like svn:log or svn:author).   ***
#
#   This is why we recommend using the read-only 'svnlook' utility.
#   In the future, Subversion may enforce the rule that pre-commit
#   hooks should not modify the versioned data in txns, or else come
#   up with a mechanism to make it safe to do so (by informing the
#   committing client of the changes).  However, right now neither
#   mechanism is implemented, so hook writers just have to be careful.
#
# The default working directory for the invocation is undefined, so
# the program should set one explicitly if it cares.
#
# On a Unix system, the normal procedure is to have 'pre-commit'
# invoke other programs to do the real work, though it may do the
# work itself too.
#
# Note that 'pre-commit' must be executable by the user(s) who will
# invoke it (typically the user httpd runs as), and that user must
# have filesystem-level permission to access the repository.
#
# On a Windows system, you should name the hook program
# 'pre-commit.bat' or 'pre-commit.exe',
# but the basic idea is the same.
#
# The hook program runs in an empty environment, unless the server is
# explicitly configured otherwise.  For example, a common problem is for
# the PATH environment variable to not be set to its usual value, so
# that subprograms fail to launch unless invoked via absolute path.
# If you're having unexpected problems with a hook program, the
# culprit may be unusual (or missing) environment variables.
#
# CAUTION:
# For security reasons, you MUST always properly quote arguments when
# you use them, as those arguments could contain whitespace or other
# problematic characters. Additionally, you should delimit the list
# of options with "--" before passing the arguments, so malicious
# clients cannot bootleg unexpected options to the commands your
# script aims to execute.
# For similar reasons, you should also add a trailing @ to URLs which
# are passed to SVN commands accepting URLs with peg revisions.
#
# Here is an example hook script, for a Unix /bin/sh interpreter.
# For more examples and pre-written hooks, see those in
# /usr/share/subversion/hook-scripts, and in the repository at
# http://svn.apache.org/repos/asf/subversion/trunk/tools/hook-scripts/ and
# http://svn.apache.org/repos/asf/subversion/trunk/contrib/hook-scripts/


REPOS="$1"
TXN="$2"

# Make sure that the log message contains some text.
SVNLOOK=/usr/bin/svnlook
#$SVNLOOK log -t "$TXN" "$REPOS" | \
#   grep "[a-zA-Z0-9]" > /dev/null || exit 1

# Exit on all errors.
set -e

# Check that the author of this commit has the rights to perform
# the commit on the files and directories being modified.
#"$REPOS"/hooks/commit-access-control.pl "$REPOS" $TXN \
#  "$REPOS"/hooks/commit-access-control.cfg

# 獲取TXN資訊和REPOS資訊
#echo $TXN 1>&2
#echo $REPOS 1>&2
# 獲取提交程式碼的作者資訊
AUTHOR=$($SVNLOOK author -t "$TXN" "$REPOS")
#echo $AUTHOR >> svn.log 1>&2
# 獲取改變的檔案,利用gist實現程式碼的review
DIFF=$($SVNLOOK diff -t "$TXN" "$REPOS")
DIFFPATH="/tmp/forcodereview/reviewcontent"
#echo $DIFF > $DIFFPATH
#echo $DIFF 1>&2
# add some rules for committing that should have commit msg.
LOGMSGLENGTH=$($SVNLOOK log -t "$TXN" "$REPOS" | wc -c)
LOGMSG=$($SVNLOOK log -t "$TXN" "$REPOS")
#echo $LOGMSG 1>&2
# 判斷commit資訊是否是review的還是maint的
REVIEW="REVIEW"
COMMIT="COMMIT"
REVIEWURL="http://myip/codereview/addreview.php"
choice=`echo $LOGMSG | awk -F: '{print $1}' | tr [a-z] [A-Z]`

##### 曲線救國吧
landmarkversion=$(echo $TXN | awk -F- '{print $1}')
echo $AUTHOR > /tmp/author
echo $landmarkversion > /tmp/txn
echo $LOGMSG > /tmp/logmsg
echo $DIFF > /tmp/diff


if [[ "$choice" == "REVIEW" ]];then
    # 用shell 傳送curl指令
    #RESPONSE=$(curl -i -X POST -H "'Content-Type':'application/json'" -d '{"author": "$AUTHOR", "version":"$TXN", "commitmsg": "$LOGMSG", "content":"$DIFF"}' $REVIEWURL)
    #echo $RESPONSE 1>&2
    #$version=$(echo $TXN | awk -F- '{print $1}')
    RESPONSE=$(python /home/svnrepositories/repo/hooks/post.py > /tmp/forcodereview/python-post.result)
    echo "請把下面的連結傳送給和你review程式碼的人:\nhttp://myip/codereview/getreview.php?landmarkversion=$landmarkversion&id=diff" 1>&2
    exit 1
elif [[ "$choice" == "COMMIT" ]];then
    # 可以坐下記錄什麼的,通知程式碼管理員或者團隊開發.
    # 放行,便於作進一步的提交稽核
    echo "恭喜您提交成功啦!" 1>&2
else
    echo "暫不支援的子命令" 1>&2
    exit 1
fi
if [ "$LOGMSGLENGTH" -lt 10 ];then
    echo -e "提交程式碼需要附帶提交的註釋,且字元長度別少於10吧,言簡意賅就行:)" 1>&2
    exit 1
fi
# All checks passed, so allow the commit.
exit 0

複製程式碼

post.py

cat post.py 
#!/usr/bin python
import requests
import sys
import json
# ... 省略獲取文字內容的程式碼。TODO文字方式持久化不好,需要優化下。
payload = {
    "author": author,
    "version": version,
    "commitmsg": commitmsg,
    "content":content
}

url = "http://myip/codereview/addreview.php"
res = requests.post(url=url, data=payload)
print(res.text)

複製程式碼

addreview.php

cat addreview.php 
<?php
/**
 * 用於接收來自svn hook 中的pre-commit的shell關於http的請求,解析出對應的檔案內容,並新增到review庫中。
 * */
$author = $_REQUEST["author"];
$landmarkversion = $_REQUEST["version"];
$TARGETPATH = "/tmp/forcodereview/";
$content = $_REQUEST["content"];
$commitmsg = $_REQUEST["commitmsg"];

$ret = array("code"=>-1, "msg"=>"新增程式碼review塊失敗!");
try{
    // 如果目標資料夾不存在則進行建立
    mkdirs($TARGETPATH.$landmarkversion);
    #$reviewname = time();
    $reviewname = "diff";
    $filecontent = "{$author} @ {$landmarkversion} want to commit those changes with `{$commitmsg}`\n{$content}";
    file_put_contents($TARGETPATH.$landmarkversion."/".$reviewname, $filecontent);
    $ret["code"] = 0;
    $ret["msg"] = "新增程式碼review成功,請移步:\n http://myip/codereview/getreview.php?landmarkversion={$landmarkversion}&id={$reviewname}";
}catch(Exception $e) {
    $ret["msg"] = $e->getMessage();
}
echo json_encode($ret);

function mkdirs($dir, $mode = 0777)
{
    if (is_dir($dir) || @mkdir($dir, $mode)) return TRUE;
    if (!mkdirs(dirname($dir), $mode)) return FALSE;
    return @mkdir($dir, $mode);
}

複製程式碼

getreview.php

cat getreview.php 
<?php
/**
 * 用於獲取code review結果。通過id代指對應的檔名。
 * landmarkversion:最新程式碼的下一個版本號
 * id: 防止覆蓋的檔案review檔名
 * */
$TARGETPATH = "/tmp/forcodereview/";
$landmarkversion = $_REQUEST["landmarkversion"];
$reviewname = $_REQUEST["id"];

$filename = $TARGETPATH.$landmarkversion."/".$reviewname;
$content = file_get_contents($filename);

$data = explode("\n", $content);
$header = $data[0].$data[1];
$body = $data[2];
?>

<div class="container">
    <h3><?php echo $header;?></h3>
    <hr>
    <p><?php print_r($body);?></p>
</div>

複製程式碼

具體使用

review模式

在提交資訊中使用REVIEW(不區分大小寫)作為字首即可。這樣不會真正的提交改動。而是會將改動內容,傳送到addreview.php來生成一個連結。用來檢視對應的變動內容。

code review模式

commit模式

在提交資訊中使用COMMIT(不區分大小寫)作為字首,會繞開review檢查,直接提交到版本庫中。

code commit模式

總結

  • 磕磕絆絆,服務算是搭建起來了,雖然很不優雅。現在想想,要是一開始gist服務就能用該有多好。

  • nginx部署多個服務且互不影響的實現(這裡偷了個懶,我的便籤工具是python寫的RESTful形式,所以沒有字尾名,而PHP剛好有字尾,藉此進行了區分。後面如果有多種服務,可能還需要使用不同的轉發策略。)

待解決、優化的地方

  • shell下使用curl傳送POST請求,這個依舊沒解決,對此比較瞭解的博友們歡迎一起交流。

  • getreview.php實在是太難看了。後面想想怎麼來美化一下。方便閱讀和瀏覽。

不過越是有困難,才越能提升自己不是。有需求,就要去實現,過程中難免會遇到各種阻礙,我能做的,就是一個個去面對,去解決。

相關文章