├── .gitignore
├── LICENSE
├── README.md
├── README.rst
├── README_zh.md
├── gitagent
├── __init__.py
├── __main__.py
├── agent.py
├── auth.py
└── client.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.swo
3 | *.pyc
4 | .DS_Store
5 | gitagent/config.json
6 | dist/*
7 | GitAgent.egg-info/*
8 | __pycache__
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitAgent
2 | A web server receive HTTP request to pull local repository
3 |
4 | [中文文档](https://github.com/alexazhou/GitAgent/blob/master/README_zh.md)
5 |
6 | ## Desc
7 |
8 | GitAgent run as a webserver. It receive command from http requests and do operation to local git repositorys.
9 |
10 | So GitAgent let you can do git operation over http request.
11 |
12 | With GitAgent, you can a git repository on other machine to:
13 |
14 | * get current status
15 | * pull latest code
16 | * checkout branch
17 | ...
18 |
19 | GitAgent also support execute some commant after pull success, and use a password to protect http request.
20 |
21 | ## install
22 |
23 | python3 -m pip install gitagent
24 |
25 | ## require
26 |
27 | GitAgent based on python3, and those libs was required.
28 |
29 | * Tornado
30 | * GitPython
31 | * ws4py
32 |
33 | if you use pip install GitAgent, the requirements will be install automatic.
34 |
35 | ## config
36 |
37 | ##### Basic config format
38 | The basic format of config.json is like this.
39 |
40 | ```
41 | example_config = {
42 | "bind_ip":"0.0.0.0",
43 | "port":10000,
44 | "repo":{
45 | "self":{
46 | "repo_path":"./",
47 | }
48 | },
49 | }
50 | ```
51 | If need, you can put more than one repon into it.
52 |
53 |
54 | ##### full config format
55 |
56 | if you need use password, or execute command after pull, you can add some args to config file like that.
57 |
58 | ```
59 | example_config_full = {
60 | "bind_ip":"0.0.0.0",
61 | "port":10000,
62 | "repo":{
63 | "self":{
64 | "repo_path":"./",
65 | "command":{
66 | "cmd1":"the command 1",
67 | "cmd2":"the command 2",
68 | }
69 | }
70 | },
71 | "password":"123456"
72 | }
73 |
74 | ```
75 |
76 |
77 | ## Usage
78 |
79 | #### step 1: Write default config file
80 | ```python3 -m gitagent [-c config.json] write```
81 |
82 | The default config file will by written to config.json, then you can known the config format.
83 |
84 | if the -c arg don't gived, gitagent will write the config.json to current directory
85 |
86 | #### step 2: Edit the config.json
87 |
88 | Just edit the config file as you need
89 |
90 | #### step 3: Run gitagent
91 | ```python3 -m gitagent [-c config.json] run```
92 |
93 | If you havn't see any error message, the gitagent is running.
94 |
95 | ## API
96 |
97 | #### list all repos
98 |
99 | ```curl -v 'http://localhost:10000/repo'```
100 |
101 | Return:
102 |
103 | ```
104 | [
105 | "demo1",
106 | "demo2",
107 | "demo3"
108 | ]
109 | ```
110 |
111 |
112 | #### repo status
113 |
114 | ```curl -v 'http://localhost:10000/repo/demo1'```
115 |
116 | Return:
117 |
118 | ```
119 | {
120 | {
121 | "author": "AlexaZhou",
122 | "busy": false,
123 | "changed_files": {
124 | "A": [],
125 | "D": [],
126 | "M": [
127 | "agent.py"
128 | ],
129 | "R": []
130 | },
131 | "dirty": true,
132 | "hash": "c8c082d898c2dc18adb8e79f8992c074fb2294ce",
133 | "message": "some message text",
134 | "untracked_files": [
135 | "config.json"
136 | ]
137 | }
138 | ```
139 |
140 | busy means the repo is processing a pull request or other action
141 |
142 | #### repo pull / switch branch / switch hash
143 |
144 | ```curl -v -d 'git_branch=master&git_hash=abcdefg&command=cmd1&block=1' 'http://localhost:10000/repo/demo1/pull'```
145 |
146 | Return:
147 |
148 | ```
149 | {
150 | "ret": "success"
151 | }
152 | ```
153 |
154 | args:
155 |
156 | * ****git_branch****: the branch you want to checkout.
157 | * ****git_hash****: is a optional arg. if git_hash not given, gitagent will checkout lastest commit on the target branch
158 | * ****command****: is a optional arg. if command was gived, it will be execute after pull.
159 | * ****block****: can be 0/1, if block = 1, the request will block until the git work finish
160 |
161 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | GitAgent
2 | ========
3 |
4 | A web server receive HTTP request to pull local repository
5 |
6 | install
7 | -------
8 |
9 | python3 -m pip install gitagent
10 |
11 | require
12 | -------
13 |
14 | GitAgent based on python3, and those libs was required.
15 |
16 | - Tornado
17 | - GitPython
18 |
19 | if you use pip install GitAgent, the requirements will be install
20 | automatic.
21 |
22 | Desc
23 | ----
24 |
25 | GitAgent run as a webserver. It receive command from http requests and
26 | do operation to local git repositorys.
27 |
28 | So GitAgent let you can do git operation over http request.
29 |
30 | With GitAgent, you can a git repository on other machine to:
31 |
32 | - get current status
33 | - pull latest code
34 | - checkout branch …
35 |
36 | GitAgent also support execute some commant after pull success, and use a
37 | password to protect http request.
38 |
39 | You can see detail documents over https://github.com/alexazhou/GitAgent
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | # GitAgent
2 | 一个允许你通过 Http 请求来操作其他机器上 Git 仓库的服务
3 |
4 | ## 介绍
5 |
6 | GitAgent 作为一个web服务来运行. 接收来自 Http 请求的命令来对本地的 Git 仓库进行操作
7 |
8 | 有了 GitAgent,你可以对其他机器上的 Git 仓库做下面这些事情
9 |
10 | * 获取当前仓库的状态
11 | * pull 最新的代码
12 | * checkout 分支/版本
13 | ...
14 |
15 | GitAgent 还支持:
16 |
17 | * 在 pull 成功之后执行指定的命令(主要是为了方便完成部署的附加工作)
18 | * 也允许设置密码来保护接口的安全性
19 | * 通过 websocket 实时回传git pull 和命令执行过程中的日志输出 😎
20 |
21 | ## 安装
22 |
23 | GitAgent 已经封装成库,通过以下命令即可安装
24 |
25 | ```
26 | python3 -m pip install gitagent
27 | ```
28 |
29 |
30 | ## 依赖
31 |
32 | GitAgent 基于 Python3,下面这些库是需要的。
33 |
34 | * Tornado
35 | * GitPython
36 | * ws4py
37 |
38 | 如果你是通过 pip 安装的 GitAgent, 那么这些依赖会被自动装好.
39 |
40 | ## 配置
41 |
42 | ##### 最简配置割格式
43 |
44 | 最简化的配置文件格式如下
45 | ```
46 | example_config = {
47 | "bind_ip":"0.0.0.0",
48 | "port":10000,
49 | "repo":{
50 | "self":{
51 | "repo_path":"./",
52 | }
53 | },
54 | }
55 | ```
56 | 如果需要可以在里面定义多个仓库
57 |
58 |
59 | ##### 完整配置格式
60 |
61 | 如果需要使用密码,或者在 pull 之后执行命令,那么像下面这样多定义一些字段。
62 |
63 | ```
64 | example_config_full = {
65 | "bind_ip":"0.0.0.0",
66 | "port":10000,
67 | "repo":{
68 | "self":{
69 | "repo_path":"./",
70 | "command":{
71 | "cmd1":"the command 1",
72 | "cmd2":"the command 2",
73 | }
74 | }
75 | },
76 | "password":"123456"
77 | }
78 |
79 | ```
80 |
81 | command 中可以定义多个命令,通过 http 请求来控制 pull 的时候,可以指定在 pull 完成之后,来执行这里面的命令
82 |
83 |
84 | ## Usage
85 |
86 | #### step 1: 生成配置文件
87 | ```python3 -m gitagent [-c config.json] write```
88 |
89 | 执行这条命令之后,默认配置模版会被写入到指定的文件
90 |
91 | 如果没有给出 -c 参数, gitagent 会写入配置模板到当前目录的 config.json
92 |
93 | #### step 2: 编辑配置文件
94 |
95 | 按照自己的情况编辑配置模板
96 |
97 | #### step 3: 运行 gitagent
98 | ```python3 -m gitagent [-c config.json] run```
99 |
100 | 如果没有报错,那么gitagent就已经在运行了(目前 gitagent 在前台运行,如果需要的话可以使用 supervisor 使其在后台运行 )
101 |
102 | ## API
103 |
104 | #### 列出当前仓库
105 |
106 | ```curl -v 'http://localhost:10000/repo'```
107 |
108 | Return:
109 |
110 | ```
111 | [
112 | "demo1",
113 | "demo2",
114 | "demo3"
115 | ]
116 | ```
117 |
118 |
119 | ####仓库状态
120 |
121 | ```curl -v 'http://localhost:10000/repo/demo1'```
122 |
123 | Return:
124 |
125 | ```
126 | {
127 | {
128 | "author": "AlexaZhou",
129 | "busy": false,
130 | "changed_files": {
131 | "A": [],
132 | "D": [],
133 | "M": [
134 | "agent.py"
135 | ],
136 | "R": []
137 | },
138 | "dirty": true,
139 | "hash": "c8c082d898c2dc18adb8e79f8992c074fb2294ce",
140 | "message": "some message text",
141 | "untracked_files": [
142 | "config.json"
143 | ]
144 | }
145 | ```
146 |
147 | * author: 当前所在 commit 的作者
148 | * hash: 当前所在 commit 的版本
149 | * message: 当前所在 commit 的 log
150 | * busy: 仓库当前是否正在执行其他的操作
151 | * dirty: 仓库的文件是否有变更
152 | * changed_files: 修改过的文件
153 | * untracked_files: 未跟踪的文件
154 |
155 | #### 对仓库进行 pull / 切换分支 / 切换版本
156 |
157 | ```curl -v -d 'git_branch=master&git_hash=abcdefg&command=cmd1&block=1' 'http://localhost:10000/repo/demo1/pull'```
158 |
159 | Return:
160 |
161 | ```
162 | {
163 | "ret": "success"
164 | }
165 | ```
166 |
167 | 参数:
168 |
169 | * ****git_branch****: 需要 pull/checkout 的分支
170 | * ****git_hash****: 可选参数. 如果没有指定 git_hash, gitagent 将自动 checkout 目标分支上面的最新一个提交
171 | * ****command****: 可选参数. 如果指定了 command,那么这个 command 将会被在 pull 成功之后来执行
172 | * ****block****: 可以是 0/1, 如果 block = 1, 那么这个请求会阻塞到操作全部完成之后才返回
173 |
174 |
175 | #### 身份验证
176 |
177 | 通过在 config.json 中添加 password 项,可以对接口进行保护。
178 | 添加 password 项之后,调用 API 时,需要带上身份验证信息才可以正常使用,否则会被阻止。身份验证信息有以下两种方式:
179 |
180 | * 参数中添加一项 password,值和设置的 password 一致 (安全性弱)
181 | * 参数中添加两项 time 和 sign,其中 time 为当前的时间戳,sign 为使用 password 对请求进行签名的结果
182 |
183 | 第二种方式中签名计算方式如下:
184 |
185 | ```
186 | sign = md5( method + uri + '?' + 参数字符串按字母排序后拼接 + password )
187 |
188 | 当 password 为 123456 时,以这个请求为例:
189 | curl -v -d 'git_branch=master&git_hash=abcdefg&command=cmd1&block=1' 'http://localhost:10000/repo/demo1/pull
190 |
191 | 签名计算方式为:
192 | sign = md5( 'POST' + '/repo/demo1/pull' + '?' + 'block=1&command=cmd1&time=1469938982&git_branch=master&git_hash=abcdefg' + '123456' )
193 |
194 | 计算得到 sign 为 '217ead22e7d680a3fe5a31b0e557b1c7'
195 |
196 | 那最后添加验证信息后的请求应该是
197 | curl -v -d 'git_branch=master&git_hash=abcdefg&command=cmd1&block=1&time=1469938982&sign=217ead22e7d680a3fe5a31b0e557b1c7' 'http://localhost:10000/repo/demo1/pull
198 |
199 | ```
200 |
201 |
202 |
203 |
204 | ## Client
205 |
206 | GitAgent 还包含了一个 client 😈,基于 requests 库,封装了通过 http 请求操作 GitAgent 的相关代码。如果使用 python 的话,只需要通过
207 |
208 | ```from gitagent import client```
209 |
210 | import 之后,就可以直接使用啦
211 |
212 | #### 创建 client 对象
213 |
214 | ```
215 | agent_client = client.AgentClient( SERVER_ADDR, SERVER_PORT, password=None )
216 | ```
217 |
218 | #### 获取仓库列表
219 |
220 | ```
221 | agent_client.repo_list()
222 |
223 | >> ['repo1','repo2','repo3']
224 | ```
225 |
226 | #### 获取仓库状态
227 |
228 | ```
229 | agent_client.repo_status('repo1')
230 | {'untracked_files': ['a.txt', 'config.json', 'xxx.json'], 'busy': False, 'hash': '827b39799a543fee30a174d44cd0c5451776e413', 'dirty': True, 'changed_files': {'R': [], 'A': [], 'D': [], 'M': []}, 'author': 'AlexaZhou', 'branch': 'master', 'message': '\u66f4\u65b0\u6587\u6863\n'}
231 | ```
232 |
233 | #### 对仓库进行操作
234 | ```
235 | agent_client.pull('repo1', branch='master', hash='abcdefg', command='cmd1', block=1)
236 | >>{'ret': 'success', 'err_msg': None}
237 | ```
238 | 注: 参数含义参考前面 API 部分的介绍
239 |
240 |
241 |
242 |
--------------------------------------------------------------------------------
/gitagent/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # @Date : 2016-07-23 10:20
4 | # @Author : Alexa (AlexaZhou@163.com)
5 | # @Link :
6 | # @Disc :
7 |
8 | __version__ = '0.0.16'
9 |
10 |
--------------------------------------------------------------------------------
/gitagent/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # @Date : 2016-07-23
4 | # @Author : Alexa (AlexaZhou@163.com)
5 | # @Link :
6 | # @Disc :
7 |
8 | import sys
9 | import os
10 | import json
11 | import getopt
12 | import gitagent.agent as agent
13 |
14 | help_doc = '''usage: [options] cmd
15 | [-c filename] [--type=simple/full] write: write default config format into a file
16 | [-c filename] run: run with config
17 | '''
18 |
19 | example_config = {
20 | "bind_ip":"0.0.0.0",
21 | "port":10000,
22 | "repo":{
23 | "self":{
24 | "repo_path":"./",
25 | }
26 | },
27 | }
28 |
29 | example_config_full = {
30 | "bind_ip":"0.0.0.0",
31 | "port":10000,
32 | "repo":{
33 | "self":{
34 | "repo_path":"./",
35 | "command":{
36 | "cmd1":"the command 1",
37 | "cmd2":"the command 2",
38 | }
39 | }
40 | },
41 | "password":"123456"
42 | }
43 |
44 | def write_example_config( config_name, config_type='simple' ):
45 | print(' write_example_config ')
46 |
47 | if os.path.exists( config_name ):
48 | print('the file already exists')
49 | sys.exit(1)
50 |
51 | if config_type == 'simple':
52 | config_dict = example_config
53 | elif config_type == 'full':
54 | config_dict = example_config_full
55 | else:
56 | raise Exception("Unknown config type")
57 |
58 | with open( config_name,'w' ) as f:
59 | f.write( json.dumps( config_dict, sort_keys=True, indent=4, ensure_ascii=False ) )
60 | print('write config to %s successed'%config_name)
61 |
62 |
63 | def load_config( config_name ):
64 | print(' load_config:',config_name)
65 | config = None
66 | with open( config_name, 'r' ) as f:
67 | config = json.load(f)
68 |
69 | return config
70 |
71 | def exit_with_message( message ):
72 | print( message )
73 | print(help_doc)
74 | sys.exit(1)
75 |
76 |
77 | if __name__ == "__main__":
78 | opts, args = getopt.getopt(sys.argv[1:], 'c:', ['type='])
79 |
80 | if len(args) != 1:
81 | exit_with_message('args error')
82 |
83 | cmd = args[0]
84 | if cmd not in ['write','run']:
85 | exit_with_message('args error')
86 |
87 | config_name = './config.json'
88 | config_type = 'simple'
89 | for option, value in opts:
90 | #print("option:%s --> value:%s"%(option, value))
91 | if option == '-c':
92 | config_name = value
93 | elif option == '--type':
94 | config_type = value
95 |
96 | if cmd == 'write':
97 | write_example_config( config_name, config_type )
98 | else:
99 | config = load_config( config_name )
100 | agent.set_config( config )
101 | agent.start_server( )
102 |
103 | sys.exit(0)
104 |
105 |
--------------------------------------------------------------------------------
/gitagent/agent.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # @Date : 2016-07-14 14:35
4 | # @Author : Alexa (AlexaZhou@163.com)
5 | # @Link :
6 | # @Disc :
7 |
8 | import os
9 | import fcntl
10 | import subprocess
11 | import tornado.ioloop
12 | import tornado.web
13 | import tornado.websocket
14 | import time
15 | import json
16 | import git
17 | import threading
18 | import logging
19 | from gitagent import auth
20 |
21 |
22 | settings = {
23 | 'debug' : True,
24 | "static_path": os.path.join(os.path.dirname(__file__), "static")
25 | }
26 |
27 | repo_lock = {}
28 | client_sockets = {}
29 |
30 | pretty_json_dump = lambda x:json.dumps( x,sort_keys=True,indent=4,ensure_ascii=False )
31 |
32 | config = None
33 |
34 | def get_config( ):
35 | return config
36 |
37 | def set_config( value ):
38 | global config
39 | config = value
40 |
41 | class git_work_progress( git.RemoteProgress ):
42 | def __init__(self,delegate):
43 | git.RemoteProgress.__init__(self)
44 | self.delegate = delegate
45 |
46 | def update(self,op_code,cur_count,max_count=None,message=""):
47 | print( '-->',op_code,cur_count,max_count,message )
48 | self.delegate.console_output( '\r' + "working with op_code[%s] progress[%s/%s] %s"%(op_code,cur_count,max_count,message) )
49 |
50 | class GitWorker():
51 | def __init__(self, repo_path, git_branch, git_hash, command=None, console_id=None, GIT_SSH_COMMAND=None):
52 | self.repo_path = repo_path
53 | self.git_branch = git_branch
54 | self.git_hash = git_hash
55 | self.console_id = console_id
56 | self.GIT_SSH_COMMAND = GIT_SSH_COMMAND
57 | self.finish_ret = None
58 | self.output = ''
59 | self.err_msg = None
60 | self.command = command
61 |
62 | def console_output(self,s):
63 | print('console [%s]>>'%self.console_id,s )
64 | if self.console_id != None:
65 |
66 | try:
67 | ws_cocket = client_sockets[ self.console_id ]
68 | msg = {}
69 | msg['type'] = 'output'
70 | msg['content'] = s
71 | ws_cocket.write_message( msg )
72 |
73 | except Exception as e:
74 | print('write to websocket failed:',e)
75 |
76 | def non_block_read(self, output):
77 | fd = output.fileno()
78 | fl = fcntl.fcntl(fd, fcntl.F_GETFL)
79 | fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
80 | try:
81 | ret = output.read().decode('utf-8')
82 | if ret == None:
83 | ret = ""
84 | return ret
85 | except:
86 | return ""
87 |
88 | def worker(self):
89 | print( "-"*20 + "git checkout " + "-"*20 )
90 | print( "branch:" + self.git_branch )
91 | print( "hash:" + str(self.git_hash))
92 |
93 | progress_delegate = git_work_progress( self )
94 |
95 | try:
96 | repo=git.Repo( self.repo_path )
97 | if self.GIT_SSH_COMMAND != None:
98 | repo.git.update_environment( GIT_SSH_COMMAND=self.GIT_SSH_COMMAND )
99 |
100 | branch_now = None
101 | if repo.head.is_detached == False:
102 | branch_now = repo.active_branch.name
103 |
104 | print( 'Now repo is on branch:',branch_now )
105 | if self.git_branch in repo.branches:
106 | #make sure on right branch
107 |
108 | if branch_now != self.git_branch:
109 | self.console_output( 'checkout branch %s...'%self.git_branch )
110 | repo.branches[self.git_branch].checkout()
111 | #pull
112 | self.console_output( 'pull...' )
113 | repo.remotes['origin'].pull( progress= progress_delegate )
114 | else:
115 | #if the target branch is not existed in local, checkout out it at first
116 | self.console_output( 'branch %s not existed local. update remote branches...'%self.git_branch )
117 | origin = repo.remotes['origin']
118 | origin.update( )
119 | self.console_output( 'checkout branch %s...'%self.git_branch )
120 | origin.refs[self.git_branch].checkout( b=self.git_branch )
121 |
122 | if self.git_hash != None:
123 | #TODO:判断本地是否存在 hash 对应的commit,如果存在则跳过 pull 的动作
124 | self.console_output( 'git checkout %s...'%self.git_hash )
125 | git_exec = repo.git
126 | git_exec.checkout( self.git_hash )
127 |
128 | if self.command != None:
129 | self.console_output('Exec command: %s'%self.command)
130 | p_command = subprocess.Popen(self.command, shell=True, bufsize=1024000, cwd=self.repo_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
131 | p_returncode = None
132 |
133 | while True:
134 | p_output = self.non_block_read(p_command.stdout)
135 | if len(p_output) != 0:
136 | self.console_output( p_output )
137 |
138 | p_returncode = p_command.poll()
139 | if p_returncode != None:
140 | break
141 |
142 | time.sleep(0.01)
143 |
144 | if p_returncode != 0:
145 | raise Exception("exce command [%s] return code !=0"%self.command)
146 |
147 | self.finish_ret = 'success'
148 | except Exception as e:
149 | print('Exception:',e)
150 | self.err_msg = str(e)
151 | self.finish_ret = 'failed'
152 |
153 | print( "-"*20 + "git checkout finish:" + self.finish_ret + "-"*20 )
154 |
155 | def start(self):
156 | t = threading.Thread( target = self.worker )
157 | t.start()
158 |
159 |
160 | def return_json(fn):
161 | def wrapper( self, *args, **kwargs ):
162 | self.set_header("Content-Type", "application/json; charset=UTF-8")
163 | return fn( self, *args, **kwargs )
164 |
165 | return wrapper
166 |
167 | def verify_request( request ):
168 |
169 | config = get_config()
170 | if 'password' in config:
171 | password = config['password']
172 |
173 | method = request.method
174 | uri = request.path
175 | request_args = {}
176 | for name in request.arguments:
177 | request_args[name] = request.arguments[name][0].decode('utf-8')
178 |
179 | #use password directly auth
180 | if request_args.get('password') == password:
181 | return True
182 |
183 | #use password sign auth
184 | request_time = int(request_args['time'])
185 | if abs(request_time - time.time()) > 60:
186 | print( 'time diff too much, auth failed' )
187 | return False
188 |
189 | sign = request_args['sign']
190 | del request_args['sign']
191 |
192 | sign_right = auth.sign( method, uri, request_args, password, time_stamp = request_args['time'] )['sign']
193 | print( 'sign_right:',sign_right )
194 |
195 | if sign_right != sign:
196 | print( 'verify request failed due to sign not match' )
197 | return False
198 |
199 | return True
200 |
201 | def auth_verify(fn):
202 | def wrapper( self, *args, **kwargs ):
203 | if verify_request(self.request) == False:
204 | raise tornado.web.HTTPError(501)
205 | return fn( self, *args, **kwargs )
206 |
207 | return wrapper
208 |
209 | class MainHandler(tornado.web.RequestHandler):
210 | def get(self):
211 | self.write("Hello, GitAgent~")
212 |
213 | class RepoHandler(tornado.web.RequestHandler):
214 | @return_json
215 | @auth_verify
216 | def get(self):
217 | config = get_config()
218 | ret = list(config['repo'].keys())
219 | self.write( pretty_json_dump(ret) )
220 |
221 | class StatusHandler(tornado.web.RequestHandler):
222 | @return_json
223 | @auth_verify
224 | def get(self,repo):
225 | config = get_config()
226 | if repo not in config['repo']:
227 | raise tornado.web.HTTPError(404)
228 |
229 | repo_path = config['repo'][ repo ]['repo_path']
230 | repo = git.Repo( repo_path )
231 | commit = repo.commit("HEAD")
232 |
233 | info = {}
234 |
235 | if repo.head.is_detached == True:
236 | info['branch'] = None
237 | else:
238 | info['branch'] = repo.active_branch.name
239 |
240 | info['hash'] = commit.hexsha
241 | info['author'] = str(commit.author)
242 | info['message'] = commit.message
243 | info['busy'] = repo in repo_lock
244 | info['dirty'] = repo.is_dirty()
245 | info['untracked_files'] = repo.untracked_files
246 | info['changed_files'] = {}
247 |
248 | change = info['changed_files']
249 | diff = repo.index.diff(None)
250 |
251 | name_getter = lambda diff:diff.a_path
252 | for change_type in "ADRM":
253 | print( 'change_type:',change_type )
254 | change[change_type] = list(map( name_getter, diff.iter_change_type( change_type )))
255 |
256 | self.write( pretty_json_dump(info) )
257 |
258 | class PullHandle(tornado.web.RequestHandler):
259 | @tornado.web.asynchronous
260 | @tornado.gen.coroutine
261 | def post(self,repo):
262 | self.set_header("Content-Type", "application/json; charset=UTF-8")
263 | config = get_config()
264 |
265 | if verify_request(self.request) == False:
266 | raise tornado.web.HTTPError(501)
267 |
268 | if repo not in config['repo']:
269 | raise tornado.web.HTTPError(404)
270 |
271 | block = self.get_argument( 'block', '0')
272 | git_branch = self.get_argument( 'git_branch', 'master')
273 | git_hash = self.get_argument( 'git_hash', None)
274 | console_id = self.get_argument( 'console_id', None)
275 | cmd = self.get_argument( 'command', None)
276 | GIT_SSH_COMMAND = None
277 |
278 | ret = {}
279 | ret['ret'] = 'success'
280 | ret['err_msg'] = None
281 | command = None
282 |
283 | if cmd != None:
284 | if cmd not in config['repo'][repo]['command']:
285 | raise tornado.web.HTTPError(501)
286 | else:
287 | command = config['repo'][repo]['command'][cmd]
288 |
289 | if repo in repo_lock:
290 | ret['ret'] = 'failure'
291 | ret['err_msg'] = 'repo is busying'
292 | self.write( pretty_json_dump(ret))
293 | self.finish()
294 | return
295 | else:
296 | repo_lock[repo] = True
297 |
298 | repo_path = config['repo'][ repo ]['repo_path']
299 | if 'GIT_SSH_COMMAND' in config['repo'][repo]:
300 | GIT_SSH_COMMAND = config['repo'][repo]['GIT_SSH_COMMAND']
301 | print('use GIT_SSH_COMMAND:',GIT_SSH_COMMAND)
302 |
303 | git_worker = GitWorker( repo_path, git_branch, git_hash, command, console_id, GIT_SSH_COMMAND)
304 | git_worker.start()
305 |
306 | if block == '0':#no block
307 | ret['ret'] = 'success'
308 | else:#block until git worker finish
309 | while git_worker.finish_ret == None:
310 | yield tornado.gen.sleep(0.01)
311 |
312 | ret['ret'] = git_worker.finish_ret
313 | ret['err_msg'] = git_worker.err_msg
314 |
315 | self.write( pretty_json_dump(ret))
316 | self.finish()
317 | del repo_lock[repo]
318 |
319 | class CommandHandle(tornado.web.RequestHandler):
320 | def post(self,repo):
321 | pass
322 |
323 | class ConsoleHandler(tornado.websocket.WebSocketHandler):
324 | """docstring for ConsoleHandler"""
325 |
326 | def check_origin(self, origin):
327 | return True
328 |
329 | def open(self):
330 | print("websocket open")
331 | self.write_message(json.dumps({
332 | 'type': 'sys',
333 | 'message': 'Welcome to WebSocket',
334 | 'id': str(id(self)),
335 | }))
336 | client_sockets[ str(id(self)) ] = self
337 |
338 | def on_close(self):
339 | print("websocket close")
340 | del client_sockets[ str(id(self)) ]
341 |
342 |
343 | application = tornado.web.Application([
344 | ("/repo/([^/]+)/exec", CommandHandle),
345 | ("/repo/([^/]+)/pull", PullHandle),
346 | ("/repo/([^/]+)", StatusHandler),
347 | ("/repo", RepoHandler ),
348 | ("/console", ConsoleHandler ),
349 | ("/", MainHandler),
350 | ],**settings)
351 |
352 | def start_server():
353 |
354 | logging.basicConfig()
355 | logging.root.setLevel(logging.INFO)
356 |
357 | config = get_config()
358 | application.listen( config['port'], address=config['bind_ip'] )
359 | tornado.ioloop.IOLoop.instance().start()
360 |
--------------------------------------------------------------------------------
/gitagent/auth.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # @Date : 2016-07-24
4 | # @Author : Alexa (AlexaZhou@163.com)
5 | # @Link :
6 | # @Disc :
7 |
8 | import time
9 | import hashlib
10 |
11 | def sign( method, uri, args, sign_key, time_stamp = None ):
12 | if time_stamp == None:
13 | time_stamp = str(int(time.time()))
14 |
15 | args['time'] = time_stamp
16 | args_keys = list(args.keys())
17 | args_keys.sort()
18 |
19 | args_str = ''
20 | for key in args_keys:
21 | if len(args_str) != 0:
22 | args_str += '&'
23 |
24 | args_str += key + '=' + str(args[key])
25 |
26 | str_to_sign = method + uri + '?' + args_str + sign_key
27 | #print('str_to_sign:',str_to_sign)
28 |
29 | m = hashlib.md5()
30 | m.update(str_to_sign.encode('utf-8'))
31 |
32 | #print('md5:%s'%m.hexdigest())
33 | args['sign'] = m.hexdigest()
34 | return args
35 |
--------------------------------------------------------------------------------
/gitagent/client.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # @Date : 2016-07-24
4 | # @Author : Alexa (AlexaZhou@163.com)
5 | # @Link :
6 | # @Disc :
7 |
8 | import requests
9 | import threading
10 | import time
11 | import json
12 | from gitagent import auth
13 | from ws4py.client.threadedclient import WebSocketClient
14 |
15 |
16 | class WebSocketConsole(WebSocketClient):
17 | def __init__(self, ip, port, console_receiver):
18 | self.websocket_id = None
19 | self.console_receiver = console_receiver
20 | url = "ws://%s:%s/console"%(ip, port)
21 | WebSocketClient.__init__(self, url, protocols=['http-only', 'chat'])
22 |
23 | def opened(self):
24 | self.console_output('websocket console connected')
25 |
26 | def closed(self, code, reason=None):
27 | self.console_output("websocket console disconnected")
28 |
29 | def received_message(self, m):
30 | #print('console ->',m)
31 | data = json.loads( m.data.decode('utf-8') )
32 | if data['type'] == 'sys':
33 | self.websocket_id = data['id']
34 | elif data['type'] == 'output':
35 | self.console_output( data['content'] )
36 |
37 | def console_output(self,s):
38 | self.console_receiver( s )
39 |
40 |
41 | class AgentClient():
42 | def __init__(self, ip, port, password=None, console_receiver=print):
43 | self.web_console = None
44 | self.ip = ip
45 | self.port = str(port)
46 | self.base_url = ip + ':' + self.port
47 | self.password = password
48 | self.console_receiver = console_receiver
49 |
50 | def request_sign(self, method, uri, args):
51 |
52 | if self.password != None:
53 | args = auth.sign( method, uri, args, self.password )
54 |
55 | return args
56 |
57 | def repo_list(self):
58 | uri = '/repo'
59 | args = {}
60 | args = self.request_sign( 'GET', uri, args )
61 |
62 | r = requests.get( 'http://' + self.base_url + uri, data=args, timeout=10 )
63 | if r.status_code != 200:
64 | raise Exception('Request failed with status_code:%s response:%s'%(r.status_code, r.content))
65 | return r.json()
66 |
67 | def repo_status(self, repo):
68 | uri = '/repo/' + repo
69 | args = {}
70 | args = self.request_sign( 'GET', uri, args )
71 | r = requests.get( 'http://' + self.base_url + uri , data=args, timeout=10 )
72 | if r.status_code != 200:
73 | raise Exception('Request failed with status_code:%s response:%s'%(r.status_code, r.content))
74 | return r.json()
75 |
76 | def pull(self, repo, git_branch='master', git_hash=None, command=None, block = 1):
77 | args ={ 'git_branch':git_branch, 'block':block }
78 | if git_hash != None:
79 | args['git_hash'] = git_hash
80 |
81 | if self.web_console != None:
82 | args['console_id'] = self.web_console.websocket_id
83 |
84 | if command != None:
85 | args['command'] = command
86 |
87 | uri = '/repo/' + repo + '/pull'
88 | args = self.request_sign( 'POST', uri, args )
89 |
90 | #print( 'connect GitAgent to deploy...' )
91 | r = requests.post( 'http://' + self.base_url + uri , data=args, timeout=600 )
92 | if r.status_code != 200:
93 | raise Exception('Request failed with status_code:%s response:%s'%(r.status_code, r.content))
94 | r_json = r.json()
95 | return r_json
96 |
97 | def connect_websocket(self):
98 | self.web_console = WebSocketConsole( self.ip, self.port, self.console_receiver )
99 | def console_worker():
100 | self.web_console.connect()
101 | self.web_console.run_forever()
102 |
103 | t = threading.Thread( target=console_worker )
104 | t.setDaemon( True )
105 | t.start()
106 |
107 | time_begin = time.time()
108 | while time.time() - time_begin < 5:
109 | if self.web_console.websocket_id != None:
110 | break
111 | time.sleep(0.1)
112 | else:
113 | raise Exception('WebSocket Connect Error')
114 |
115 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # @Date : 2016-07-23 10:20
4 | # @Author : Alexa (AlexaZhou@163.com)
5 | # @Link :
6 | # @Disc :
7 |
8 | """A setuptools based setup module.
9 | See:
10 | https://packaging.python.org/en/latest/distributing.html
11 | https://github.com/pypa/sampleproject
12 | """
13 |
14 | # Always prefer setuptools over distutils
15 | from setuptools import setup, find_packages
16 | # To use a consistent encoding
17 | from codecs import open
18 | from os import path
19 |
20 | import gitagent
21 |
22 | here = path.abspath(path.dirname(__file__))
23 |
24 | # Get the long description from the README file
25 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
26 | long_description = f.read()
27 |
28 | setup(
29 | name='GitAgent',
30 |
31 | # Versions should comply with PEP440. For a discussion on single-sourcing
32 | # the version across setup.py and the project code, see
33 | # https://packaging.python.org/en/latest/single_source_version.html
34 | version=gitagent.__version__,
35 |
36 | description='A web server receive HTTP request to pull local repository',
37 | long_description=long_description,
38 |
39 | # The project's main homepage.
40 | url='https://github.com/alexazhou/GitAgent',
41 |
42 | # Author details
43 | author='AlexaZhou',
44 | author_email='AlexaZhou@163.com',
45 |
46 | # Choose your license
47 | license='LGPL',
48 |
49 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
50 | classifiers=[
51 | # How mature is this project? Common values are
52 | # 3 - Alpha
53 | # 4 - Beta
54 | # 5 - Production/Stable
55 | 'Development Status :: 3 - Alpha',
56 |
57 | # Indicate who your project is intended for
58 | 'Intended Audience :: Developers',
59 | 'Topic :: Software Development :: Build Tools',
60 |
61 | # Pick your license as you wish (should match "license" above)
62 | 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)',
63 |
64 | # Specify the Python versions you support here. In particular, ensure
65 | # that you indicate whether you support Python 2, Python 3 or both.
66 | 'Programming Language :: Python :: 3',
67 | 'Programming Language :: Python :: 3.3',
68 | 'Programming Language :: Python :: 3.4',
69 | 'Programming Language :: Python :: 3.5',
70 | ],
71 |
72 | # What does your project relate to?
73 | keywords='git pull hook',
74 |
75 | # You can just specify the packages manually here if your project is
76 | # simple. Or you can use find_packages().
77 | packages=find_packages(exclude=['contrib', 'docs', 'tests']),
78 |
79 | # Alternatively, if you want to distribute just a my_module.py, uncomment
80 | # this:
81 | # py_modules=["my_module"],
82 |
83 | # List run-time dependencies here. These will be installed by pip when
84 | # your project is installed. For an analysis of "install_requires" vs pip's
85 | # requirements files see:
86 | # https://packaging.python.org/en/latest/requirements.html
87 | install_requires=['tornado','GitPython','requests','ws4py'],
88 |
89 | # List additional groups of dependencies here (e.g. development
90 | # dependencies). You can install these using the following syntax,
91 | # for example:
92 | # $ pip install -e .[dev,test]
93 | extras_require={
94 | 'dev': [],
95 | 'test': [],
96 | },
97 |
98 | # If there are data files included in your packages that need to be
99 | # installed, specify them here. If using Python 2.6 or less, then these
100 | # have to be included in MANIFEST.in as well.
101 | package_data={
102 | 'sample': [],
103 | },
104 |
105 | # Although 'package_data' is the preferred approach, in some case you may
106 | # need to place data files outside of your packages. See:
107 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
108 | # In this case, 'data_file' will be installed into '/my_data'
109 | data_files=[ ],
110 |
111 | # To provide executable scripts, use entry points in preference to the
112 | # "scripts" keyword. Entry points provide cross-platform support and allow
113 | # pip to create the appropriate form of executable for the target platform.
114 | entry_points={
115 | },
116 | )
117 |
--------------------------------------------------------------------------------