├── .gitignore ├── LICENSE ├── README.md ├── code_statistics.py ├── day.py ├── month.py ├── requirements.txt ├── run.sh ├── template ├── 周统计模板.xlsx └── 月统计模板.xlsx └── week.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mario 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitLab 开发者代码提交量统计 2 | 统计 GitLab 所有开发者代码提交情况,按删除、新增、总计、次数四个维度展示,支持日、月、周三种时间范围,自动将统计结果推送钉钉群 3 | 4 | 5 | ### 使用说明 6 | 1. 安装需要的支持库 7 | `pip install -r requirements.txt` 8 | 9 | 2. 修改 `code_statistics.py` 文件 L13-L24 内容以及 `day.py、month.py、week.py` 三个文件的倒数第二行内容,最后运行即可,这三个文件分别对应 日、月、周三个统计时间范围,需要统计哪个时间就运行哪个脚本 10 | 11 | 12 | ### 自动运行 13 | Linux 设置 crontab 定时任务即可 14 | 15 | ```bash 16 | 50 20 * * 1-6 bash run.sh day.py 17 | 00 21 * * 7 bash run.sh week.py 18 | 30 10 2 1-12 * bash run.sh month.py 19 | ``` 20 | 21 | 22 | ### 已知问题 23 | 1. 统计人数超过 25 人,Excel模板排版会有问题,因为数据直接由Excel处理的,所以需要拖下表格让他支持更多行 24 | -------------------------------------------------------------------------------- /code_statistics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # coding=utf8 3 | # @Author: Mario 4 | # @Date : 2021/01/01 5 | # @Desc : GitLab 按时间查看各用户代码提交量官方API版(钉钉推送版) 6 | 7 | import datetime 8 | import json 9 | 10 | import requests 11 | from dateutil.parser import parse 12 | 13 | # 需修改如下四条信息 14 | gitlab_url = "" # GitLab 地址 15 | private_token = "" # GitLab Access Tokens(管理员权限) 16 | report_path = "/data/" # 生成的报告文件存放目录 17 | dingding_token = "" # 钉钉机器人token 18 | 19 | info_master = [] 20 | info_other = [] 21 | statistics = {} 22 | 23 | # 需要去除的统计的 Git 用户名 24 | exclude_names = ["Administrator",] 25 | 26 | headers = { 27 | 'Connection': 'close', 28 | } 29 | 30 | 31 | # UTC时间转时间戳 32 | def utc_time(time): 33 | dt = parse(time) 34 | return int(dt.timestamp()) 35 | 36 | 37 | # 根据日期获取星期 38 | def get_week_num(date): 39 | week = datetime.datetime.strptime(date, "%Y-%m-%d").weekday() + 1 40 | if week == 1: 41 | return "周一" 42 | elif week == 2: 43 | return "周二" 44 | elif week == 3: 45 | return "周三" 46 | elif week == 4: 47 | return "周四" 48 | elif week == 5: 49 | return "周五" 50 | elif week == 6: 51 | return "周六" 52 | else: 53 | return "周日" 54 | 55 | 56 | # 获取 GitLab 上的所有项目 57 | def gitlab_projects(project_kind): 58 | project_ids = [] 59 | page = 1 60 | while True: 61 | url = gitlab_url + "api/v4/projects/?private_token=" + private_token + "&page=" + str(page) + "&per_page=20" 62 | while True: 63 | try: 64 | res = requests.get(url, headers=headers, timeout=10) 65 | break 66 | except Exception as e: 67 | print(e) 68 | continue 69 | projects = json.loads(res.text) 70 | if len(projects) == 0: 71 | break 72 | else: 73 | for project in projects: 74 | if project["namespace"]["kind"] != project_kind or project["archived"]: # 统计哪种类型未归档的仓库 user、group 75 | continue 76 | else: 77 | # print(project["namespace"]["name"] + " ID:" + str(project["id"]) + " 描述:" + project["description"]) 78 | project_ids.append(project["id"]) 79 | page += 1 80 | print("共获取到 " + str(len(project_ids)) + " 个有效项目") 81 | return project_ids 82 | 83 | 84 | # 获取 GitLab 上的项目 id 中的分支 85 | def project_branches(project_id): 86 | branch_names = [] 87 | page = 1 88 | while True: 89 | url = gitlab_url + "api/v4/projects/" + str( 90 | project_id) + "/repository/branches?private_token=" + private_token + "&page=" + str(page) + "&per_page=20" 91 | while True: 92 | try: 93 | res = requests.get(url, headers=headers, timeout=10) 94 | break 95 | except Exception as e: 96 | print(e) 97 | continue 98 | branches = json.loads(res.text) 99 | if len(branches) == 0: 100 | break 101 | else: 102 | for branch in branches: 103 | branch_names.append(branch["name"]) 104 | page += 1 105 | return branch_names 106 | 107 | 108 | # 获取 GitLab 上的项目分支中的 commits,当 title 或 message 首单词为 Merge 时,表示合并操作,剔除此代码量 109 | def project_commits(project_id, branch, start_time, end_time): 110 | commit_ids = [] 111 | page = 1 112 | while True: 113 | url = gitlab_url + "api/v4/projects/" + str( 114 | project_id) + "/repository/commits?ref_name=" + branch + "&private_token=" + private_token + "&page=" + str( 115 | page) + "&per_page=20" 116 | while True: 117 | try: 118 | res = requests.get(url, headers=headers, timeout=10) 119 | break 120 | except Exception as e: 121 | print(e) 122 | continue 123 | commits = json.loads(res.text) 124 | if len(commits) == 0: 125 | break 126 | else: 127 | for commit in commits: 128 | if "Merge" in commit["title"] or "Merge" in commit["message"] or "合并" in commit["title"] or "合并" in \ 129 | commit["message"]: # 不统计合并操作 130 | continue 131 | elif utc_time(commit["authored_date"]) < utc_time(start_time) or utc_time( 132 | commit["authored_date"]) > utc_time(end_time): # 不满足时间区间 133 | continue 134 | else: 135 | commit_ids.append(commit["id"]) 136 | page += 1 137 | return commit_ids 138 | 139 | 140 | # 根据 commits 的 id 获取代码量,type: 1 为主分支,2为其他分支 141 | def commit_code(project_id, commit_id, branch_type): 142 | global info_master, info_other 143 | url = gitlab_url + "api/v4/projects/" + str( 144 | project_id) + "/repository/commits/" + commit_id + "?private_token=" + private_token 145 | while True: 146 | try: 147 | res = requests.get(url, headers=headers, timeout=10) 148 | break 149 | except Exception as e: 150 | print(e) 151 | continue 152 | data = json.loads(res.text) 153 | obj = {"name": data["author_name"], "additions": data["stats"]["additions"], 154 | "deletions": data["stats"]["deletions"], "total": data["stats"]["total"]} # Git工具用户名,新增代码数,删除代码数,总计代码数 155 | if data["author_name"] not in exclude_names: # 去除不需要统计的Git用户名 156 | if branch_type == 1: 157 | info_master.append(obj) 158 | elif branch_type == 2: 159 | info_other.append(obj) 160 | # else: 161 | # do some things 162 | 163 | 164 | # GitLab 数据查询 165 | def gitlab_info(start_time, end_time, project_kind): 166 | for project_id in gitlab_projects(project_kind): # 遍历所有项目ID 167 | for branch_name in project_branches(project_id): # 遍历每个项目中的分支 168 | if branch_name == "master" and project_kind != "user": # 主分支(个人仓直接统计全部) 169 | for commit_id in project_commits(project_id, branch_name, start_time, end_time): # 遍历每个分支中的 commit id 170 | commit_code(project_id, commit_id, 1) # 获取代码提交量 171 | else: # 其他分支 172 | for commit_id in project_commits(project_id, branch_name, start_time, end_time): 173 | commit_code(project_id, commit_id, 2) # 获取代码提交量 174 | 175 | 176 | # 统计数据处理 177 | def gitlab_statistics_data(branch_type): 178 | global statistics 179 | statistics.clear() 180 | 181 | name = [] # Git工具用户名 182 | additions = [] # 新增代码数 183 | deletions = [] # 删除代码数 184 | total = [] # 总计代码数 185 | times = [] # 提交次数 186 | 187 | if branch_type == 1: 188 | info = info_master 189 | elif branch_type == 2: 190 | info = info_other 191 | # else 192 | # do some things 193 | 194 | for i in info: 195 | for key, value in i.items(): 196 | if key == "name": 197 | name.append(value) 198 | if key == "additions": 199 | additions.append(value) 200 | if key == "deletions": 201 | deletions.append(value) 202 | if key == "total": 203 | total.append(value) 204 | times.append(1) # 提交次数 205 | array = list(zip(name, additions, deletions, total, times)) 206 | # print(array) 207 | 208 | # 去重累加 209 | for j in array: 210 | name = j[0] 211 | additions = j[1] 212 | deletions = j[2] 213 | total = j[3] 214 | times = j[4] 215 | if name in statistics.keys(): 216 | statistics[name][0] += additions 217 | statistics[name][1] += deletions 218 | statistics[name][2] += total 219 | statistics[name][3] += times 220 | else: 221 | statistics.update({name: [additions, deletions, total, times]}) 222 | # else: 223 | # do some things 224 | return statistics 225 | 226 | 227 | # 代码统计内容列表处理 228 | def gitlab_statistics_content(file_name): 229 | for i in statistics.keys(): 230 | content_save("
群组仓库
master
Git用户名 | 新增代码数 | 删除代码数 | 总计代码数 | 提交次数 |
---|
dev
Git用户名 | 新增代码数 | 删除代码数 | 总计代码数 | 提交次数 |
---|
个人仓库
all
Git用户名 | 新增代码数 | 删除代码数 | 总计代码数 | 提交次数 |
---|