├── .ghuser.io.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README ├── README.html ├── README.md ├── backlinks.txt ├── gicowa ├── __init__.py ├── gicowa.py └── impl │ ├── __init__.py │ ├── encoding.py │ ├── mail.py │ ├── output.py │ ├── persistence.py │ └── timestamp.py ├── github_icon.png ├── maintain.md ├── requirements.txt ├── scripts └── gen_doc.sh ├── setup.py └── test ├── README.md ├── __init__.py ├── test_gicowa.py └── test_mail.py /.ghuser.io.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Repo metadata for ghuser.io. See https://github.com/ghuser-io/ghuser.io/blob/master/docs/repo-settings.md", 3 | "avatar_url": "https://cdn.jsdelivr.net/gh/AurelienLourot/github-commit-watcher@21449af89fe668a318f392f84322cdd368452a92/github_icon.png", 4 | "techs": ["GitHub API", "Travis CI"] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /gicowa.egg-info/ 3 | *.pyc 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | install: 5 | - pip install -r requirements.txt 6 | script: 7 | - python -m unittest discover -v 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | .. Generated by: 3 | $ ./scripts/gen_doc.sh 4 | 5 | 6 | Official documentation `here `_. 7 | 8 | gicowa.py - GitHub Commit Watcher 9 | ================================= 10 | 11 | GitHub's *Watch* feature doesn't send notifications when commits are 12 | pushed. This script aims to implement this feature and much more. 13 | 14 | **Call for maintainers:** I don't use this project myself anymore 15 | but IFTTT instead (see below). If you're interested in taking over 16 | the maintenance of this project, or just helping, please let me know 17 | (e.g. by opening an issue). 18 | 19 | Installation 20 | ------------ 21 | 22 | :: 23 | 24 | $ sudo apt-get install sendmail 25 | $ sudo pip install gicowa 26 | 27 | Quick setup 28 | ----------- 29 | 30 | Add the following line to your ``/etc/crontab``: 31 | 32 | :: 33 | 34 | 0 * * * * root gicowa --persist --no-color --mailto myself@mydomain.com lastwatchedcommits MyGitHubUsername sincelast > /tmp/gicowa 2>&1 35 | 36 | **That's it.** As long as your machine is running you'll get e-mails 37 | when something gets pushed on a repo you're watching. 38 | 39 | **NOTES:** 40 | 41 | - The e-mails are likely to be considered as spam until you mark 42 | one as non-spam in your e-mail client. Or use the ``--mailfrom`` 43 | option. 44 | - If you're watching 15 repos or more, you probably want to use the 45 | ``--credentials`` option to make sure you don't hit the GitHub 46 | API rate limit. 47 | 48 | Other/Advanced usage 49 | -------------------- 50 | 51 | ``gicowa`` is a generic command-line tool with which you can make much 52 | more that just implementing the use case depicted in the introduction. 53 | This section shows what it can. 54 | 55 | List repos watched by a user 56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | 58 | :: 59 | 60 | $ gicowa watchlist AurelienLourot 61 | watchlist AurelienLourot 62 | brandon-rhodes/uncommitted 63 | AurelienLourot/crouton-emacs-conf 64 | brillout/FasterWeb 65 | AurelienLourot/github-commit-watcher 66 | 67 | List last commits on a repo 68 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 69 | 70 | :: 71 | 72 | $ gicowa lastrepocommits AurelienLourot/github-commit-watcher since 2015 07 05 09 12 00 73 | lastrepocommits AurelienLourot/github-commit-watcher since 2015-07-05 09:12:00 74 | Last commit pushed on 2015-07-05 10:48:58 75 | Committed on 2015-07-05 10:46:27 - Aurelien Lourot - Minor cleanup. 76 | Committed on 2015-07-05 09:39:01 - Aurelien Lourot - watchlist command implemented. 77 | Committed on 2015-07-05 09:12:00 - Aurelien Lourot - argparse added. 78 | 79 | -------------- 80 | 81 | **NOTES:** 82 | 83 | - Keep in mind that a commit's *committer timestamp* isn't the time 84 | at which it gets pushed. 85 | - The lines starting with ``Committed on`` list commits on the 86 | ``master`` branch only. Their timestamps are the *committer 87 | timestamps*. 88 | - The line starting with ``Last commit pushed on`` shows the time 89 | at which a commit got pushed on the repository for the last time 90 | on any branch. 91 | 92 | List last commits on repos watched by a user 93 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 94 | 95 | :: 96 | 97 | $ gicowa lastwatchedcommits AurelienLourot since 2015 07 04 00 00 00 98 | lastwatchedcommits AurelienLourot since 2015-07-04 00:00:00 99 | AurelienLourot/crouton-emacs-conf - Last commit pushed on 2015-07-04 17:10:18 100 | AurelienLourot/crouton-emacs-conf - Committed on 2015-07-04 17:08:48 - Aurelien Lourot - Support for Del key. 101 | brillout/FasterWeb - Last commit pushed on 2015-07-04 16:40:54 102 | brillout/FasterWeb - Committed on 2015-07-04 16:38:55 - brillout - add README 103 | AurelienLourot/github-commit-watcher - Last commit pushed on 2015-07-05 10:48:58 104 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 10:46:27 - Aurelien Lourot - Minor cleanup. 105 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:39:01 - Aurelien Lourot - watchlist command implemented. 106 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:12:00 - Aurelien Lourot - argparse added. 107 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:07:14 - AurelienLourot - Initial commit 108 | 109 | -------------- 110 | 111 | **NOTE:** if you're watching 15 repos or more, you probably want to 112 | use the ``--credentials`` option to make sure you don't hit the 113 | GitHub API rate limit. 114 | 115 | List last commits since last run 116 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 117 | 118 | Any listing command taking a ``since `` argument takes also a 119 | ``sincelast`` one. It will then use the time where that same command has 120 | been run for the last time on that machine with the option 121 | ``--persist``. This option makes ``gicowa`` remember the last execution 122 | time of each command in ``~/.gicowa``. 123 | 124 | :: 125 | 126 | $ gicowa --persist lastwatchedcommits AurelienLourot sincelast 127 | lastwatchedcommits AurelienLourot since 2015-07-05 20:17:46 128 | $ gicowa --persist lastwatchedcommits AurelienLourot sincelast 129 | lastwatchedcommits AurelienLourot since 2015-07-05 20:25:33 130 | 131 | Send output by e-mail 132 | ~~~~~~~~~~~~~~~~~~~~~ 133 | 134 | You can send the output of any command to yourself by e-mail: 135 | 136 | :: 137 | 138 | $ gicowa --no-color --mailto myself@mydomain.com lastwatchedcommits AurelienLourot since 2015 07 04 00 00 00 139 | lastwatchedcommits AurelienLourot since 2015-07-04 00:00:00 140 | AurelienLourot/crouton-emacs-conf - Last commit pushed on 2015-07-04 17:10:18 141 | AurelienLourot/crouton-emacs-conf - Committed on 2015-07-04 17:08:48 - Aurelien Lourot - Support for Del key. 142 | brillout/FasterWeb - Last commit pushed on 2015-07-04 16:40:54 143 | brillout/FasterWeb - Committed on 2015-07-04 16:38:55 - brillout - add README 144 | AurelienLourot/github-commit-watcher - Last commit pushed on 2015-07-05 10:48:58 145 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 10:46:27 - Aurelien Lourot - Minor cleanup. 146 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:39:01 - Aurelien Lourot - watchlist command implemented. 147 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:12:00 - Aurelien Lourot - argparse added. 148 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:07:14 - AurelienLourot - Initial commit 149 | Sent by e-mail to myself@mydomain.com 150 | 151 | -------------- 152 | 153 | **NOTES:** 154 | 155 | - You probably want to use ``--no-color`` because your e-mail 156 | client is likely not to render the bash color escape sequences 157 | properly. 158 | - The e-mails are likely to be considered as spam until you mark 159 | one as non-spam in your e-mail client. Or use the ``--mailfrom`` 160 | option. 161 | 162 | Changelog 163 | --------- 164 | 165 | **1.2.3** (2015-10-17) to **1.2.5** (2015-10-19): 166 | 167 | - Exception on non-ASCII characters fixed. 168 | 169 | **1.2.2** (2015-10-12): 170 | 171 | - Machine name appended to e-mail content. 172 | 173 | **1.2.1** (2015-08-20): 174 | 175 | - Documentation improved. 176 | 177 | **1.2.0** (2015-08-20): 178 | 179 | - ``--version`` option implemented. 180 | 181 | **1.1.0** (2015-08-20): 182 | 183 | - ``--errorto`` option implemented. 184 | 185 | **1.0.1** (2015-08-18) to **1.0.9** (2015-08-19): 186 | 187 | - Documentation improved. 188 | 189 | Contributors 190 | ------------ 191 | 192 | - `Aurelien Lourot `__ 193 | - `cassiodoroVicinetti `__ 194 | 195 | Similar projects 196 | ---------------- 197 | 198 | The following projects provide similar functionalities: 199 | 200 | - `IFTTT `__, see `this 201 | post `__. 202 | - `Zapier `__, however you have to create a "Zap" 203 | for each single project you want to watch. See `this 204 | thread `__. 205 | - `HubNotify `__, however you will be notified 206 | only for new tags, not new commits. 207 | 208 | -------------------------------------------------------------------------------- /README.html: -------------------------------------------------------------------------------- 1 |

