做爲一個免費提供私有倉庫的代碼託管平臺,碼雲時常要考慮利用現有的資源支持更多的用戶,對於體積較大的存存儲庫, 因爲 git 的分佈式特性,服務器每每須要更多的硬件資源來支撐這些存儲庫的訪問。c++
碼雲對 git 倉庫的大小限制爲 1GB,用戶在本地可使用以下命令查看存儲庫的大小。git
du -sh .git/objectsgithub
這個命令在 Git for Windows 中能夠找到,也可使用 www.sysinternals.com 提供的 du (Directory disk usage reporter)工具。服務器
碼雲對文件的限制爲 100 MB,超過 50 MB 會提出警告。一部分用戶很容易將生成的二進制文件添加到版本控制之中,當推送到 碼雲上就被拒絕推送了。當用戶須要檢查或者回退就會感到很是麻煩,開發 git-analyze 的目的也就是爲了解決這些用戶的煩惱。app
git-analyze 此工具的設計上是根據用戶的輸入,掃描存儲庫特定分支從哪次提交引入了體積超出限制的文件。分佈式
git 有多種實現,好比 Linus 的 git(官方 git),libgit2,jgit 等等,官方 git 是一個由多個子命令組成的程序集合。 可是,若是要新增一個工具到 git 官方仍是比較麻煩,定製的 git 也容易帶來兼容性問題,不利於用戶體驗。 JGIT 是 Java 實現的 git 類庫,若是要實現這些工具,還要用戶安裝 JRE 或者攜帶 JRE,而且 Java 也不擅長作跨平臺 命令。libgit2 是 C 實現的一個跨平臺 git 協議實現庫,而且提供多種語言的 banding,因此用 libgit2 再合適不過。ide
git-analyze 支持參數:函數
git-analyze 倉庫參數爲:工具
git-analyze /path/to/repo master # 也能夠是 引用全名,兩者的相對順序必須是先路徑後引用,標籤參數不作要求。測試
git-analyze 在用戶輸入參數後,使用 libgit2 打開存儲庫。目前只支持工做目錄的根目錄和 .git 目錄。
git 的每一次提交都是文件快照,並不像 Subversion 同樣每個文件都有版本號。若是要知道是否有新的文件被添加或者 是被修改,則須要與上一個提交進行比較,一般就是當前的 commit 與 parent commit 比較,在 libgit2 中,並不能直接 比較,須要比較 commit 的根 tree。使用 git_commit_tree 獲得 tree 對象,git_diff_tree_to_tree 比較 tree,git_diff_foreach 去遍歷 diff 的內容,這裏因爲咱們只須要查看文件修改,因此,git_diff_foreach binary_cb hunk_cb line_cb callback 設置 爲空便可,git_diff_foreach 的 API 在下面:
咱們在 回調函數中,只響應 diff 類型爲新增和修改的文件類型。
當出現合併時,咱們的策略是,只比較第一個 parent commit,大文件引入行爲歸咎與合併者。
當遍歷到初始提交時,parent commit 也就不存在了,因此,咱們要使用 treewalk 遍歷全部的文件,檢測引入的大文件。
當使用 --all 參數時,git-analyze 會忽略引用參數,直接遍歷全部本地分支對應的引用,而後逐一檢測。
libgit2 使用 CMake 做爲構建文件,CMake 可以根據不一樣的平臺生成不一樣類型的項目文件,如 Visual Studio 的 msbuild 項目文件,Makefile 文件 等,而後支持自動打包,例以下面的一些代碼就能夠支持生成 Windows 安裝程序,Ubuntu DEB 包
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib") set(CPACK_PACKAGE_NAME "git-analyze") set(CPACK_PACKAGE_VENDOR "OSChina.NET") set(CPACK_PACKAGE_DESCRIPTION "This is git analyze tools") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "GIT Analyze") set(CPACK_PACKAGE_INSTALL_DIRECTORY "git-analyze") set(CPACK_PACKAGE_VERSION_MAJOR ${GITANALYZE_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${GITANALYZE_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${GITANALYZE_VERSION_PATCH}) set(CPACK_PACKAGE_VERSION ${PACKAGE_VERSION}) set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") set(CPACK_PACKAGE_DESCRIPTION "Git Analyze") set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.3.1-6)") set(CPACK_PACKAGE_CONTACT "admin@oschina.cn") set(CPACK_DEBIAN_PACKAGE_SECTION T) if(WIN32 AND NOT UNIX) set(CPACK_PACKAGE_INSTALL_REGISTRY_KEY "GitAnalyze") set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}\\\\cmake\\\\git.ico") set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}\\\\cmake\\\\git.ico") set(CPACK_NSIS_MODIFY_PATH "ON") set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL "ON") if( CMAKE_CL_64 ) set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64") endif() endif() include(CPack) if(WIN32) include(InstallRequiredSystemLibraries) endif() install(TARGETS git-analyze DESTINATION bin )
CMake 也能自動識別程序資源源文件 (.rc 文件),程序清單 (.manifest) 。
#C++ Based hook command if(WIN32) add_executable(git-analyze driver.cc analyze.cc environment.cc git-analyze.rc git-analyze.manifest ) else() add_executable(git-analyze driver.cc analyze.cc environment.cc ) endif()
將 libgit2 做爲一個依賴加入項目中,只須要在 CMakeLists.txt 中添加 add_subdirectory(vendor/libgit2) 可。
UNIX® 系統支持信號 SIGALRM ,註冊信號後, 而後可使用 alarm 激活定時器,git-analyze 在非 Windows 平臺 是同 alarm 實現定時器,不過 alarm 精度不高,若是要使用更高精度的可使用 ualarm 。
WINDOW ® 系統的定時器有 CreateWaitableTimer timeSetEvent CreateTimerQueueTimer 等,分別應對不一樣的場景。 好比 timeSetEvent 其實是使用 Windows Event 對象實現,內部仍是開了線程,git-analyze 實現的 Timer 功能是啓動 一個新的線程,而後 Sleep 後,運行 exit 退出進程,調用 exit 後會調用 ExitProcess 因此進程會退出,而後主進程結束時 也會調用 ExitProcess 退出。
在 Git 中, 有 revert 和 reset 命令,而 git-rollback 實現 git 特定分支的回滾, 只是一個直觀簡單的替代。須要使用高級功能 可使用 git reset 或者 revert。
支持參數:
使用 --backid 參數時,git-rollback 先須要回溯檢測 commit 是否在分支上,存在的時候會設置 refname (這個支持分支名和引用全名) 的 commit 爲 --backid 的值,而後運行 git gc ,當添加 --force 時會清理掉那些懸空對象。
使用 --backrev 時, git-rollback 會回溯 commit,而後當回溯次數與 --backrev 值一致時,將當前 commit 的 oid 設置到引用上,與 --backid 的策略一致便可。
因爲 libgit2 暫時並未提供 GC 功能,咱們調用的是原生命令,在 UNIX 類系統中,咱們先得到環境變量 PATH,而後遍歷這些目錄是否 存在 git ,存在後,使用 fork-execvp-wait 一系列 API 運行 git GC。
在 Windows 中,咱們從 git-rollback 的當前目錄,以及 git-rollback 進程所在目錄,以及 PATH 中查找 git,若是沒有找到,則從 註冊表中查找 Git for Windows 的安裝路徑。部分的代碼以下:
class WCharacters { private: wchar_t *wstr; public: WCharacters(const char *str) : wstr(nullptr) { if (str == nullptr) return; int unicodeLen = ::MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); if (unicodeLen == 0) return; wstr = new wchar_t[unicodeLen + 1]; if (wstr == nullptr) return; wstr[unicodeLen] = 0; ::MultiByteToWideChar(CP_UTF8, 0, str, -1, (LPWSTR)wstr, unicodeLen); } const wchar_t *Get() { if (!wstr) return nullptr; return const_cast<const wchar_t *>(wstr); } ~WCharacters() { if (wstr) delete[] wstr; } }; inline bool PathFileIsExistsU(const std::wstring &path) { auto i = GetFileAttributesW(path.c_str()); return INVALID_FILE_ATTRIBUTES != i; } inline bool PathRemoveFileSpecU(wchar_t *begin, wchar_t *end) { for (; end > begin; end--) { if (*end == '/' || *end == '\\') { *end = 0; return true; } } return false; } typedef BOOL(WINAPI *LPFN_ISWOW64PROCESS)(HANDLE, PBOOL); BOOL IsRunOnWin64() { BOOL bIsWow64 = FALSE; LPFN_ISWOW64PROCESS fnIsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress( GetModuleHandleW(L"kernel32"), "IsWow64Process"); if (NULL != fnIsWow64Process) { if (!fnIsWow64Process(GetCurrentProcess(), &bIsWow64)) { // handle error } } return bIsWow64; } BOOL WINAPI FindGitInstallationLocation(std::wstring &location) { // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1 // InstallLocation HKEY hInst = nullptr; LSTATUS result = ERROR_SUCCESS; const wchar_t *git4win = LR"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1)"; const wchar_t *installKey = L"InstallLocation"; WCHAR buffer[4096] = {0}; #if defined(_M_X64) if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, git4win, 0, KEY_READ, &hInst) != ERROR_SUCCESS) { if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, git4win, 0, KEY_READ | KEY_WOW64_32KEY, &hInst) != ERROR_SUCCESS) { // Cannot found msysgit or Git for Windows install return FALSE; } } #else if (IsRunOnWin64()) { if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, git4win, 0, KEY_READ | KEY_WOW64_64KEY, &hInst) != ERROR_SUCCESS) { if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, git4win, 0, KEY_READ, &hInst) != ERROR_SUCCESS) { // Cannot found msysgit or Git for Windows install return FALSE; } } } else { if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, git4win, 0, KEY_READ, &hInst) != ERROR_SUCCESS) { return FALSE; } } #endif DWORD type = 0; DWORD dwSize = 4096 * sizeof(wchar_t); result = RegGetValueW(hInst, nullptr, installKey, RRF_RT_REG_SZ, &type, buffer, &dwSize); if (result == ERROR_SUCCESS) { location.assign(buffer); } RegCloseKey(hInst); return result == ERROR_SUCCESS; } //// // //// // bool search_git_from_path(std::wstring &gitbin) { // /// // WCHAR buffer[4096] = {0}; // DWORD dwLength = 0; // //// // if ((dwLength = // SearchPathW(nullptr, L"git", L".exe", 4096, buffer, nullptr)) > 0) // { // gitbin.assign(buffer, dwLength); // return true; // } // return false; // } bool SearchGitForWindowsInstall(std::wstring &gitbin) { // if (!FindGitInstallationLocation(gitbin)) return false; gitbin.push_back(L'\\'); gitbin.append(L"git.exe"); if (PathFileIsExistsU(gitbin)) return true; return false; } // bool GitExecutePathSearchAuto(const wchar_t *cmd, std::wstring &gitbin) { //// Self , Path Env, if (PathFileIsExistsU(cmd)) { gitbin.assign(cmd); return true; } std::wstring Path; Path.reserve(0x8000); /// 32767 /// auto len = GetModuleFileNameW(nullptr, &Path[0], 32767); if (len > 0) { auto end = &Path[0] + len; PathRemoveFileSpecU(&Path[0], end); gitbin.assign(&Path[0]); gitbin.push_back(L'\\'); gitbin.append(cmd); if (PathFileIsExistsU(gitbin)) return true; gitbin.clear(); } /// GetEnvironmentVariableW(L"PATH", &Path[0], 32767); auto iter = &Path[0]; for (; *iter; iter++) { if (*iter == ';') { gitbin.push_back(L'\\'); gitbin.append(cmd); if (PathFileIsExistsU(gitbin)) { return true; } gitbin.clear(); } else { gitbin.push_back(*iter); } } return false; } /// First search git from path. bool GitGCInvoke(const std::string &dir, bool forced) { /// WCharacters wstr(dir.c_str()); /// convert to UTF16 std::wstring gitbin; if (!GitExecutePathSearchAuto(L"git.exe", gitbin)) { if (!SearchGitForWindowsInstall(gitbin)) { BaseErrorMessagePrint( "Not Found git in your PATH environemnt variable and Registry !"); return false; } } ///////////////////////////////////////////////////////// std::wstring cmdline; cmdline.reserve(0x8000); _snwprintf_s(&cmdline[0], 32767, 32767, LR"("%s" gc )", gitbin.c_str()); if (forced) { wcscat_s(&cmdline[0], 32767, L"--prune=now --force"); } STARTUPINFOW si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); ZeroMemory(&pi, sizeof(pi)); si.cb = sizeof(si); if (!CreateProcessW(nullptr, &cmdline[0], nullptr, nullptr, FALSE, 0, nullptr, wstr.Get(), &si, &pi)) { return false; } bool result = false; if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_OBJECT_0) { DWORD dwExit = 0; if (GetExitCodeProcess(pi.hProcess, &dwExit) && dwExit == 0) { result = true; } } CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return result; }
libgit2 使用的是 UTF-8 編碼,在 Windows 中轉變爲 UTF16 編碼,使用 Windows API 完成一系列操做.
若是按照默認的 main 傳遞命令行參數,那麼可能會發生錯誤,在 Windows 中, 建立進程是經過 CreateProcess 這樣的 API 實現的, NT 內核將命令行參數寫入的進程的 PEB 中, CRT 初始化時,根據啓動函數類型執行不一樣的策略 (WinMain wWinMain main wmain) , 好比 main , CRT 經過 GetCommandLineA 得到命令行參數,而後將 LPCSTR 轉變成 char * Argv[] 的形式. GetCommandLineA 得到 的命令行參數也是由 PEB 的命令行參數轉換編碼過來的. main 命令行參數的編碼即當前代碼頁的編碼,也就是 CP_ACP , 好比 Windows 下常見的 936 GBK。
這樣一來,libgit2 傳入非 西文字符 就會操做失敗, 爲了支持 Windows 平臺,筆者使用 wmain ,而後將命令行參數依次轉變爲 UTF-8, 這樣就能夠解決不支持非西文字符的問題。而後 POSIX 平臺依然使用 main。
#if defined(_WIN32) && !defined(__CYGWIN__) #include <Windows.h> //// To convert Utf8 char *CopyToUtf8(const wchar_t *wstr) { auto l = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); char *buf = (char *)malloc(sizeof(char) * l + 1); if (buf == nullptr) throw std::runtime_error("Out of Memory "); WideCharToMultiByte(CP_UTF8, 0, wstr, -1, buf, l, NULL, NULL); return buf; } int wmain(int argc, wchar_t **argv) { std::vector<char *> Argv_; auto Release = [&]() { for (auto &a : Argv_) { free(a); } }; try { for (int i = 0; i < argc; i++) { Argv_.push_back(CopyToUtf8(argv[i])); } } catch (const std::exception &e) { BaseErrorMessagePrint("Exception: %s\n", e.what()); Release(); return -1; } AnalyzeArgs analyzeArgs; ProcessArgv((int)Argv_.size(), Argv_.data(), analyzeArgs); if (ProcessAnalyzeTask(analyzeArgs)) { BaseConsoleWrite("git-analyze: Operation completed !\n"); } else { BaseErrorMessagePrint("git-analyze: Operation aborted !\n"); } Release(); return 0; } #else int main(int argc, char **argv) { AnalyzeArgs analyzeArgs; ProcessArgv(argc, argv, analyzeArgs); if (ProcessAnalyzeTask(analyzeArgs)) { BaseConsoleWrite("git-analyze: Operation completed !\n"); } else { BaseErrorMessagePrint("git-analyze: Operation aborted !\n"); } return 0; } #endif
另一個問題,因爲參數和 libgit2 都是使用的 UTF8 編碼,默認狀況下,Windows 控制檯的代碼頁在輸出 UTF8 編碼 字符的狀況下可能會亂碼,libgit2 並無去調整,而控制檯的代碼頁若是手動調整,可能會致使其餘程序亂碼。 固然能夠調用 SetConsoleOutputCP 去修改代碼頁,筆者並未測試,筆者採用的是和 git 官方同樣的策略, 檢測程序當前的標準輸出標準錯誤是不是字符設備,這個可使用 _isatty 來檢測,固然也可使用下面的代碼 來實現檢測:
bool IsUnderConhost(FILE *fp) { HANDLE hStderr = reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(fp))); return GetFileType(hStderr) == FILE_TYPE_CHAR; }
可是,重要的一點,MSYS2 的終端模擬器 Mintty 編碼是 UTF8 ,_isatty 並不會將 Mintty 識別爲字符設備,這是因爲 MSYS2 或者 Cygwin 中,使用的是管道的方式讀取程序的輸出渲染到 Mintty,不過 MSYS2 的環境變量中會存在 TERM 這樣的變量,就能夠用下面的代碼去識別:
bool IsWindowsTTY() { if (GetEnvironmentVariableW(L"TERM", NULL, 0) == 0) { if (GetLastError() == ERROR_ENVVAR_NOT_FOUND) return false; } return true; }
在輸出錯誤的時候,咱們能夠修改輸出顏色,在控制檯中,可使用 SetConsoleTextAttribute,使用 GetConsoleScreenBufferInfo 得到控制檯的顏色,控制檯是 256 色的,其中高 4位是背景色,低四位是前景色,因此可使用下面的代碼實現色彩輸出:
int BaseErrorWriteConhost(const char *buf, size_t len) { // TO set Foreground color HANDLE hConsole = GetStdHandle(STD_ERROR_HANDLE); CONSOLE_SCREEN_BUFFER_INFO csbi; GetConsoleScreenBufferInfo(hConsole, &csbi); WORD oldColor = csbi.wAttributes; WORD newColor = (oldColor & 0xF0) | FOREGROUND_INTENSITY | FOREGROUND_RED; SetConsoleTextAttribute(hConsole, newColor); DWORD dwWrite; WCharacters wstr(buf, len); WriteConsoleW(hConsole, wstr.Get(), wstr.Length(), &dwWrite, nullptr); SetConsoleTextAttribute(hConsole, oldColor); return dwWrite; }
在 Unix 或者 MSYS2 中,能夠在輸出中加入 \e[31m (GCC) \33[31m (MSVC) 這樣的字符控制終端文字顏色。
更多的代碼請查看 git-analyze