gicowa.py - GitHub Commit Watcher

2 | 3 |

GitHub's Watch feature doesn't send notifications when commits are pushed. This script 4 | aims to implement this feature and much more.

5 | 6 |
7 | Call for maintainers: I don't use this project myself anymore but IFTTT instead (see below). 8 | If you're interested in taking over the maintenance of this project, or just helping, please let me 9 | know (e.g. by opening an issue). 10 |
11 | 12 |

Installation

13 | 14 |
 15 | $ sudo apt-get install sendmail
 16 | $ sudo pip install gicowa
 17 | 
18 | 19 |

Quick setup

20 | 21 |

Add the following line to your /etc/crontab:

22 | 23 |
 24 | 0 * * * * root gicowa --persist --no-color --mailto myself@mydomain.com lastwatchedcommits MyGitHubUsername sincelast > /tmp/gicowa 2>&1
 25 | 
26 | 27 |

That's it. As long as your machine is running you'll get e-mails when something gets 28 | pushed on a repo you're watching.

29 | 30 |
31 | NOTES: 32 |
    33 |
  • The e-mails are likely to be considered as spam until you mark one as non-spam in your e-mail 34 | client. Or use the --mailfrom option.
  • 35 |
  • If you're watching 15 repos or more, you probably want to use the --credentials 36 | option to make sure you don't hit the GitHub API rate limit.
  • 37 |
38 |
39 | 40 |

Other/Advanced usage

41 | 42 |

gicowa is a generic command-line tool with which you can make much more that just 43 | implementing the use case depicted in the introduction. This section shows what it can.

44 | 45 |

List repos watched by a user

46 | 47 |
 48 | $ gicowa watchlist AurelienLourot
 49 | watchlist AurelienLourot
 50 | brandon-rhodes/uncommitted
 51 | AurelienLourot/crouton-emacs-conf
 52 | brillout/FasterWeb
 53 | AurelienLourot/github-commit-watcher
 54 | 
55 | 56 |

List last commits on a repo

57 | 58 |
 59 | $ gicowa lastrepocommits AurelienLourot/github-commit-watcher since 2015 07 05 09 12 00
 60 | lastrepocommits AurelienLourot/github-commit-watcher since 2015-07-05 09:12:00
 61 | Last commit pushed on 2015-07-05 10:48:58
 62 | Committed on 2015-07-05 10:46:27 - Aurelien Lourot - Minor cleanup.
 63 | Committed on 2015-07-05 09:39:01 - Aurelien Lourot - watchlist command implemented.
 64 | Committed on 2015-07-05 09:12:00 - Aurelien Lourot - argparse added.
 65 | 
66 | 67 |
68 | 69 |
70 | NOTES: 71 |
    72 |
  • Keep in mind that a commit's committer timestamp isn't the time at which it gets pushed. 73 |
  • 74 |
  • The lines starting with Committed on list commits on the master branch 75 | only. Their timestamps are the committer timestamps.
  • 76 |
  • The line starting with Last commit pushed on shows the time at which a commit got 77 | pushed on the repository for the last time on any branch.
  • 78 |
79 |
80 | 81 |

List last commits on repos watched by a user

82 | 83 |
 84 | $ gicowa lastwatchedcommits AurelienLourot since 2015 07 04 00 00 00
 85 | lastwatchedcommits AurelienLourot since 2015-07-04 00:00:00
 86 | AurelienLourot/crouton-emacs-conf - Last commit pushed on 2015-07-04 17:10:18
 87 | AurelienLourot/crouton-emacs-conf - Committed on 2015-07-04 17:08:48 - Aurelien Lourot - Support for Del key.
 88 | brillout/FasterWeb - Last commit pushed on 2015-07-04 16:40:54
 89 | brillout/FasterWeb - Committed on 2015-07-04 16:38:55 - brillout - add README
 90 | AurelienLourot/github-commit-watcher - Last commit pushed on 2015-07-05 10:48:58
 91 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 10:46:27 - Aurelien Lourot - Minor cleanup.
 92 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:39:01 - Aurelien Lourot - watchlist command implemented.
 93 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:12:00 - Aurelien Lourot - argparse added.
 94 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:07:14 - AurelienLourot - Initial commit
 95 | 
96 | 97 |
98 | 99 |
100 | NOTE: if you're watching 15 repos or more, you probably want to use the 101 | --credentials option to make sure you don't hit the GitHub API rate limit. 102 |
103 | 104 |

List last commits since last run

105 | 106 |

Any listing command taking a since <timestamp> argument takes also a 107 | sincelast one. It will then use the time where that same command has been run for the 108 | last time on that machine with the option --persist. This option makes 109 | gicowa remember the last execution time of each command in ~/.gicowa.

110 | 111 |
112 | $ gicowa --persist lastwatchedcommits AurelienLourot sincelast
113 | lastwatchedcommits AurelienLourot since 2015-07-05 20:17:46
114 | $ gicowa --persist lastwatchedcommits AurelienLourot sincelast
115 | lastwatchedcommits AurelienLourot since 2015-07-05 20:25:33
116 | 
117 | 118 |

Send output by e-mail

119 | 120 |

You can send the output of any command to yourself by e-mail:

121 | 122 |
123 | $ gicowa --no-color --mailto myself@mydomain.com lastwatchedcommits AurelienLourot since 2015 07 04 00 00 00
124 | lastwatchedcommits AurelienLourot since 2015-07-04 00:00:00
125 | AurelienLourot/crouton-emacs-conf - Last commit pushed on 2015-07-04 17:10:18
126 | AurelienLourot/crouton-emacs-conf - Committed on 2015-07-04 17:08:48 - Aurelien Lourot - Support for Del key.
127 | brillout/FasterWeb - Last commit pushed on 2015-07-04 16:40:54
128 | brillout/FasterWeb - Committed on 2015-07-04 16:38:55 - brillout - add README
129 | AurelienLourot/github-commit-watcher - Last commit pushed on 2015-07-05 10:48:58
130 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 10:46:27 - Aurelien Lourot - Minor cleanup.
131 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:39:01 - Aurelien Lourot - watchlist command implemented.
132 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:12:00 - Aurelien Lourot - argparse added.
133 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:07:14 - AurelienLourot - Initial commit
134 | Sent by e-mail to myself@mydomain.com
135 | 
136 | 137 |
138 | 139 |
140 | NOTES: 141 |
    142 |
  • You probably want to use --no-color because your e-mail client is likely not to 143 | render the bash color escape sequences properly.
  • 144 |
  • The e-mails are likely to be considered as spam until you mark one as non-spam in your e-mail 145 | client. Or use the --mailfrom option.
  • 146 |
147 |
148 | 149 |

Changelog

150 | 151 |

1.2.3 (2015-10-17) to 1.2.5 (2015-10-19):

152 | 153 |
    154 |
  • Exception on non-ASCII characters fixed.
  • 155 |
156 | 157 |

1.2.2 (2015-10-12):

158 | 159 |
    160 |
  • Machine name appended to e-mail content.
  • 161 |
162 | 163 |

1.2.1 (2015-08-20):

164 | 165 |
    166 |
  • Documentation improved.
  • 167 |
168 | 169 |

1.2.0 (2015-08-20):

170 | 171 |
    172 |
  • --version option implemented.
  • 173 |
174 | 175 |

1.1.0 (2015-08-20):

176 | 177 |
    178 |
  • --errorto option implemented.
  • 179 |
180 | 181 |

1.0.1 (2015-08-18) to 1.0.9 (2015-08-19):

182 | 183 |
    184 |
  • Documentation improved.
  • 185 |
186 | 187 |

Contributors

188 | 189 | 193 | 194 |

Similar projects

195 | 196 |

The following projects provide similar functionalities:

197 |
    198 |
  • IFTTT, see 199 | this post.
  • 200 |
  • Zapier, however you have to create a "Zap" for each single 201 | project you want to watch. See 202 | this thread.
  • 203 |
  • HubNotify, however you will be notified only for new tags, 204 | not new commits.
  • 205 |
206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | [![Build Status](https://travis-ci.org/AurelienLourot/github-commit-watcher.svg?branch=master)](https://travis-ci.org/AurelienLourot/github-commit-watcher) 8 | 9 | Official documentation [here](http://a.ghuser.io/gicowa). 10 | 11 | # gicowa.py - GitHub Commit Watcher 12 | 13 | GitHub's _Watch_ feature doesn't send notifications when commits are pushed. 14 | This script aims to implement this feature and much more. 15 | 16 | > **Call for maintainers:** I don't use this project myself anymore but IFTTT 17 | instead (see below). If you're interested in taking over the maintenance of 18 | this project, or just helping, please let me know (e.g. by opening an issue). 19 | 20 | ## Installation 21 | 22 | 23 | 24 | $ sudo apt-get install sendmail 25 | $ sudo pip install gicowa 26 | 27 | 28 | ## Quick setup 29 | 30 | Add the following line to your `/etc/crontab`: 31 | 32 | 33 | 34 | 0 * * * * root gicowa --persist --no-color --mailto myself@mydomain.com lastwatchedcommits MyGitHubUsername sincelast > /tmp/gicowa 2>&1 35 | 36 | 37 | **That's it.** As long as your machine is running you'll get e-mails when something gets pushed on a repo you're watching. 38 | 39 | > **NOTES:** 40 | 41 | > 42 | 43 | > * The e-mails are likely to be considered as spam until you mark one as 44 | non-spam in your e-mail client. Or use the `--mailfrom` option. 45 | 46 | > * If you're watching 15 repos or more, you probably want to use the 47 | `--credentials` option to make sure you don't hit the GitHub API rate limit. 48 | 49 | ## Other/Advanced usage 50 | 51 | `gicowa` is a generic command-line tool with which you can make much more that 52 | just implementing the use case depicted in the introduction. This section 53 | shows what it can. 54 | 55 | ### List repos watched by a user 56 | 57 | 58 | 59 | $ gicowa watchlist AurelienLourot 60 | watchlist AurelienLourot 61 | brandon-rhodes/uncommitted 62 | AurelienLourot/crouton-emacs-conf 63 | brillout/FasterWeb 64 | AurelienLourot/github-commit-watcher 65 | 66 | 67 | ### List last commits on a repo 68 | 69 | 70 | 71 | $ gicowa lastrepocommits AurelienLourot/github-commit-watcher since 2015 07 05 09 12 00 72 | lastrepocommits AurelienLourot/github-commit-watcher since 2015-07-05 09:12:00 73 | Last commit pushed on 2015-07-05 10:48:58 74 | Committed on 2015-07-05 10:46:27 - Aurelien Lourot - Minor cleanup. 75 | Committed on 2015-07-05 09:39:01 - Aurelien Lourot - watchlist command implemented. 76 | Committed on 2015-07-05 09:12:00 - Aurelien Lourot - argparse added. 77 | 78 | 79 | * * * 80 | 81 | > **NOTES:** 82 | 83 | > 84 | 85 | > * Keep in mind that a commit's _committer timestamp_ isn't the time at 86 | which it gets pushed. 87 | 88 | > * The lines starting with `Committed on` list commits on the `master` 89 | branch only. Their timestamps are the _committer timestamps_. 90 | 91 | > * The line starting with `Last commit pushed on` shows the time at which a 92 | commit got pushed on the repository for the last time on any branch. 93 | 94 | ### List last commits on repos watched by a user 95 | 96 | 97 | 98 | $ gicowa lastwatchedcommits AurelienLourot since 2015 07 04 00 00 00 99 | lastwatchedcommits AurelienLourot since 2015-07-04 00:00:00 100 | AurelienLourot/crouton-emacs-conf - Last commit pushed on 2015-07-04 17:10:18 101 | AurelienLourot/crouton-emacs-conf - Committed on 2015-07-04 17:08:48 - Aurelien Lourot - Support for Del key. 102 | brillout/FasterWeb - Last commit pushed on 2015-07-04 16:40:54 103 | brillout/FasterWeb - Committed on 2015-07-04 16:38:55 - brillout - add README 104 | AurelienLourot/github-commit-watcher - Last commit pushed on 2015-07-05 10:48:58 105 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 10:46:27 - Aurelien Lourot - Minor cleanup. 106 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:39:01 - Aurelien Lourot - watchlist command implemented. 107 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:12:00 - Aurelien Lourot - argparse added. 108 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:07:14 - AurelienLourot - Initial commit 109 | 110 | 111 | * * * 112 | 113 | > **NOTE:** if you're watching 15 repos or more, you probably want to use the 114 | `--credentials` option to make sure you don't hit the GitHub API rate limit. 115 | 116 | ### List last commits since last run 117 | 118 | Any listing command taking a `since ` argument takes also a 119 | `sincelast` one. It will then use the time where that same command has been 120 | run for the last time on that machine with the option `--persist`. This option 121 | makes `gicowa` remember the last execution time of each command in 122 | `~/.gicowa`. 123 | 124 | 125 | 126 | $ gicowa --persist lastwatchedcommits AurelienLourot sincelast 127 | lastwatchedcommits AurelienLourot since 2015-07-05 20:17:46 128 | $ gicowa --persist lastwatchedcommits AurelienLourot sincelast 129 | lastwatchedcommits AurelienLourot since 2015-07-05 20:25:33 130 | 131 | 132 | ### Send output by e-mail 133 | 134 | You can send the output of any command to yourself by e-mail: 135 | 136 | 137 | 138 | $ gicowa --no-color --mailto myself@mydomain.com lastwatchedcommits AurelienLourot since 2015 07 04 00 00 00 139 | lastwatchedcommits AurelienLourot since 2015-07-04 00:00:00 140 | AurelienLourot/crouton-emacs-conf - Last commit pushed on 2015-07-04 17:10:18 141 | AurelienLourot/crouton-emacs-conf - Committed on 2015-07-04 17:08:48 - Aurelien Lourot - Support for Del key. 142 | brillout/FasterWeb - Last commit pushed on 2015-07-04 16:40:54 143 | brillout/FasterWeb - Committed on 2015-07-04 16:38:55 - brillout - add README 144 | AurelienLourot/github-commit-watcher - Last commit pushed on 2015-07-05 10:48:58 145 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 10:46:27 - Aurelien Lourot - Minor cleanup. 146 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:39:01 - Aurelien Lourot - watchlist command implemented. 147 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:12:00 - Aurelien Lourot - argparse added. 148 | AurelienLourot/github-commit-watcher - Committed on 2015-07-05 09:07:14 - AurelienLourot - Initial commit 149 | Sent by e-mail to myself@mydomain.com 150 | 151 | 152 | * * * 153 | 154 | > **NOTES:** 155 | 156 | > 157 | 158 | > * You probably want to use `--no-color` because your e-mail client is 159 | likely not to render the bash color escape sequences properly. 160 | 161 | > * The e-mails are likely to be considered as spam until you mark one as 162 | non-spam in your e-mail client. Or use the `--mailfrom` option. 163 | 164 | ## Changelog 165 | 166 | **1.2.3** (2015-10-17) to **1.2.5** (2015-10-19): 167 | 168 | * Exception on non-ASCII characters fixed. 169 | 170 | **1.2.2** (2015-10-12): 171 | 172 | * Machine name appended to e-mail content. 173 | 174 | **1.2.1** (2015-08-20): 175 | 176 | * Documentation improved. 177 | 178 | **1.2.0** (2015-08-20): 179 | 180 | * `--version` option implemented. 181 | 182 | **1.1.0** (2015-08-20): 183 | 184 | * `--errorto` option implemented. 185 | 186 | **1.0.1** (2015-08-18) to **1.0.9** (2015-08-19): 187 | 188 | * Documentation improved. 189 | 190 | ## Contributors 191 | 192 | * [Aurelien Lourot](https://ghuser.io/AurelienLourot) 193 | * [cassiodoroVicinetti](https://github.com/cassiodoroVicinetti) 194 | 195 | ## Similar projects 196 | 197 | The following projects provide similar functionalities: 198 | 199 | * [IFTTT](https://ifttt.com/), see [this post](http://www.warski.org/blog/2013/04/per-commit-e-mail-github-notifications/). 200 | * [Zapier](https://zapier.com/), however you have to create a "Zap" for each single project you want to watch. See [this thread](http://webapps.stackexchange.com/questions/62701/github-receive-updates-of-starred-projects-via-email-of-commits-on-master-branch). 201 | * [HubNotify](http://hubnotify.com/), however you will be notified only for new tags, not new commits. 202 | 203 | -------------------------------------------------------------------------------- /backlinks.txt: -------------------------------------------------------------------------------- 1 | https://stackoverflow.com/questions/8371173/how-to-get-notified-when-someone-pushes-into-a-github-branch/31373602#31373602 2 | http://www.systutorials.com/1473/setting-up-git-commit-email-notification/ 3 | http://webapps.stackexchange.com/questions/43787/watch-commits-made-to-followed-github-repositories-in-the-dashboard/80176#80176 4 | https://www.quora.com/Whats-a-good-way-of-generating-commit-emails-from-GitHub 5 | http://stackoverflow.com/questions/9845655/how-do-i-get-notifications-for-commits-to-the-framework/34529361#34529361 6 | http://webapps.stackexchange.com/questions/62701/github-receive-updates-of-starred-projects-via-email-of-commits-on-master-branch 7 | -------------------------------------------------------------------------------- /gicowa/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.2.5" 2 | -------------------------------------------------------------------------------- /gicowa/gicowa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import os 6 | import socket 7 | import sys 8 | import traceback 9 | 10 | import github 11 | 12 | from __init__ import __version__ 13 | import impl.encoding 14 | import impl.mail 15 | import impl.output 16 | import impl.persistence 17 | from impl.timestamp import Timestamp 18 | 19 | def _since_command(command_argname): 20 | """Decorator for Cli commands that require a 'since' argument. 21 | 22 | @param command_argname: Name of args's property containing args's command argument. 23 | """ 24 | def wrapper(func): 25 | def decorated(self, args): 26 | """ 27 | @param args: from argparse. 28 | """ 29 | now = Timestamp() 30 | command = args.command + " " + getattr(args, command_argname) 31 | if not args.sincelast: 32 | since = Timestamp(args) 33 | else: 34 | try: 35 | since = Timestamp(self._memory.timestamps[command]) 36 | except KeyError: # this command gets executed for the first time 37 | since = now 38 | self._output.echo(command + " since " + unicode(since)) 39 | 40 | result = func(self, args, since) 41 | 42 | # Remember this last execution: 43 | self._memory.timestamps[command] = now.data 44 | 45 | return result 46 | 47 | return decorated 48 | return wrapper 49 | 50 | class Cli: 51 | def __init__(self, argv, mail_sender, output): 52 | """Main class. 53 | @param mail_sender: Instance of impl.mail.MailSender. 54 | @param output: Instance of impl.output.Output. 55 | """ 56 | self.errorto = None 57 | self.__argv = argv 58 | self.__github = None 59 | self.__mail_sender = mail_sender 60 | self._output = output 61 | self._memory = impl.persistence.Memory() 62 | 63 | def run(self): 64 | parser = argparse.ArgumentParser(description="watch GitHub commits easily") 65 | 66 | parser.add_argument("--version", action="version", 67 | version="%(prog)s version " + __version__) 68 | 69 | parser.add_argument("--no-color", action="store_true", help="disable color in output") 70 | 71 | credentials_option = "--credentials" 72 | parser.add_argument(credentials_option, 73 | help="your GitHub login and password (e.g. 'AurelienLourot:password')") 74 | 75 | parser.add_argument("--mailto", 76 | help="e-mail address to which the output should be sent in any case " 77 | + "(e.g. 'aurelien.lourot@gmail.com')") 78 | parser.add_argument("--mailfrom", 79 | help="e-mail server and credentials from which the output should be " 80 | + "sent (e.g. 'smtp.googlemail.com:465:aurelien.lourot@gmail.com:password')") 81 | parser.add_argument("--errorto", 82 | help="e-mail address to which the output should be sent in case of an " 83 | + "error (e.g. 'aurelien.lourot@gmail.com')") 84 | 85 | parser.add_argument(self._persist_option, action="store_true", 86 | help="gicowa will keep track of the last commands run in %s" % 87 | (self._memory.filename)) 88 | 89 | subparsers = parser.add_subparsers(help="available commands") 90 | 91 | descr = "list repos watched by a user" 92 | parser_watchlist = subparsers.add_parser("watchlist", description=descr, help=descr) 93 | parser_watchlist.set_defaults(command="watchlist", impl=self.__watchlist) 94 | self._add_argument_watcher_name(parser_watchlist) 95 | 96 | descr = "list last commits on a repo" 97 | parser_lastrepocommits = subparsers.add_parser("lastrepocommits", description=descr, 98 | help=descr) 99 | parser_lastrepocommits.set_defaults(command="lastrepocommits", impl=self.__lastrepocommits) 100 | parser_lastrepocommits.add_argument("repo", 101 | help="repository's full name (e.g. 'AurelienLourot/github-commit-watcher')") 102 | self._add_arguments_since_committer_timestamp(parser_lastrepocommits) 103 | 104 | descr = "list last commits watched by a user" 105 | parser_lastwatchedcommits = subparsers.add_parser("lastwatchedcommits", description=descr, 106 | help=descr) 107 | parser_lastwatchedcommits.set_defaults(command="lastwatchedcommits", 108 | impl=self.__lastwatchedcommits) 109 | self._add_argument_watcher_name(parser_lastwatchedcommits) 110 | self._add_arguments_since_committer_timestamp(parser_lastwatchedcommits) 111 | 112 | args = parser.parse_args(self.__argv) 113 | 114 | if args.mailfrom is not None: 115 | mailfrom = args.mailfrom.split(":", 3) 116 | try: 117 | self.__mail_sender.server = mailfrom[0] 118 | self.__mail_sender.port = mailfrom[1] 119 | self.__mail_sender.sender = mailfrom[2] 120 | self.__mail_sender.password = mailfrom[3] 121 | except IndexError as e: 122 | e.args += ("Bad mailfrom syntax.",) 123 | raise 124 | if args.mailto is not None: 125 | self.__mail_sender.dest.add(args.mailto) 126 | self.errorto = args.errorto 127 | 128 | self._output.colored = not args.no_color 129 | 130 | if args.credentials is not None: 131 | credentials = args.credentials.split(":", 1) 132 | try: 133 | self.__github = github.Github(credentials[0], credentials[1]) 134 | except IndexError as e: 135 | e.args += ("Bad credentials' syntax.",) 136 | raise 137 | else: 138 | self.__github = github.Github() 139 | 140 | try: 141 | args.impl(args) 142 | except github.GithubException as e: 143 | if e.status == 401 and args.credentials is not None: 144 | e.args += ("Bad credentials?",) 145 | if e.status == 403 and args.credentials is None: 146 | e.args += ("API rate limit exceeded? Use the %s option." % (credentials_option),) 147 | raise 148 | except socket.gaierror as e: 149 | e.args += ("No internet connection?",) 150 | raise 151 | 152 | if len(self.__mail_sender.dest): 153 | email_sent = _send_output_by_mail_if_necessary(self.__mail_sender, args.command + ".", 154 | self._output) 155 | if not email_sent: 156 | self._output.echo("No e-mail sent.") 157 | 158 | if args.persist: 159 | self._memory.save() 160 | 161 | @staticmethod 162 | def _add_argument_watcher_name(parser): 163 | """Adds an argument corresponding to a watcher's user name to an argparse parser. 164 | """ 165 | parser.add_argument("username", help="watcher's name (e.g. 'AurelienLourot')") 166 | 167 | @classmethod 168 | def _add_arguments_since_committer_timestamp(cls, parser): 169 | """Adds arguments corresponding to a "since" committer timestamp to an argparse parser. 170 | """ 171 | subparsers = parser.add_subparsers(help="oldest committer UTC timestamp to consider") 172 | 173 | descr = "explicit" 174 | parser_since = subparsers.add_parser("since", description=descr, help=descr) 175 | since = parser_since.add_argument_group(" ".join(zip(*Timestamp.fields)[0]), 176 | "(e.g. '2015 07 05 09 12 00')") 177 | parser_since.set_defaults(sincelast=False) 178 | for field in Timestamp.fields: 179 | since.add_argument(field[0], help=field[1]) 180 | 181 | descr = "use last time run with %s" % (cls._persist_option) 182 | parser_sincelast = subparsers.add_parser("sincelast", description=descr, help=descr) 183 | parser_sincelast.set_defaults(sincelast=True) 184 | 185 | def __watchlist(self, args): 186 | """Implements 'watchlist' command. 187 | Prints all watched repos of 'args.username'. 188 | 189 | @param args: from argparse. 190 | """ 191 | command = args.command + " " + args.username 192 | self._output.echo(command) 193 | 194 | for repo in self.__get_watchlist(args.username): 195 | self._output.echo(self._output.red(repo)) 196 | 197 | @_since_command("repo") 198 | def __lastrepocommits(self, args, since): 199 | """Implements 'lastrepocommits' command. 200 | Prints all commits on 'args.repo' with committer timestamp bigger than 201 | 'args.YYYY,MM,DD,hh,mm,ss'. 202 | 203 | @param args: from argparse. 204 | @param since: from decoration. 205 | """ 206 | pushed = self.__has_been_pushed(args.repo, since.to_datetime()) 207 | if pushed is not None: 208 | self._output.echo(pushed) 209 | for commit in self.__get_last_commits(args.repo, since.to_datetime()): 210 | self._output.echo(commit) 211 | 212 | @_since_command("username") 213 | def __lastwatchedcommits(self, args, since): 214 | """Implements 'lastwatchedcommits' command. 215 | Prints all commits on repos watched by 'args.username' with committer timestamp bigger than 216 | 'args.YYYY,MM,DD,hh,mm,ss'. 217 | 218 | @param args: from argparse. 219 | @param since: from decoration. 220 | """ 221 | for repo in self.__get_watchlist(args.username): 222 | pushed = self.__has_been_pushed(repo, since.to_datetime()) 223 | if pushed is not None: 224 | self._output.echo("%s - %s" % (self._output.red(repo), pushed)) 225 | for commit in self.__get_last_commits(repo, since.to_datetime()): 226 | self._output.echo("%s - %s" % (self._output.red(repo), commit)) 227 | 228 | def __get_watchlist(self, username): 229 | """Returns list of all watched repos of 'username'. 230 | """ 231 | try: 232 | user = self.__github.get_user(username) 233 | except github.GithubException as e: 234 | if e.status == 404: 235 | e.args += ("%s user doesn't exist?" % (username),) 236 | raise 237 | 238 | return [repo.full_name for repo in user.get_subscriptions()] 239 | 240 | def __get_last_commits(self, repo_full_name, since): 241 | """Returns list of all commits on 'repo_full_name' with committer timestamp bigger than 242 | 'since'. 243 | """ 244 | repo = self.__get_repo(repo_full_name) 245 | result = [] 246 | for i in repo.get_commits(since=since): 247 | commit = repo.get_git_commit(i.sha) 248 | result.append("Committed on %s - %s - %s" 249 | % (self._output.green(commit.committer.date), 250 | self._output.blue(commit.committer.name), commit.message)) 251 | return result 252 | 253 | def __has_been_pushed(self, repo_full_name, since): 254 | """Returns string describing last push timestamp of 'repo_full_name''s last commit if after 255 | 'since'. Returns None otherwise. 256 | """ 257 | repo = self.__get_repo(repo_full_name) 258 | if repo.pushed_at >= since: 259 | return "Last commit pushed on " + self._output.green(repo.pushed_at) 260 | 261 | def __get_repo(self, full_name): 262 | """Returns github repository. Raises if couldn't be found. 263 | """ 264 | try: 265 | return self.__github.get_repo(full_name) 266 | except github.GithubException as e: 267 | if e.status == 404: 268 | e.args += ("%s repo doesn't exist?" % (full_name),) 269 | raise 270 | 271 | _persist_option = "--persist" 272 | 273 | def _send_output_by_mail_if_necessary(mail_sender, email_subject, output): 274 | """Returns True if an e-mail was sent. 275 | @param mail_sender: Instance of impl.mail.MailSender. 276 | @param output: Dependency. Inject an instance of impl.output.Output. 277 | """ 278 | if output.echoed.count("\n") <= 1: 279 | return False 280 | email_content = output.echoed + "\nSent from %s.\n" % (os.uname()[1]) 281 | mail_sender.send_email(email_subject, email_content) 282 | output.echo("Sent by e-mail to %s" % ", ".join(mail_sender.dest)) 283 | return True 284 | 285 | def _print(text): 286 | """coding/decoding-friendly version of print(). 287 | See http://nedbatchelder.com/text/unipain/unipain.html 288 | @param text: Must be either a unicode or a utf-8 str. 289 | """ 290 | unicode_text = text 291 | if isinstance(unicode_text, str): 292 | unicode_text = text.decode(impl.encoding.preferred, "replace") 293 | 294 | try: 295 | print(unicode_text) 296 | return 297 | except UnicodeDecodeError: 298 | pass 299 | except UnicodeEncodeError: 300 | pass 301 | 302 | # Well, let's make it ascii then: 303 | ascii_text = unicode_text.encode("ascii", "replace") 304 | print(ascii_text) 305 | 306 | def main(): 307 | mail_sender = impl.mail.MailSender() 308 | output = impl.output.Output(_print) 309 | cli = Cli(sys.argv[1:], mail_sender, output) 310 | try: 311 | cli.run() 312 | except Exception as e: 313 | error_msg = "Oops, an error occured.\n" + "\n".join(unicode(i) for i in e.args) + "\n\n" 314 | error_msg += traceback.format_exc() 315 | try: 316 | output.echo(error_msg) 317 | if cli.errorto is not None: 318 | mail_sender.dest.add(cli.errorto) 319 | if len(mail_sender.dest): 320 | _send_output_by_mail_if_necessary(mail_sender, "error.", output) 321 | except: 322 | _print(error_msg) 323 | raise 324 | -------------------------------------------------------------------------------- /gicowa/impl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lourot/github-commit-watcher/ca4ea1ee8ebaefdf270e4d16735d563f23cf4833/gicowa/impl/__init__.py -------------------------------------------------------------------------------- /gicowa/impl/encoding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | preferred = "utf-8" 5 | -------------------------------------------------------------------------------- /gicowa/impl/mail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from email.mime.text import MIMEText 5 | import smtplib 6 | 7 | import encoding 8 | 9 | class MailSender: 10 | def __init__(self): 11 | self.server = "localhost" 12 | self.port = None 13 | self.sender = "gicowa@ghuser.io" 14 | self.dest = set() 15 | self.password = None 16 | 17 | def send_email(self, subject, content): 18 | email = MIMEText(content, "plain", encoding.preferred) 19 | email["Subject"] = "[gicowa] %s" % (subject) 20 | email["From"] = self.sender 21 | email["To"] = ", ".join(self.dest) 22 | if self.port is None or self.password is None: 23 | smtp = smtplib.SMTP(self.server) 24 | else: 25 | smtp = smtplib.SMTP_SSL(self.server, self.port) 26 | smtp.login(self.sender, self.password) 27 | try: 28 | smtp.sendmail(self.sender, self.dest, email.as_string()) 29 | except smtplib.SMTPRecipientsRefused as e: 30 | e.args += ("%s addresses malformed?" % ", ".join(self.dest),) 31 | raise 32 | smtp.quit() 33 | -------------------------------------------------------------------------------- /gicowa/impl/output.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | class Output: 5 | def __init__(self, print_function): 6 | """ 7 | @param print_function: Dependency. Inject a function implementing the same interface as 8 | print(). 9 | """ 10 | self.__print_function = print_function 11 | self.colored = True 12 | 13 | # Contains at any time the whole text that has been echoed by this instance: 14 | self.echoed = "" 15 | 16 | def echo(self, text): 17 | self.__print_function(text) 18 | self.echoed += text + "\n" 19 | 20 | def red(self, text): 21 | return self.__colored(text, 31) 22 | 23 | def green(self, text): 24 | return self.__colored(text, 32) 25 | 26 | def blue(self, text): 27 | return self.__colored(text, 34) 28 | 29 | def __colored(self, text, color): 30 | """Returns 'text' with a color, i.e. bash and zsh would print the returned string in the 31 | given color. 32 | Returns 'text' with no color if not self.colored. 33 | """ 34 | text = unicode(text) 35 | return text if not self.colored else "\033[" + unicode(color) + "m" + text + "\033[0m" 36 | -------------------------------------------------------------------------------- /gicowa/impl/persistence.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | import os 6 | 7 | class Memory: 8 | filename = "~/.gicowa" 9 | 10 | def __init__(self): 11 | # e.g. {"lastwatchedcommits AurelienLourot": {"YYYY": "2015", 12 | # "MM" : "07", 13 | # "DD" : "04", 14 | # "hh" : "00", 15 | # "mm" : "00", 16 | # "ss" : "00"}} 17 | self.timestamps = {} 18 | 19 | try: 20 | with open(os.path.expanduser(self.filename), "rb") as f: 21 | try: 22 | self.timestamps = json.loads(f.read()) 23 | except ValueError as e: 24 | e.args += ("%s file damaged?" % (self.filename),) 25 | raise 26 | except IOError: 27 | # Ignores when file doesn't exist yet 28 | pass 29 | 30 | def save(self): 31 | with open(os.path.expanduser(self.filename), "wb") as f: 32 | f.write(json.dumps(self.timestamps, indent=2)) 33 | -------------------------------------------------------------------------------- /gicowa/impl/timestamp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import datetime 5 | 6 | import encoding 7 | 8 | class Timestamp: 9 | fields = (("YYYY", "year" ), 10 | ("MM" , "month" ), 11 | ("DD" , "day" ), 12 | ("hh" , "hour" ), 13 | ("mm" , "minute"), 14 | ("ss" , "second")) 15 | 16 | def __init__(self, obj=None): 17 | """Builds from 'obj' having the members 'fields' or being a dictionary with these fields. 18 | Builds from current UTC time if no 'obj' provided. 19 | """ 20 | now = datetime.datetime.utcnow() 21 | self.data = {} 22 | for field in self.fields: 23 | if obj is not None: 24 | if isinstance(obj, dict): 25 | self.data[field[0]] = obj[field[0]] 26 | else: 27 | self.data[field[0]] = getattr(obj, field[0]) 28 | else: 29 | self.data[field[0]] = getattr(now, field[1]) 30 | 31 | def __unicode__(self): 32 | return unicode(self.to_datetime()) 33 | 34 | def __str__(self): 35 | return unicode(self).encode(encoding.preferred) 36 | 37 | def __eq__(self, other): 38 | for field in self.fields: 39 | if self.data[field[0]] != other.data[field[0]]: 40 | return False 41 | return True 42 | 43 | def to_datetime(self): 44 | try: 45 | args = [] 46 | for field in self.fields: 47 | args.append(int(self.data[field[0]])) 48 | return datetime.datetime(*args) 49 | except ValueError as e: 50 | e.args += ("Timestamp malformed?",) 51 | raise 52 | -------------------------------------------------------------------------------- /github_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lourot/github-commit-watcher/ca4ea1ee8ebaefdf270e4d16735d563f23cf4833/github_icon.png -------------------------------------------------------------------------------- /maintain.md: -------------------------------------------------------------------------------- 1 | # Release a new version 2 | 3 | ## Run the unit tests 4 | 5 | See [test/README.md](test/README.md) 6 | 7 | ## Increase the version number 8 | 9 | In [gicowa/\__init\__.py](gicowa/__init__.py) to `7.8.9` in this example. 10 | 11 | ## Extend the changelog 12 | 13 | In [README.html](README.html) 14 | 15 | ## Generate the documentation in other formats 16 | 17 | ``` 18 | $ sudo pip install -r requirements.txt 19 | $ sudo apt-get install pandoc 20 | $ ./scripts/gen_doc.sh 21 | ``` 22 | 23 | ## Generate the package to be published 24 | 25 | ``` 26 | $ python setup.py sdist 27 | ``` 28 | 29 | And check that the resulting `dist/gicowa-7.8.9.tar.gz` looks well-formed. 30 | 31 | ## Install the package locally 32 | 33 | ``` 34 | $ sudo apt-get install sendmail 35 | $ sudo pip install dist/gicowa-7.8.9.tar.gz 36 | ``` 37 | 38 | and test it briefly, e.g. 39 | 40 | ``` 41 | $ gicowa --version 42 | $ gicowa watchlist AurelienLourot 43 | ``` 44 | 45 | ## Commit your changes, create a git tag and push 46 | 47 | ``` 48 | $ git add -u 49 | $ git commit -m "Version 7.8.9" 50 | $ git push 51 | $ git tag "7.8.9" 52 | $ git push --tags 53 | ``` 54 | 55 | And check that the resulting [official documentation](http://a.ghuser.io/gicowa) looks well-formed. 56 | 57 | ## Push the package to PyPI Test 58 | 59 | Create `~/.pypirc` as follows: 60 | 61 | ``` 62 | [distutils] 63 | index-servers = 64 | pypi 65 | pypitest 66 | 67 | [pypi] 68 | repository: https://pypi.python.org/pypi 69 | username:lourot 70 | password:mypassword 71 | 72 | [pypitest] 73 | repository: https://testpypi.python.org/pypi 74 | username:lourot 75 | password:mypassword 76 | ``` 77 | 78 | and push: 79 | 80 | ``` 81 | $ python setup.py sdist upload -r pypitest 82 | ``` 83 | 84 | > **NOTE:** if this fails because the project doesn't exist in PyPI Test anymore, register it again: 85 | > 86 | > ``` 87 | > $ python setup.py register -r pypitest 88 | > ``` 89 | 90 | Finally check that the package looks well-formed at `https://testpypi.python.org/pypi/gicowa/7.8.9` 91 | 92 | ## Push the package to PyPI 93 | 94 | ``` 95 | $ python setup.py sdist upload -r pypi 96 | ``` 97 | 98 | and check that the package looks well-formed at `https://pypi.python.org/pypi/gicowa/7.8.9` 99 | 100 | Finally check that the package can be installed from PyPI: 101 | 102 | ``` 103 | $ sudo pip uninstall gicowa 104 | $ sudo pip install gicowa 105 | ``` 106 | 107 | ## Add release notes 108 | 109 | to [https://github.com/AurelienLourot/github-commit-watcher/tags](https://github.com/AurelienLourot/github-commit-watcher/tags) 110 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyGithub==1.26.0 2 | mock==1.0.1 3 | html2text==2015.6.21 4 | -------------------------------------------------------------------------------- /scripts/gen_doc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "" > README.md 4 | echo "" >> README.md 8 | echo "" >> README.md 9 | echo "[![Build Status](https://travis-ci.org/AurelienLourot/github-commit-watcher.svg?branch=master)](https://travis-ci.org/AurelienLourot/github-commit-watcher)" >> README.md 10 | echo "" >> README.md 11 | echo "Official documentation [here](http://a.ghuser.io/gicowa)." >> README.md 12 | echo "" >> README.md 13 | html2text README.html >> README.md 14 | 15 | echo "" > README 16 | echo ".. Generated by:" >> README 17 | echo " $ ./scripts/gen_doc.sh" >> README 18 | echo "" >> README 19 | echo "" >> README 20 | echo "Official documentation \`here \`_." >> README 21 | echo "" >> README 22 | # apt-get install pandoc # 1.12.2.1 23 | pandoc --from=html --to=rst < README.html >> README 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import gicowa 3 | 4 | setup(name="gicowa", 5 | version=gicowa.__version__, 6 | install_requires=["PyGithub"], 7 | description="GitHub Commit Watcher", 8 | long_description=open("README").read(), 9 | keywords=["gicowa", 10 | "github", 11 | "commit", 12 | "commits", 13 | "watcher", 14 | "watch", 15 | "push", 16 | "pushed", 17 | "notification", 18 | "notifications"], 19 | author="Aurelien Lourot", 20 | author_email="aurelien.lourot@gmail.com", 21 | url="https://github.com/AurelienLourot/github-commit-watcher", 22 | download_url="https://github.com/AurelienLourot/github-commit-watcher/tarball/" 23 | + gicowa.__version__, 24 | license="public domain", 25 | classifiers=["Development Status :: 4 - Beta", 26 | "Environment :: Console", 27 | "Intended Audience :: Developers", 28 | "License :: Public Domain", 29 | "Natural Language :: English", 30 | "Operating System :: POSIX :: Linux", 31 | "Programming Language :: Python :: 2.7", 32 | "Topic :: Software Development :: Version Control", 33 | "Topic :: Utilities"], 34 | packages=["gicowa", 35 | "gicowa/impl"], 36 | entry_points=""" 37 | [console_scripts] 38 | gicowa = gicowa.gicowa:main 39 | """) 40 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Running all unit tests 2 | 3 | ``` 4 | # From this directory: 5 | $ cd ../ 6 | $ sudo pip install -r requirements.txt 7 | $ python -m unittest discover -v 8 | ``` 9 | 10 | This will discover all tests inside the test directory and run all test cases. 11 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lourot/github-commit-watcher/ca4ea1ee8ebaefdf270e4d16735d563f23cf4833/test/__init__.py -------------------------------------------------------------------------------- /test/test_gicowa.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import codecs 4 | import mock 5 | import os 6 | import sys 7 | import unittest 8 | 9 | import github 10 | 11 | import gicowa.gicowa as gcw 12 | import gicowa.impl.mail as mail 13 | import gicowa.impl.output as output 14 | import gicowa.impl.timestamp as timestamp 15 | 16 | class MockPrint: 17 | def __init__(self): 18 | self.printed = "" 19 | 20 | def do_print(self, text): 21 | self.printed += text + "\n" 22 | 23 | class GicowaTests(unittest.TestCase): 24 | def __init__(self, *args, **kwargs): 25 | super(GicowaTests, self).__init__(*args, **kwargs) 26 | self.__mock_github = mock.Mock() 27 | self.__mock_github_user = mock.Mock() 28 | 29 | def setUp(self): 30 | mock_committer = lambda: None # ~ object with no properties (yet) 31 | mock_committer.name = "myCommitter" 32 | mock_committer.date = "myDate" 33 | mock_commit = lambda: None # ~ object with no properties (yet) 34 | mock_commit.committer = mock_committer 35 | mock_commit.message = "myMessage" 36 | mock_commit.sha = "mySha" 37 | 38 | def get_commits(since): 39 | return (mock_commit,) 40 | 41 | def get_git_commit(sha): 42 | return mock_commit 43 | 44 | repos = [] 45 | for i in xrange(1, 3+1): 46 | repo = mock.Mock() 47 | repo.full_name = "mySubscription" + str(i) 48 | repo.pushed_at = timestamp.Timestamp({"YYYY": 2015, 49 | "MM": 10, 50 | "DD": 11, 51 | "hh": 20, 52 | "mm": 22, 53 | "ss": 24}).to_datetime() 54 | repo.get_commits = get_commits 55 | repo.get_git_commit = get_git_commit 56 | repos.append(repo) 57 | 58 | def get_repo(full_name): 59 | for repo in repos: 60 | if repo.full_name == full_name: 61 | return repo 62 | 63 | self.__mock_github_user.get_subscriptions.return_value = repos 64 | self.__mock_github.get_user.return_value = self.__mock_github_user 65 | self.__mock_github.get_repo = get_repo 66 | 67 | self.__mock_github.get_user.side_effect = None 68 | 69 | def test_output(self): 70 | mock_stdout = MockPrint() 71 | out = output.Output(mock_stdout.do_print) 72 | out.print_function = mock_stdout.do_print 73 | out.echoed = "" 74 | out.echo("hello") 75 | out.echo("hi") 76 | self.assertEqual(out.echoed, mock_stdout.printed) 77 | 78 | def test_timestamp(self): 79 | stamp = timestamp.Timestamp({"YYYY": 2015, 80 | "MM": 10, 81 | "DD": 11, 82 | "hh": 20, 83 | "mm": 22, 84 | "ss": 24}) 85 | expected = "2015-10-11 20:22:24" 86 | actual = unicode(stamp) 87 | self.assertEqual(actual, expected) 88 | 89 | @mock.patch("github.Github") 90 | def test_watchlist(self, mock_github_constructor): 91 | mock_github_constructor.return_value = self.__mock_github 92 | mock_stdout = MockPrint() 93 | cli = gcw.Cli(("watchlist", "myUsername"), mail.MailSender(), 94 | output.Output(mock_stdout.do_print)) 95 | cli.run() 96 | expected = "watchlist myUsername\n" \ 97 | + "\033[31mmySubscription1\033[0m\n" \ 98 | + "\033[31mmySubscription2\033[0m\n" \ 99 | + "\033[31mmySubscription3\033[0m\n" 100 | actual = mock_stdout.printed 101 | self.assertEqual(actual, expected) 102 | 103 | @mock.patch("github.Github") 104 | def test_nocolor(self, mock_github_constructor): 105 | mock_github_constructor.return_value = self.__mock_github 106 | mock_stdout = MockPrint() 107 | cli = gcw.Cli(("--no-color", "watchlist", "myUsername"), mail.MailSender(), 108 | output.Output(mock_stdout.do_print)) 109 | cli.run() 110 | expected = "watchlist myUsername\n" \ 111 | + "mySubscription1\n" \ 112 | + "mySubscription2\n" \ 113 | + "mySubscription3\n" 114 | actual = mock_stdout.printed 115 | self.assertEqual(actual, expected) 116 | 117 | @mock.patch("github.Github") 118 | def test_lastrepocommits(self, mock_github_constructor): 119 | mock_github_constructor.return_value = self.__mock_github 120 | mock_stdout = MockPrint() 121 | cli = gcw.Cli( 122 | ("--no-color", "lastrepocommits", "mySubscription1", "since", "2015", "10", "11", "20", 123 | "08", "00"), mail.MailSender(), output.Output(mock_stdout.do_print)) 124 | cli.run() 125 | expected = "lastrepocommits mySubscription1 since 2015-10-11 20:08:00\n" \ 126 | + "Last commit pushed on 2015-10-11 20:22:24\n" \ 127 | + "Committed on myDate - myCommitter - myMessage\n" 128 | actual = mock_stdout.printed 129 | self.assertEqual(actual, expected) 130 | 131 | @mock.patch("github.Github") 132 | def test_lastwatchedcommits(self, mock_github_constructor): 133 | mock_github_constructor.return_value = self.__mock_github 134 | mock_stdout = MockPrint() 135 | cli = gcw.Cli( 136 | ("--no-color", "lastwatchedcommits", "myUsername", "since", "2015", "10", "11", "20", 137 | "08", "00"), mail.MailSender(), output.Output(mock_stdout.do_print)) 138 | cli.run() 139 | expected = "lastwatchedcommits myUsername since 2015-10-11 20:08:00\n" \ 140 | + "mySubscription1 - Last commit pushed on 2015-10-11 20:22:24\n" \ 141 | + "mySubscription1 - Committed on myDate - myCommitter - myMessage\n" \ 142 | + "mySubscription2 - Last commit pushed on 2015-10-11 20:22:24\n" \ 143 | + "mySubscription2 - Committed on myDate - myCommitter - myMessage\n" \ 144 | + "mySubscription3 - Last commit pushed on 2015-10-11 20:22:24\n" \ 145 | + "mySubscription3 - Committed on myDate - myCommitter - myMessage\n" 146 | actual = mock_stdout.printed 147 | self.assertEqual(actual, expected) 148 | 149 | @mock.patch("gicowa.impl.mail.MailSender.send_email") 150 | @mock.patch("github.Github") 151 | def test_mailto(self, mock_github_constructor, mock_send_email): 152 | mock_github_constructor.return_value = self.__mock_github 153 | mock_stdout = MockPrint() 154 | cli = gcw.Cli(("--no-color", "--mailto", "myMail@myDomain.com", "watchlist", 155 | "myUsername"), mail.MailSender(), output.Output(mock_stdout.do_print)) 156 | cli.run() 157 | mock_send_email.assert_called_once_with("watchlist.", """\ 158 | watchlist myUsername 159 | mySubscription1 160 | mySubscription2 161 | mySubscription3 162 | 163 | Sent from %s. 164 | """ % (os.uname()[1])) 165 | 166 | @mock.patch("github.Github") 167 | def test_no_email_sent(self, mock_github_constructor): 168 | mock_github_constructor.return_value = self.__mock_github 169 | self.__mock_github_user.get_subscriptions.return_value = () 170 | mock_stdout = MockPrint() 171 | cli = gcw.Cli(("--no-color", "--mailto", "myMail@myDomain.com", "watchlist", 172 | "myUsername"), mail.MailSender(), output.Output(mock_stdout.do_print)) 173 | cli.run() 174 | expected = "watchlist myUsername\n" \ 175 | + "No e-mail sent.\n" 176 | actual = mock_stdout.printed 177 | self.assertEqual(actual, expected) 178 | 179 | @mock.patch("github.Github") 180 | def test_credentials(self, mock_github_constructor): 181 | mock_github_constructor.return_value = self.__mock_github 182 | mock_stdout = MockPrint() 183 | cli = gcw.Cli(("--credentials", "myUsername1:myPassword", "watchlist", "myUsername2"), 184 | mail.MailSender(), output.Output(mock_stdout.do_print)) 185 | cli.run() 186 | mock_github_constructor.assert_called_once_with("myUsername1", "myPassword") 187 | 188 | @mock.patch("github.Github") 189 | def test_bad_credentials(self, mock_github_constructor): 190 | mock_github_constructor.return_value = self.__mock_github 191 | self.__mock_github.get_user.side_effect = github.GithubException(401, "data") 192 | mock_stdout = MockPrint() 193 | cli = gcw.Cli(("--credentials", "myUsername1:myPassword", "watchlist", "myUsername2"), 194 | mail.MailSender(), output.Output(mock_stdout.do_print)) 195 | with self.assertRaises(github.GithubException): 196 | cli.run() 197 | 198 | @mock.patch("github.Github") 199 | def test_user_doesnt_exist(self, mock_github_constructor): 200 | mock_github_constructor.return_value = self.__mock_github 201 | self.__mock_github.get_user.side_effect = github.GithubException(404, "data") 202 | mock_stdout = MockPrint() 203 | cli = gcw.Cli(("--credentials", "myUsername1:myPassword", "watchlist", "myUsername2"), 204 | mail.MailSender(), output.Output(mock_stdout.do_print)) 205 | with self.assertRaises(github.GithubException): 206 | cli.run() 207 | 208 | @mock.patch("github.Github") 209 | def test_repo_doesnt_exist(self, mock_github_constructor): 210 | mock_github_constructor.return_value = self.__mock_github 211 | self.__mock_github.get_repo = mock.Mock(side_effect=github.GithubException(404, "data")) 212 | mock_stdout = MockPrint() 213 | cli = gcw.Cli(("lastrepocommits", "mySubscription1", "since", "2015", "10", "11", "20", 214 | "08", "00"), mail.MailSender(), output.Output(mock_stdout.do_print)) 215 | with self.assertRaises(github.GithubException): 216 | cli.run() 217 | 218 | @mock.patch("gicowa.impl.persistence.Memory.save") 219 | @mock.patch("github.Github") 220 | def test_persist(self, mock_github_constructor, mock_save): 221 | mock_github_constructor.return_value = self.__mock_github 222 | mock_stdout = MockPrint() 223 | cli = gcw.Cli( 224 | ("--persist", "lastrepocommits", "mySubscription1", "since", "2015", "10", "11", "20", 225 | "08", "00"), mail.MailSender(), output.Output(mock_stdout.do_print)) 226 | cli.run() 227 | mock_save.assert_called_once_with() 228 | 229 | def test_sincelast(self): 230 | """Tests the sincelast functionality in _since_command decorator. 231 | """ 232 | now = lambda: None # ~ object with no properties (yet) 233 | now.year = 2015 234 | now.month = 12 235 | now.day = 20 236 | now.hour = 19 237 | now.minute = 46 238 | now.second = 24 239 | 240 | # See http://www.voidspace.org.uk/python/mock/examples.html#partial-mocking : 241 | with mock.patch("gicowa.impl.timestamp.datetime.datetime") as mock_datetime: 242 | mock_datetime.utcnow.return_value = now 243 | 244 | class MyCli: 245 | def __init__(self): 246 | self._output = output.Output(MockPrint().do_print) 247 | self._memory = lambda: None # ~ object with no properties (yet) 248 | self._memory.timestamps = {"my_command my_argument1": {"YYYY": 2015, 249 | "MM": 10, 250 | "DD": 11, 251 | "hh": 20, 252 | "mm": 22, 253 | "ss": 24}} 254 | 255 | self.last_result = None 256 | 257 | @gcw._since_command("my_argument") 258 | def my_command(self, args, since): 259 | self.last_result = since 260 | 261 | args = lambda: None # ~ object with no properties (yet) 262 | args.command = "my_command" 263 | args.my_argument = "my_argument1" 264 | args.sincelast = True 265 | 266 | # Last command in memory: 267 | my_cli = MyCli() 268 | my_cli.my_command(args) 269 | self.assertEqual(my_cli.last_result, timestamp.Timestamp({"YYYY": 2015, 270 | "MM": 10, 271 | "DD": 11, 272 | "hh": 20, 273 | "mm": 22, 274 | "ss": 24})) 275 | 276 | # Last command not in memory: 277 | args.my_argument = "my_argument2" 278 | my_cli.my_command(args) 279 | self.assertEqual(my_cli.last_result, timestamp.Timestamp({"YYYY": 2015, 280 | "MM": 12, 281 | "DD": 20, 282 | "hh": 19, 283 | "mm": 46, 284 | "ss": 24})) 285 | 286 | def test_help(self): 287 | mock_stdout = MockPrint() 288 | cli = gcw.Cli(("--help",), mail.MailSender(), output.Output(mock_stdout.do_print)) 289 | with self.assertRaises(SystemExit): 290 | cli.run() 291 | 292 | def test_print_utf8_string(self): 293 | utf8_string = "Tschüß!" 294 | gcw._print(utf8_string) # shouldn't raise 295 | 296 | def test_print_utf8_string_to_ascii_stdout(self): 297 | # Change stdout's encoding to ascii: 298 | default_stdout = sys.stdout 299 | sys.stdout = codecs.getwriter("ascii")(default_stdout) 300 | 301 | # Show that the default print fails to print a utf-8 string to an ascii stdout. 302 | # See http://nedbatchelder.com/text/unipain/unipain.html 303 | utf8_string = "Tschüß!" 304 | with self.assertRaises(UnicodeDecodeError): 305 | print(utf8_string) 306 | 307 | # Show that our _print is able to do it: 308 | gcw._print(utf8_string) 309 | 310 | # Restore stdout's encoding (to utf-8): 311 | sys.stdout = default_stdout 312 | -------------------------------------------------------------------------------- /test/test_mail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from email.mime.text import MIMEText 4 | import mock 5 | import unittest 6 | 7 | import gicowa.impl.mail as mail 8 | 9 | class MailTests(unittest.TestCase): 10 | @mock.patch("smtplib.SMTP") 11 | def test_send_email(self, mock_smtp_constructor): 12 | mock_smtp = mock.Mock() 13 | mock_smtp_constructor.return_value = mock_smtp 14 | 15 | subject = "subject" 16 | content = "content" 17 | dest = set(("dest1@domain.com", "dest2@domain.com")) 18 | 19 | mail_sender = mail.MailSender() 20 | mail_sender.dest = dest 21 | mail_sender.send_email(subject, content) 22 | 23 | mock_smtp_constructor.assert_called_once_with("localhost") 24 | 25 | expected_email = MIMEText(content, "plain", "utf-8") 26 | expected_email["Subject"] = "[gicowa] %s" % (subject) 27 | expected_email["From"] = "gicowa@ghuser.io" 28 | expected_email["To"] = ", ".join(dest) 29 | mock_smtp.sendmail.assert_called_once_with("gicowa@ghuser.io", dest, 30 | expected_email.as_string()) 31 | --------------------------------------------------------------------------------