git-maintain 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +194 -0
- data/bin/git-maintain +55 -0
- data/git-maintain-completion.sh +99 -0
- data/lib/addons/RDMACore.rb +61 -0
- data/lib/branch.rb +460 -0
- data/lib/common.rb +123 -0
- data/lib/repo.rb +182 -0
- data/lib/travis.rb +105 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0c79a790fc66646271a7722a583509c1ce9d5c69dd1e69d0762b09fb5a5c19ab
|
4
|
+
data.tar.gz: 2d3a81431bbd6d4394f3eaa17c524b8b3ff1b7f752da10f165d5fadaa41b1c4f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ab558a35d36b5c767590b1dabe14bcedff7a51e7d74deb83ba142a543332450e14aa357099d9444dd83fd54c8d793229de39f4f260dcc3b0d2490abc9413bfab
|
7
|
+
data.tar.gz: 3b1d4a0166512d02d48673b676fbe924a364d90b01c5e343a8fa65ae412db910b9634fd0b947367e4bfe97cf3d5f28ea7ddce3863e79b5e2b7a7530a0ec345e2
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2018 SUSE
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
git-maintain is a simple ruby script to deal with all the hassle of maintaining stable branches in a project.
|
2
|
+
|
3
|
+
The idea is to script most of the maintenance tasks so the maintainer can focus on just reviewing and not on writing up release notes, looking for commits and such.
|
4
|
+
|
5
|
+
- Note: the workflow is highly inspired to the git-topic-branches (https://github.com/nmorey/git-topic-branches)
|
6
|
+
and both can be used at the same time
|
7
|
+
- Note2: Releasing on github is done through the git-release (https://github.com/mpalmer/github-release)
|
8
|
+
|
9
|
+
# Command summary
|
10
|
+
|
11
|
+
- **cp**: Backport commits and eventually push them to github
|
12
|
+
- **steal**: Steal commit from upstream that fixes commit in the branch or were tagged as stable
|
13
|
+
- **list**: List commit present in the branch but not in the stable branch
|
14
|
+
- **merge**: Merge branch with suffix specified in -m <suff> into the main branch
|
15
|
+
- **push**: Push branches to github for validation
|
16
|
+
- **monitor**: Check the travis state of all branches
|
17
|
+
- **push_stable**: Push to stable repo
|
18
|
+
- **monitor_stable**: Check the travis state of all stable branches
|
19
|
+
- **release**: Create new release on all concerned branches
|
20
|
+
- **reset**: Reset branch against upstream
|
21
|
+
- **submit_release**: Push the to stable and create the release packages
|
22
|
+
|
23
|
+
# Configuration
|
24
|
+
|
25
|
+
## Basic shell setup
|
26
|
+
- git-maintain should be in your path
|
27
|
+
- Load git-maintain-completion.sh for shell completion
|
28
|
+
|
29
|
+
## Remote setup
|
30
|
+
- the 'github' remote should be your own WIP github to test out branches before submitting to the official repo
|
31
|
+
- the 'origin' remote should be the official repo in read-only mode to avoid any accidental pushes
|
32
|
+
- the 'stable' remote should be the official repo in RW mode
|
33
|
+
|
34
|
+
## Stealing commits
|
35
|
+
|
36
|
+
The steal feature uses was shamelessly copied it from https://git.kernel.org/pub/scm/linux/kernel/git/sashal/stable-tools.git/tree/
|
37
|
+
|
38
|
+
## Making releases
|
39
|
+
The release process being very specific to each project, the release command does nothing by default.
|
40
|
+
However the behaviour can be overriden for specific repo (detected by repo name)
|
41
|
+
|
42
|
+
Check the lib/addons/RDMACore.rb for an example.
|
43
|
+
In this case, the release command bump the version in all the appropriate files (after computing the previous and next version numbers), commit and tags the commit.
|
44
|
+
|
45
|
+
|
46
|
+
# How do I use it
|
47
|
+
|
48
|
+
## Branch setup
|
49
|
+
As said, this uses the branching schemes recommended/compatible of git-topic-branches.
|
50
|
+
|
51
|
+
A non compulsory recommendation is to create
|
52
|
+
I personnaly uses this scheme for my rdma-core work:
|
53
|
+
```
|
54
|
+
$ git branch
|
55
|
+
dev/stable-v15/master
|
56
|
+
dev/stable-v15/pending
|
57
|
+
dev/stable-v16/master
|
58
|
+
dev/stable-v16/pending
|
59
|
+
dev/stable-v17/master
|
60
|
+
dev/stable-v17/pending
|
61
|
+
dev/stable-v17/test
|
62
|
+
dev/stable-v18/master
|
63
|
+
dev/stable-v18/pending
|
64
|
+
* dev/stable-v19/master
|
65
|
+
```
|
66
|
+
|
67
|
+
I also use the git-topic-branches features:
|
68
|
+
```
|
69
|
+
git config --get-regexp devel-base
|
70
|
+
devel-base.dev---stable-v19 origin/stable-v19
|
71
|
+
devel-base.dev---stable-v18 origin/stable-v18
|
72
|
+
devel-base.dev---stable-v17 origin/stable-v17
|
73
|
+
devel-base.dev---stable-v16 origin/stable-v16
|
74
|
+
devel-base.dev---stable-v15 origin/stable-v15
|
75
|
+
```
|
76
|
+
This allows me to:
|
77
|
+
- autorebase on the stable branches. This is useful for dealing with development branches for stable branches
|
78
|
+
- Detect when all my patches have made it to the upstream repo
|
79
|
+
|
80
|
+
I also set the right config value for the 'steal' command to work
|
81
|
+
```
|
82
|
+
$ git config --get-regexp stable-base
|
83
|
+
stable-base.dev---stable-v19 v19
|
84
|
+
stable-base.dev---stable-v18 v18
|
85
|
+
stable-base.dev---stable-v17 v17
|
86
|
+
stable-base.dev---stable-v16 v16
|
87
|
+
stable-base.dev---stable-v15 v15
|
88
|
+
```
|
89
|
+
This will allow 'git maintain steal' to figure out what should be backported in the stable branches.
|
90
|
+
|
91
|
+
## Day-to-day workflow
|
92
|
+
|
93
|
+
Watch the mailing-lists (and/or github and/or the upstream branches) for patches that are tagged for maintainance.
|
94
|
+
Apply them to the appropriate branches
|
95
|
+
|
96
|
+
```git maintain cp -s deadbeef --version '1[789]'```
|
97
|
+
|
98
|
+
And push them to my own github repo so that Travis will check everything out
|
99
|
+
|
100
|
+
```git maintain push --version '1[789]'```
|
101
|
+
|
102
|
+
Some time later, check their status
|
103
|
+
|
104
|
+
```git maintain monitor --version '1[789]'```
|
105
|
+
|
106
|
+
If everything looks good, push to the stable repo
|
107
|
+
|
108
|
+
```git maintain push_stable --version '1[789]'```
|
109
|
+
|
110
|
+
If patches have been sent to the ML but are not yet accepted, I usually try them out on a "pending" branch.
|
111
|
+
|
112
|
+
```git maintain cp -s deadbeef --version '1[789]' -b pending```
|
113
|
+
|
114
|
+
Note that it is your own job to create the branches (yet!)
|
115
|
+
|
116
|
+
Push it to my own github too.
|
117
|
+
|
118
|
+
```git maintain push --version '1[789]' -b pending```
|
119
|
+
|
120
|
+
Once this gets accepted (and Travis is OK too), I merge this branch back to my 'master'
|
121
|
+
|
122
|
+
```git maintain merge --version '1[789]' -m pending```
|
123
|
+
|
124
|
+
The default -b option here is master so it is not required to specify it. Also branch suffixed with something else than master cannot be pushed to stable branches for safety reasons.
|
125
|
+
|
126
|
+
If this is all broken and the patch should not be applied, I simply reset my branch
|
127
|
+
|
128
|
+
```git maintain reset --version '1[789]' -m pending```
|
129
|
+
|
130
|
+
Note: This has been made as safe as possible and is querying you before doing anything destructive.
|
131
|
+
|
132
|
+
## Stealing commits
|
133
|
+
|
134
|
+
This uses a ruby version of the 'steal-stable-commits' script originally available here https://git.kernel.org/pub/scm/linux/kernel/git/sashal/stable-tools.git/tree/
|
135
|
+
|
136
|
+
It allows to automatically cherry-pick commits that have been marked as fixing a bug in your branch.
|
137
|
+
|
138
|
+
What it does is parse the master branch for commits that contains the standard
|
139
|
+
|
140
|
+
```Fixes: deadbeef00 ("broken commit msg")```
|
141
|
+
|
142
|
+
It will then check if the broken commit is in your branch (and not already fixed). It will then prompt you for reviewing the fix and apply it on your local stable branch.
|
143
|
+
|
144
|
+
If the developers from your project follow this, it removes a lot of the hassle of finding which patch applies to which branches, It will do it for you !
|
145
|
+
|
146
|
+
You can get more infos on how all this works here: https://git.kernel.org/pub/scm/linux/kernel/git/sashal/stable-tools.git/tree/README (stable steal-commits section).
|
147
|
+
|
148
|
+
One of the added feature is the ability to blacklist a commit for certain branch.
|
149
|
+
(Useful when there is a commit broken in your stable branch but the fix breaks the ABI or simply won't apply.)
|
150
|
+
|
151
|
+
Blacklisting the fix (for a specific branch) will prevent it from ever popping up when stealing commits.
|
152
|
+
Blacklisting is done through git-notes. It attaches a note to the "fix" commit in master and simply add the name of the branch it is blacklisted in. To run-blacklist the commit, edit the note and remove the branch name.
|
153
|
+
|
154
|
+
Note that blacklisted commites will show an info message when skipped so you don't have to dig through all the notes to find which one you wanted.
|
155
|
+
|
156
|
+
To run:
|
157
|
+
|
158
|
+
```git maintain steal```
|
159
|
+
|
160
|
+
|
161
|
+
## Releases
|
162
|
+
|
163
|
+
Once some work has been done, it is time for a new release.
|
164
|
+
|
165
|
+
First, unless you're working for the rdma-core repo, you'll need to add your own 'add-on' class to do automatize tyour release process. Please look at 'addons/RDMACore.rb' for an example.
|
166
|
+
|
167
|
+
Then all you need to do is create your release(s)
|
168
|
+
|
169
|
+
```git maintain release --version '1[789]'```
|
170
|
+
|
171
|
+
This will run your addon code. What you usually want to do in there is create a tag and eventually bump version numbers, add releases notes, etc.
|
172
|
+
|
173
|
+
I strongly advise here to then use the 'push_stable' command. It will update the branches, but NOT push the tag.
|
174
|
+
This means that if something has been broken by the release commit (if any), there is still time to fix it.
|
175
|
+
|
176
|
+
The tag will not have been propagated anywhere else and can be deleted manually.
|
177
|
+
|
178
|
+
```git maintain push_stable --version '1[789]'```
|
179
|
+
|
180
|
+
You can then monitor the status on Travis
|
181
|
+
|
182
|
+
```git maintain monitor_stable --version '1[789]'```
|
183
|
+
|
184
|
+
|
185
|
+
Once everything is green, it is time to submit your release
|
186
|
+
|
187
|
+
```git maintain submit_release --version '1[789]'```
|
188
|
+
|
189
|
+
This will submit the all pending tags to github, and prepare an email for the project mailing list (requires the 'patch.target' option from git-topic-branches)
|
190
|
+
|
191
|
+
Review your email, send it and your day is over !
|
192
|
+
|
193
|
+
Enjoy, and feel free to report bugs, missing features and/or send patches
|
194
|
+
|
data/bin/git-maintain
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
BACKPORT_LIB_DIR = File.dirname(__FILE__) + '/../lib'
|
8
|
+
$LOAD_PATH.push(BACKPORT_LIB_DIR)
|
9
|
+
require 'common'
|
10
|
+
$LOAD_PATH.pop()
|
11
|
+
|
12
|
+
opts = {
|
13
|
+
:br_suff => "master",
|
14
|
+
:no => false,
|
15
|
+
}
|
16
|
+
|
17
|
+
actionParser = OptionParser.new(nil, 60)
|
18
|
+
actionParser.banner = "Usage: #{__FILE__} <action> [action options]"
|
19
|
+
actionParser.separator ""
|
20
|
+
actionParser.separator "Options:"
|
21
|
+
actionParser.on("-h", "--help", "Display usage.") { |val| puts actionParser.to_s; exit 0 }
|
22
|
+
actionParser.separator "Possible actions:"
|
23
|
+
GitMaintain::getActionAttr("ACTION_HELP").each(){|x|
|
24
|
+
actionParser.separator "\t " + x
|
25
|
+
}
|
26
|
+
rest = actionParser.order!(ARGV);
|
27
|
+
if rest.length <= 0 then
|
28
|
+
STDERR.puts("Error: No action provided")
|
29
|
+
puts actionParser.to_s()
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
|
33
|
+
action_s = ARGV[0]
|
34
|
+
action = action_s.to_sym()
|
35
|
+
raise("Invalid action '#{action_s}'") if GitMaintain::getActionAttr("ACTION_LIST").index(action) == nil
|
36
|
+
opts[:action] = action
|
37
|
+
ARGV.shift()
|
38
|
+
|
39
|
+
optsParser = OptionParser.new(nil, 60)
|
40
|
+
optsParser.banner = "Usage: #{__FILE__} #{action_s} "
|
41
|
+
optsParser.separator ""
|
42
|
+
optsParser.separator "Options:"
|
43
|
+
optsParser.on("-h", "--help", "Display usage.") { |val| puts optsParser.to_s; exit 0 }
|
44
|
+
optsParser.on("-b", "--branch-suffix [SUFFIX]", "Branch suffix. Default is 'master'.") {
|
45
|
+
|val| opts[:br_suff] = val}
|
46
|
+
optsParser.on("-n", "--no", "Assume no to all questions.") {
|
47
|
+
|val| opts[:no] = true}
|
48
|
+
GitMaintain::setOpts(action, optsParser, opts)
|
49
|
+
|
50
|
+
rest = optsParser.order!(ARGV);
|
51
|
+
raise("Extra Unexpected extra arguments provided: " + rest.map(){|x|"'" + x + "'"}.join(", ")) if rest.length != 0
|
52
|
+
|
53
|
+
GitMaintain::checkOpts(opts)
|
54
|
+
GitMaintain::execAction(opts, opts[:action])
|
55
|
+
|
@@ -0,0 +1,99 @@
|
|
1
|
+
export _GIT_MAINTAIN_CMD_AWK=$(([ -f /bin/awk ] && echo "/bin/awk") || echo "/usr/bin/awk")
|
2
|
+
export _GIT_MAINTAIN_CMD_SORT=$(([ -f /bin/sort ] && echo "/bin/sort") || echo "/usr/bin/sort")
|
3
|
+
export _GIT_MAINTAIN_CMD_EGREP=$(([ -f /bin/egrep ] && echo "/bin/egrep") || echo "/usr/bin/egrep")
|
4
|
+
export _GIT_MAINTAIN_CMD_SED=$(([ -f /bin/sed ] && echo "/bin/sed") || echo "/usr/bin/sed")
|
5
|
+
|
6
|
+
_git_maintain_genoptlist(){
|
7
|
+
local COMMAND=$*
|
8
|
+
${COMMAND} --help 2>&1 | \
|
9
|
+
${_GIT_MAINTAIN_CMD_AWK} 'BEGIN { found = 0 } { if(found == 1) print $$0; if($$1 == "Options:") {found = 1}}' | \
|
10
|
+
${_GIT_MAINTAIN_CMD_EGREP} -e "^[[:space:]]*--" -e "^[[:space:]]*-[a-zA-Z0-9]" | \
|
11
|
+
${_GIT_MAINTAIN_CMD_SED} -e 's/^[[:space:]]*//' -e 's/^-[^-], //' | \
|
12
|
+
${_GIT_MAINTAIN_CMD_AWK} '{ print $1}' | \
|
13
|
+
${_GIT_MAINTAIN_CMD_SED} -e 's/^\(.*\)\[no-\]\(.*$\)/\1\2\n\1no-\2/' | \
|
14
|
+
${_GIT_MAINTAIN_CMD_SORT} -u
|
15
|
+
}
|
16
|
+
_complete_git_maintain_branch(){
|
17
|
+
case $prev in
|
18
|
+
-b|--branch-suffix)
|
19
|
+
__gitcomp_nl "$(git maintain list_suffixes)"
|
20
|
+
;;
|
21
|
+
-v|--base-version)
|
22
|
+
BRANCHES=
|
23
|
+
__gitcomp_nl "$(git maintain list_branches)"
|
24
|
+
;;
|
25
|
+
esac
|
26
|
+
}
|
27
|
+
|
28
|
+
_git_maintain_cp()
|
29
|
+
{
|
30
|
+
local OPT_LIST=$(_git_maintain_genoptlist git maintain cp)
|
31
|
+
|
32
|
+
case "$prev" in
|
33
|
+
-c|--sha1);;
|
34
|
+
*)
|
35
|
+
__gitcomp_nl "$OPT_LIST"
|
36
|
+
_complete_git_maintain_branch
|
37
|
+
;;
|
38
|
+
esac;
|
39
|
+
}
|
40
|
+
|
41
|
+
_git_maintain_merge()
|
42
|
+
{
|
43
|
+
local OPT_LIST=$(_git_maintain_genoptlist git maintain merge)
|
44
|
+
_get_comp_words_by_ref cur
|
45
|
+
|
46
|
+
case "$prev" in
|
47
|
+
-m|--merge)
|
48
|
+
__gitcomp_nl "$(git maintain list_suffixes)"
|
49
|
+
;;
|
50
|
+
*)
|
51
|
+
__gitcomp_nl "$OPT_LIST"
|
52
|
+
_complete_git_maintain_branch
|
53
|
+
;;
|
54
|
+
esac;
|
55
|
+
}
|
56
|
+
|
57
|
+
_git_maintain(){
|
58
|
+
local direct_call=${1:-1}
|
59
|
+
local cmd_word=$(expr $direct_call + 1)
|
60
|
+
|
61
|
+
__git_has_doubledash && return
|
62
|
+
|
63
|
+
_get_comp_words_by_ref cur
|
64
|
+
_get_comp_words_by_ref prev
|
65
|
+
_get_comp_words_by_ref cword
|
66
|
+
|
67
|
+
|
68
|
+
if [ $cword -eq $cmd_word ]; then
|
69
|
+
case "$cur" in
|
70
|
+
-*)
|
71
|
+
__gitcomp_nl "$(_git_maintain_genoptlist git maintain)"
|
72
|
+
return
|
73
|
+
;;
|
74
|
+
*)
|
75
|
+
__gitcomp_nl "$(git maintain list_actions | grep -v list_actions)"
|
76
|
+
return
|
77
|
+
;;
|
78
|
+
esac
|
79
|
+
else
|
80
|
+
_get_comp_words_by_ref words
|
81
|
+
local cmd_name=${words[$cmd_word]}
|
82
|
+
completion_func="_git_maintain_${cmd_name}"
|
83
|
+
declare -f $completion_func > /dev/null
|
84
|
+
if [ $? -eq 0 ]; then
|
85
|
+
$completion_func
|
86
|
+
else
|
87
|
+
OPT_LIST=$(_git_maintain_genoptlist git maintain $cmd_name)
|
88
|
+
case "$prev" in
|
89
|
+
*)
|
90
|
+
__gitcomp_nl "$OPT_LIST"
|
91
|
+
esac
|
92
|
+
fi
|
93
|
+
fi
|
94
|
+
|
95
|
+
}
|
96
|
+
|
97
|
+
__git_maintain(){
|
98
|
+
_git_maintain 0
|
99
|
+
} && complete -F __git_maintain git-maintain
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module GitMaintain
|
2
|
+
class RDMACoreBranch < Branch
|
3
|
+
REPO_NAME = "rdma-core"
|
4
|
+
|
5
|
+
def release(opts)
|
6
|
+
prev_ver=@repo.runGit("show HEAD:CMakeLists.txt | egrep \"[sS][eE][tT]\\\\(PACKAGE_VERSION\"").
|
7
|
+
chomp().gsub(/[sS][eE][tT]\(PACKAGE_VERSION\s*"([0-9.]*)".*$/, '\1')
|
8
|
+
ver_nums = prev_ver.split(".")
|
9
|
+
new_ver = (ver_nums[0 .. -2] + [ver_nums[-1].to_i() + 1 ]).join(".")
|
10
|
+
git_prev_ver = "v" + (ver_nums[-1] == "0" ? ver_nums[0 .. -2].join(".") : prev_ver)
|
11
|
+
|
12
|
+
puts "Preparing release #{prev_ver} => #{new_ver}"
|
13
|
+
rep = GitMaintain::checkLog(opts, @local_branch, git_prev_ver, "release")
|
14
|
+
if rep != "y" then
|
15
|
+
puts "Skipping release"
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
# Prepare tag message
|
20
|
+
tag_path=`mktemp`.chomp()
|
21
|
+
puts tag_path
|
22
|
+
tag_file = File.open(tag_path, "w+")
|
23
|
+
tag_file.puts "rdma-core-#{new_ver}:"
|
24
|
+
tag_file.puts ""
|
25
|
+
tag_file.puts "Updates from version #{prev_ver}"
|
26
|
+
tag_file.puts " * Backport fixes:"
|
27
|
+
tag_file.puts `git log HEAD ^#{git_prev_ver} --format=' * %s'`
|
28
|
+
tag_file.close()
|
29
|
+
|
30
|
+
# Update version number in relevant files
|
31
|
+
@repo.run("sed -i -e 's/\\(Version:[[:space:]]*\\)[0-9.]*/\\1#{new_ver}/g' redhat/rdma-core.spec suse/rdma-core.spec")
|
32
|
+
@repo.run("sed -i -e 's/\\([sS][eE][tT](PACKAGE_VERSION[[:space:]]*\"\\)[0-9.]*\"/\\1#{new_ver}\"/g' CMakeLists.txt")
|
33
|
+
|
34
|
+
@repo.run("cat <<EOF > debian/changelog.new
|
35
|
+
rdma-core (#{new_ver}-1) unstable; urgency=low
|
36
|
+
|
37
|
+
* New upstream release.
|
38
|
+
|
39
|
+
-- $(git config user.name) <$(git config user.email)> $(date '+%a, %d %b %Y %T %z')
|
40
|
+
|
41
|
+
$(cat debian/changelog)
|
42
|
+
EOF
|
43
|
+
mv debian/changelog.new debian/changelog")
|
44
|
+
|
45
|
+
# Add and commit
|
46
|
+
@repo.runGit("add redhat/rdma-core.spec suse/rdma-core.spec CMakeLists.txt debian/changelog")
|
47
|
+
@repo.runGit("commit -m 'Bump to version #{new_ver}' --verbose --edit --signoff")
|
48
|
+
if $? != 0 then
|
49
|
+
raise("Failed to commit on branch #{local_branch}")
|
50
|
+
end
|
51
|
+
@repo.runGit("tag -a -s v#{new_ver} --edit -F #{tag_path}")
|
52
|
+
if $? != 0 then
|
53
|
+
raise("Failed to tag branch #{local_branch}")
|
54
|
+
end
|
55
|
+
`rm -f #{tag_path}`
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
GitMaintain::registerCustom(RDMACoreBranch::REPO_NAME,
|
60
|
+
{ GitMaintain::Branch => RDMACoreBranch })
|
61
|
+
end
|
data/lib/branch.rb
ADDED
@@ -0,0 +1,460 @@
|
|
1
|
+
module GitMaintain
|
2
|
+
|
3
|
+
class CherryPickErrorException < StandardError
|
4
|
+
def initialize(str, commit)
|
5
|
+
@commit = commit
|
6
|
+
super(str)
|
7
|
+
end
|
8
|
+
attr_reader :commit
|
9
|
+
end
|
10
|
+
|
11
|
+
class Branch
|
12
|
+
ACTION_LIST = [
|
13
|
+
:cp, :steal, :list, :merge,
|
14
|
+
:push, :monitor,
|
15
|
+
:push_stable, :monitor_stable,
|
16
|
+
:release, :reset
|
17
|
+
]
|
18
|
+
NO_FETCH_ACTIONS = [
|
19
|
+
:cp, :merge, :monitor, :release
|
20
|
+
]
|
21
|
+
NO_CHECKOUT_ACTIONS = [
|
22
|
+
:list, :push, :monitor, :monitor_stable
|
23
|
+
]
|
24
|
+
ACTION_HELP = [
|
25
|
+
"* cp: Backport commits and eventually push them to github",
|
26
|
+
"* steal: Steal commit from upstream that fixes commit in the branch or were tagged as stable",
|
27
|
+
"* list: List commit present in the branch but not in the stable branch",
|
28
|
+
"* merge: Merge branch with suffix specified in -m <suff> into the main branch",
|
29
|
+
"* push: Push branches to github for validation",
|
30
|
+
"* monitor: Check the travis state of all branches",
|
31
|
+
"* push_stable: Push to stable repo",
|
32
|
+
"* monitor_stable: Check the travis state of all stable branches",
|
33
|
+
"* release: Create new release on all concerned branches",
|
34
|
+
"* reset: Reset branch against upstream",
|
35
|
+
]
|
36
|
+
|
37
|
+
def self.load(repo, version, travis, branch_suff)
|
38
|
+
repo_name = File.basename(repo.path)
|
39
|
+
return GitMaintain::loadClass(Branch, repo_name, repo, version, travis, branch_suff)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.set_opts(action, optsParser, opts)
|
43
|
+
opts[:base_ver] = 0
|
44
|
+
opts[:version] = /.*/
|
45
|
+
opts[:commits] = []
|
46
|
+
opts[:do_merge] = false
|
47
|
+
opts[:push_force] = false
|
48
|
+
opts[:no_travis] = false
|
49
|
+
|
50
|
+
optsParser.on("-v", "--base-version [MIN_VER]", Integer, "Older release to consider.") {
|
51
|
+
|val| opts[:base_ver] = val}
|
52
|
+
optsParser.on("-V", "--version [regexp]", Regexp, "Regexp to filter versions.") {
|
53
|
+
|val| opts[:version] = val}
|
54
|
+
|
55
|
+
case action
|
56
|
+
when :cp
|
57
|
+
optsParser.banner += "-c <sha1> [-c <sha1> ...]"
|
58
|
+
optsParser.on("-c", "--sha1 [SHA1]", String, "Commit to cherry-pick. Can be used multiple time.") {
|
59
|
+
|val| opts[:commits] << val}
|
60
|
+
when :merge
|
61
|
+
optsParser.banner += "-m <suffix>"
|
62
|
+
optsParser.on("-m", "--merge [SUFFIX]", "Merge branch with suffix.") {
|
63
|
+
|val| opts[:do_merge] = val}
|
64
|
+
when :push
|
65
|
+
optsParser.banner += "[-f]"
|
66
|
+
optsParser.on("-f", "--force", "Add --force to git push (for 'push' action).") {
|
67
|
+
|val| opts[:push_force] = val}
|
68
|
+
when :push_stable
|
69
|
+
optsParser.banner += "[-T]"
|
70
|
+
optsParser.on("-T", "--no-travis", "Ignore Travis build status and push anyway.") {
|
71
|
+
|val| opts[:no_travis] = val}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.check_opts(opts)
|
76
|
+
if opts[:action] == :push_stable ||
|
77
|
+
opts[:action] == :release then
|
78
|
+
if opts[:br_suff] != "master" then
|
79
|
+
raise "Action #{opts[:action]} can only be done on 'master' suffixed branches"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.execAction(opts, action)
|
85
|
+
repo = Repo::load()
|
86
|
+
travis = TravisChecker::load(repo)
|
87
|
+
|
88
|
+
if NO_FETCH_ACTIONS.index(action) == nil then
|
89
|
+
repo.stableUpdate()
|
90
|
+
end
|
91
|
+
|
92
|
+
repo.getStableList(opts[:br_suff]).each(){|br|
|
93
|
+
branch = Branch::load(repo, br, travis, opts[:br_suff])
|
94
|
+
case branch.is_targetted?(opts)
|
95
|
+
when :too_old
|
96
|
+
puts "# Skipping older v#{branch.version}"
|
97
|
+
next
|
98
|
+
when :no_match
|
99
|
+
puts "# Skipping v#{branch.version} not matching #{opts[:version].to_s()}"
|
100
|
+
next
|
101
|
+
end
|
102
|
+
|
103
|
+
puts "###############################"
|
104
|
+
puts "# Working on v#{branch.version}"
|
105
|
+
puts "###############################"
|
106
|
+
|
107
|
+
if NO_CHECKOUT_ACTIONS.index(action) == nil then
|
108
|
+
branch.checkout()
|
109
|
+
end
|
110
|
+
branch.send(action, opts)
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def initialize(repo, version, travis, branch_suff)
|
115
|
+
GitMaintain::checkDirectConstructor(self.class)
|
116
|
+
|
117
|
+
@repo = repo
|
118
|
+
@version = version
|
119
|
+
@travis = travis
|
120
|
+
@branch_suff = branch_suff
|
121
|
+
|
122
|
+
@local_branch = "dev/stable-v#{@version}/#{@branch_suff}"
|
123
|
+
@head = @repo.runGit("rev-parse #{@local_branch}")
|
124
|
+
|
125
|
+
@remote_branch ="stable-v#{@version}"
|
126
|
+
@remote_ref = "#{Repo::STABLE_REPO}/#{@remote_branch}"
|
127
|
+
@stable_head = @repo.runGit("rev-parse #{@remote_ref}")
|
128
|
+
@stable_base = @repo.findStableBase(@local_branch)
|
129
|
+
end
|
130
|
+
attr_reader :version, :local_branch, :head, :remote_branch, :remote_ref, :stable_head
|
131
|
+
|
132
|
+
def is_targetted?(opts)
|
133
|
+
if @version.to_i < opts[:base_ver] then
|
134
|
+
return :too_old
|
135
|
+
end
|
136
|
+
if @version !~ opts[:version] then
|
137
|
+
return :no_match
|
138
|
+
end
|
139
|
+
return true
|
140
|
+
end
|
141
|
+
|
142
|
+
# Checkout the repo to the given branch
|
143
|
+
def checkout()
|
144
|
+
print @repo.runGit("checkout -q #{@local_branch}")
|
145
|
+
if $? != 0 then
|
146
|
+
raise "Error: Failed to checkout the branch"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Cherry pick an array of commits
|
151
|
+
def cherry_pick(opts)
|
152
|
+
if opts[:commits].length > 0 then
|
153
|
+
@repo.runGit("cherry-pick #{opts[:commits].join(" ")}")
|
154
|
+
if $? != 0 then
|
155
|
+
puts "Cherry pick failure. Starting bash for manual fixes. Exit shell to continue"
|
156
|
+
@repo.runSystem("bash")
|
157
|
+
puts "Continuing..."
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Steal upstream commits that are not in the branch
|
163
|
+
def steal(opts)
|
164
|
+
steal_all(opts, "#{@stable_base}..origin/master")
|
165
|
+
end
|
166
|
+
|
167
|
+
# List commits in the branch that are no in the stable branch
|
168
|
+
def list(opts)
|
169
|
+
GitMaintain::checkLog(opts, @local_branch, @remote_ref, nil)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Merge merge_branch into this one
|
173
|
+
def merge(opts)
|
174
|
+
merge_branch = "dev/stable-v#{@version}/#{opts[:do_merge]}"
|
175
|
+
rep = GitMaintain::checkLog(opts, merge_branch, @local_branch, "merge")
|
176
|
+
if rep == "y" then
|
177
|
+
@repo.runGit("merge #{merge_branch}")
|
178
|
+
if $? != 0 then
|
179
|
+
puts "Merge failure. Starting bash for manual fixes. Exit shell to continue"
|
180
|
+
@repo.runSystem("bash")
|
181
|
+
puts "Continuing..."
|
182
|
+
end
|
183
|
+
else
|
184
|
+
puts "Skipping merge"
|
185
|
+
return
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Push the branch to the validation repo
|
190
|
+
def push(opts)
|
191
|
+
@repo.runGit("push #{opts[:push_force] == true ? "-f" : ""} #{Repo::VALID_REPO} #{@local_branch}")
|
192
|
+
end
|
193
|
+
|
194
|
+
# Monitor the build status on Travis
|
195
|
+
def monitor(opts)
|
196
|
+
st = @travis.getValidState(head)
|
197
|
+
puts "Status for v#{@version}: " + st
|
198
|
+
if st == "failed"
|
199
|
+
rep = "y"
|
200
|
+
suff=""
|
201
|
+
while rep == "y"
|
202
|
+
rep = GitMaintain::confirm(opts, "see the build log#{suff}")
|
203
|
+
if rep == "y" then
|
204
|
+
log = @travis.getValidLog(head)
|
205
|
+
tmp = `mktemp`.chomp()
|
206
|
+
tmpfile = File.open(tmp, "w+")
|
207
|
+
tmpfile.puts(log)
|
208
|
+
tmpfile.close()
|
209
|
+
system("less -r #{tmp}")
|
210
|
+
`rm -f #{tmp}`
|
211
|
+
end
|
212
|
+
suff=" again"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Push branch to the stable repo
|
218
|
+
def push_stable(opts)
|
219
|
+
if opts[:no_travis] != true &&
|
220
|
+
@travis.checkValidState(@head) != true then
|
221
|
+
puts "Build is not passed on travis. Skipping push to stable"
|
222
|
+
return
|
223
|
+
end
|
224
|
+
rep = GitMaintain::checkLog(opts, @local_branch, @remote_ref, "submit")
|
225
|
+
if rep == "y" then
|
226
|
+
@repo.runGit("push #{Repo::STABLE_REPO} #{@local_branch}:#{@remote_branch}")
|
227
|
+
else
|
228
|
+
puts "Skipping push to stable"
|
229
|
+
return
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Monitor the build status of the stable branch on Travis
|
234
|
+
def monitor_stable(opts)
|
235
|
+
puts "Status for v#{@version}: " + @travis.getStableState(@stable_head)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Reset the branch to the upstream stable one
|
239
|
+
def reset(opts)
|
240
|
+
rep = GitMaintain::checkLog(opts, @local_branch, @remote_ref, "reset")
|
241
|
+
if rep == "y" then
|
242
|
+
@repo.runGit("reset --hard #{@remote_ref}")
|
243
|
+
else
|
244
|
+
puts "Skipping reset"
|
245
|
+
return
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def release(opts)
|
250
|
+
puts "#No release command available for this repo"
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
def add_blacklist(commit)
|
255
|
+
@repo.runGit("notes append -m \"#{@local_branch}\" #{commit}")
|
256
|
+
end
|
257
|
+
|
258
|
+
def is_blacklisted?(commit)
|
259
|
+
@repo.runGit("notes show #{commit} 2> /dev/null").split("\n").each(){|br|
|
260
|
+
return true if br == @local_branch
|
261
|
+
}
|
262
|
+
return false
|
263
|
+
end
|
264
|
+
|
265
|
+
def make_pretty(orig_commit, commit="")
|
266
|
+
orig_sha=@repo.runGit("rev-parse #{orig_commit}")
|
267
|
+
msg_commit = (commit.to_s() == "") ? orig_sha : commit
|
268
|
+
|
269
|
+
msg_path=`mktemp`.chomp()
|
270
|
+
msg_file = File.open(msg_path, "w+")
|
271
|
+
msg_file.puts @repo.runGit("log -1 --format=\"%s%n%n[ Upstream commit #{msg_commit} ]%n%n%b\" #{orig_commit}")
|
272
|
+
msg_file.close()
|
273
|
+
@repo.runGit("commit -s --amend -F #{msg_path}")
|
274
|
+
`rm -f #{msg_path}`
|
275
|
+
end
|
276
|
+
|
277
|
+
def is_in_tree?(commit)
|
278
|
+
fullhash=@repo.runGit("rev-parse #{commit}")
|
279
|
+
# This might happen if someone pointed to a commit that doesn't exist in our
|
280
|
+
# tree.
|
281
|
+
if $? != 0 then
|
282
|
+
return false
|
283
|
+
end
|
284
|
+
|
285
|
+
# Hope for the best, same commit is/isn't in the current branch
|
286
|
+
if @repo.runGit("merge-base #{fullhash} HEAD") == fullhash then
|
287
|
+
return true
|
288
|
+
end
|
289
|
+
|
290
|
+
# Grab the subject, since commit sha1 is different between branches we
|
291
|
+
# have to look it up based on subject.
|
292
|
+
subj=@repo.runGit("log -1 --pretty=\"%s\" #{commit}")
|
293
|
+
if $? != 0 then
|
294
|
+
return false
|
295
|
+
end
|
296
|
+
|
297
|
+
# Try and find if there's a commit with given subject the hard way
|
298
|
+
@repo.runGit("log --pretty=\"%H\" -F --grep \"#{subj}\" "+
|
299
|
+
"#{@stable_base}..HEAD").split("\n").each(){|cmt|
|
300
|
+
cursubj=@repo.runGit("log -1 --format=\"%s\" #{cmt}")
|
301
|
+
if cursubj = subj then
|
302
|
+
return true
|
303
|
+
end
|
304
|
+
}
|
305
|
+
return false
|
306
|
+
end
|
307
|
+
|
308
|
+
def is_relevant?(commit)
|
309
|
+
# Let's grab the commit that this commit fixes (if exists (based on the "Fixes:" tag)).
|
310
|
+
fixescmt=@repo.runGit("log -1 #{commit} | grep -i \"fixes:\" | head -n 1 | "+
|
311
|
+
"sed -e 's/^[ \\t]*//' | cut -f 2 -d ':' | "+
|
312
|
+
"sed -e 's/^[ \\t]*//' -e 's/\\([0-9a-f]\\+\\)(/\\1 (/' | cut -f 1 -d ' '")
|
313
|
+
|
314
|
+
# If this commit fixes anything, but the broken commit isn't in our branch we don't
|
315
|
+
# need this commit either.
|
316
|
+
if fixescmt != "" then
|
317
|
+
if is_in_tree?(fixescmt) then
|
318
|
+
return true
|
319
|
+
else
|
320
|
+
return false
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
if @repo.runGit("show #{commit} | grep -i 'stable@' | wc -l") == "0" then
|
325
|
+
return false
|
326
|
+
end
|
327
|
+
|
328
|
+
# Let's see if there's a version tag in this commit
|
329
|
+
full=@repo.runGit("show #{commit} | grep -i 'stable@'").gsub(/.* /, "")
|
330
|
+
|
331
|
+
# Sanity check our extraction
|
332
|
+
if full =~ /stable/ then
|
333
|
+
return false
|
334
|
+
end
|
335
|
+
|
336
|
+
# Make sure our branch contains this version
|
337
|
+
if @repo.runGit("merge-base #{@head} #{full}") == full then
|
338
|
+
return true
|
339
|
+
end
|
340
|
+
|
341
|
+
# Tag is not in history, ignore
|
342
|
+
return false
|
343
|
+
end
|
344
|
+
|
345
|
+
def pick_one(commit)
|
346
|
+
@repo.runGit("cherry-pick --strategy=recursive -Xpatience -x #{commit} &> /dev/null")
|
347
|
+
return if $? == 0
|
348
|
+
|
349
|
+
if [ @repo.runGit("status -uno --porcelain | wc -l") != 0 ]; then
|
350
|
+
@repo.runGit("reset --hard")
|
351
|
+
return
|
352
|
+
end
|
353
|
+
@repo.runGit("reset --hard")
|
354
|
+
# That didn't work? Let's try that with every variation of the commit
|
355
|
+
# in other stable trees.
|
356
|
+
find_alts(commit).each(){|alt_commit|
|
357
|
+
@repo.runCmd("cherry-pick --strategy=recursive -Xpatience -x #{alt_commit} &> /dev/null")
|
358
|
+
if $? == 0 then
|
359
|
+
return
|
360
|
+
end
|
361
|
+
@repo.runCmd("reset --hard")
|
362
|
+
}
|
363
|
+
# Still no? Let's go back to the original commit and hand it off to
|
364
|
+
# the user.
|
365
|
+
@repo.runCmd("cherry-pick --strategy=recursive -Xpatience -x #{commit} &> /dev/null")
|
366
|
+
raise CherryPickErrorException.new("Failed to cherry pick commit #{commit}", commit)
|
367
|
+
return false
|
368
|
+
end
|
369
|
+
|
370
|
+
def confirm_one(opts, commit)
|
371
|
+
rep=""
|
372
|
+
do_cp=false
|
373
|
+
puts @repo.runGit("show --format=oneline --no-patch --no-decorate #{commit}")
|
374
|
+
while rep != "y" do
|
375
|
+
puts "Do you want to steal this commit ? (y/n/b/?)"
|
376
|
+
if opts[:no] == true then
|
377
|
+
puts "Auto-replying no due to --no option"
|
378
|
+
rep = 'n'
|
379
|
+
break
|
380
|
+
else
|
381
|
+
rep = STDIN.gets.chomp()
|
382
|
+
end
|
383
|
+
case rep
|
384
|
+
when "n"
|
385
|
+
puts "Skip this commit"
|
386
|
+
break
|
387
|
+
when "b"
|
388
|
+
puts "Blacklisting this commit for the current branch"
|
389
|
+
add_blacklist(commit)
|
390
|
+
break
|
391
|
+
when "y"
|
392
|
+
rep="y"
|
393
|
+
do_cp=true
|
394
|
+
break
|
395
|
+
when "?"
|
396
|
+
puts @repo.runGit("show #{commit}")
|
397
|
+
else
|
398
|
+
STDERR.puts "Invalid answer $rep"
|
399
|
+
puts @repo.runGit("show --format=oneline --no-patch --no-decorate #{commit}")
|
400
|
+
end
|
401
|
+
end
|
402
|
+
return do_cp
|
403
|
+
end
|
404
|
+
|
405
|
+
def steal_one(opts, commit)
|
406
|
+
subj=@repo.runGit("log -1 --format=\"%s\" #{commit}")
|
407
|
+
msg=''
|
408
|
+
|
409
|
+
# Let's grab the mainline commit id, this is useful if the version tag
|
410
|
+
# doesn't exist in the commit we're looking at but exists upstream.
|
411
|
+
orig_cmt=@repo.runGit("log --no-merges --format=\"%H\" -F --grep \"#{subj}\" " +
|
412
|
+
"#{@stable_base}..origin/master | tail -n1")
|
413
|
+
|
414
|
+
# If the commit doesn't apply for us, skip it
|
415
|
+
if is_relevant?(orig_cmt) != true
|
416
|
+
return
|
417
|
+
end
|
418
|
+
|
419
|
+
if is_in_tree?(orig_cmt) == true
|
420
|
+
# Commit is already in the stable branch, skip
|
421
|
+
return
|
422
|
+
end
|
423
|
+
|
424
|
+
# Check if it's not blacklisted by a git-notes
|
425
|
+
if is_blacklisted?(orig_cmt) == true then
|
426
|
+
# Commit is blacklisted
|
427
|
+
puts "Skipping 'blacklisted' commit " +
|
428
|
+
@repo.runGit("show --format=oneline --no-patch --no-decorate #{orig_cmt}")
|
429
|
+
return
|
430
|
+
end
|
431
|
+
|
432
|
+
do_cp = confirm_one(opts, orig_cmt)
|
433
|
+
return if do_cp != true
|
434
|
+
|
435
|
+
begin
|
436
|
+
pick_one(commit)
|
437
|
+
rescue CherryPickErrorException => e
|
438
|
+
puts "Cherry pick failed. Fix, commit (or reset) and exit."
|
439
|
+
@repo.runSystem("/bin/bash")
|
440
|
+
return
|
441
|
+
end
|
442
|
+
|
443
|
+
# If we didn't find the commit upstream then this must be a custom commit
|
444
|
+
# in the given tree - make sure the user checks this commit.
|
445
|
+
if orig_cmt == "" then
|
446
|
+
msg="Custom"
|
447
|
+
orig_cmt=@repo.runGit("rev-parse HEAD")
|
448
|
+
puts "Custom commit, please double-check!"
|
449
|
+
@repo.runSystem("/bin/bash")
|
450
|
+
end
|
451
|
+
make_pretty(orig_cmt, msg)
|
452
|
+
end
|
453
|
+
|
454
|
+
def steal_all(opts, range)
|
455
|
+
@repo.runGit("log --no-merges --format=\"%H\" #{range} | tac").split("\n").each(){|commit|
|
456
|
+
steal_one(opts, commit)
|
457
|
+
}
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
data/lib/common.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
$LOAD_PATH.push(BACKPORT_LIB_DIR)
|
2
|
+
|
3
|
+
require 'travis'
|
4
|
+
require 'repo'
|
5
|
+
require 'branch'
|
6
|
+
|
7
|
+
$LOAD_PATH.pop()
|
8
|
+
|
9
|
+
module GitMaintain
|
10
|
+
class Common
|
11
|
+
ACTION_LIST = [ :list_actions ]
|
12
|
+
ACTION_HELP = []
|
13
|
+
def self.execAction(opts, action)
|
14
|
+
puts GitMaintain::getActionAttr("ACTION_LIST").join("\n")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
ACTION_CLASS = [ Common, Branch, Repo ]
|
19
|
+
@@custom_classes = {}
|
20
|
+
|
21
|
+
def registerCustom(repo_name, classes)
|
22
|
+
raise("Multiple class for repo #{repo_name}") if @@custom_classes[repo_name] != nil
|
23
|
+
@@custom_classes[repo_name] = classes
|
24
|
+
end
|
25
|
+
module_function :registerCustom
|
26
|
+
|
27
|
+
def getCustom(repo_name)
|
28
|
+
return @@custom_classes[repo_name]
|
29
|
+
end
|
30
|
+
module_function :getCustom
|
31
|
+
|
32
|
+
def loadClass(default_class, repo_name, *more)
|
33
|
+
custom = @@custom_classes[repo_name]
|
34
|
+
if custom != nil && custom[default_class] != nil then
|
35
|
+
puts "# Detected custom #{default_class} class for repo '#{repo_name}'" if ENV['DEBUG'] == "1"
|
36
|
+
return custom[default_class].new(*more)
|
37
|
+
else
|
38
|
+
puts "# Detected NO custom #{default_class} classes for repo '#{repo_name}'" if ENV['DEBUG'] == "1"
|
39
|
+
return default_class.new(*more)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
module_function :loadClass
|
43
|
+
|
44
|
+
# Check that the constructor was called through loadClass
|
45
|
+
def checkDirectConstructor(theClass)
|
46
|
+
# Look for the "new" in the calling tree
|
47
|
+
depth = 1
|
48
|
+
while caller_locations(depth, 1)[0].label != "new"
|
49
|
+
depth +=1
|
50
|
+
end
|
51
|
+
# The function that called the constructer is just one step below
|
52
|
+
raise("Use GitMaintain::loadClass to construct a #{theClass} class") if
|
53
|
+
caller_locations(depth + 1, 1)[0].label != "loadClass"
|
54
|
+
end
|
55
|
+
module_function :checkDirectConstructor
|
56
|
+
|
57
|
+
def getActionAttr(attr)
|
58
|
+
return ACTION_CLASS.map(){|x| x.const_get(attr)}.flatten()
|
59
|
+
end
|
60
|
+
module_function :getActionAttr
|
61
|
+
|
62
|
+
def setOpts(action, optsParser, opts)
|
63
|
+
ACTION_CLASS.each(){|x|
|
64
|
+
next if x::ACTION_LIST.index(action) == nil
|
65
|
+
next if x.singleton_methods().index(:set_opts) == nil
|
66
|
+
x.set_opts(action, optsParser, opts)
|
67
|
+
break
|
68
|
+
}
|
69
|
+
end
|
70
|
+
module_function :setOpts
|
71
|
+
|
72
|
+
def checkOpts(opts)
|
73
|
+
ACTION_CLASS.each(){|x|
|
74
|
+
next if x.singleton_methods().index(:check_opts) == nil
|
75
|
+
x.check_opts(opts)
|
76
|
+
}
|
77
|
+
end
|
78
|
+
module_function :checkOpts
|
79
|
+
|
80
|
+
def execAction(opts, action)
|
81
|
+
ACTION_CLASS.each(){|x|
|
82
|
+
next if x::ACTION_LIST.index(action) == nil
|
83
|
+
x.execAction(opts, action)
|
84
|
+
break
|
85
|
+
}
|
86
|
+
end
|
87
|
+
module_function :execAction
|
88
|
+
|
89
|
+
def confirm(opts, msg)
|
90
|
+
rep = 't'
|
91
|
+
while rep != "y" && rep != "n" && rep != '' do
|
92
|
+
puts "Do you wish to #{msg} ? (y/N): "
|
93
|
+
if opts[:no] == true then
|
94
|
+
puts "Auto-replying no due to --no option"
|
95
|
+
rep = 'n'
|
96
|
+
else
|
97
|
+
rep = STDIN.gets.chomp()
|
98
|
+
end
|
99
|
+
end
|
100
|
+
return rep
|
101
|
+
end
|
102
|
+
module_function :confirm
|
103
|
+
|
104
|
+
def checkLog(opts, br1, br2, action_msg)
|
105
|
+
puts "Diff between #{br1} and #{br2}"
|
106
|
+
puts `git shortlog #{br1} ^#{br2}`
|
107
|
+
return "n" if action_msg.to_s() == ""
|
108
|
+
rep = confirm(opts, "#{action_msg} this branch")
|
109
|
+
return rep
|
110
|
+
end
|
111
|
+
module_function :checkLog
|
112
|
+
|
113
|
+
end
|
114
|
+
$LOAD_PATH.pop()
|
115
|
+
|
116
|
+
|
117
|
+
# Load all custom classes
|
118
|
+
$LOAD_PATH.push(BACKPORT_LIB_DIR + "/addons/")
|
119
|
+
Dir.entries(BACKPORT_LIB_DIR + "/addons/").each(){|entry|
|
120
|
+
next if (!File.file?(BACKPORT_LIB_DIR + "/addons/" + entry) || entry !~ /\.rb$/ );
|
121
|
+
require entry.sub(/.rb$/, "")
|
122
|
+
}
|
123
|
+
$LOAD_PATH.pop()
|
data/lib/repo.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
module GitMaintain
|
2
|
+
class Repo
|
3
|
+
VALID_REPO = "github"
|
4
|
+
STABLE_REPO = "stable"
|
5
|
+
SUBMIT_BINARY="/usr/bin/git-release.ruby2.5"
|
6
|
+
|
7
|
+
ACTION_LIST = [
|
8
|
+
:list_branches,
|
9
|
+
# Internal commands for completion
|
10
|
+
:list_suffixes, :submit_release
|
11
|
+
]
|
12
|
+
ACTION_HELP = [
|
13
|
+
"* submit_release: Push the to stable and create the release packages",
|
14
|
+
]
|
15
|
+
|
16
|
+
def self.load(path=".")
|
17
|
+
dir = Dir.pwd()
|
18
|
+
repo_name = File.basename(dir)
|
19
|
+
return GitMaintain::loadClass(Repo, repo_name, dir)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.check_opts(opts)
|
23
|
+
if opts[:action] == :submit_release then
|
24
|
+
if opts[:br_suff] != "master" then
|
25
|
+
raise "Action #{opts[:action]} can only be done on 'master' suffixed branches"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.execAction(opts, action)
|
31
|
+
repo = Repo::load()
|
32
|
+
|
33
|
+
if action == :submit_release then
|
34
|
+
repo.stableUpdate()
|
35
|
+
end
|
36
|
+
repo.send(action, opts)
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(path=nil)
|
40
|
+
GitMaintain::checkDirectConstructor(self.class)
|
41
|
+
|
42
|
+
@path = path
|
43
|
+
@stable_list=nil
|
44
|
+
@stable_branches=nil
|
45
|
+
@suffix_list=nil
|
46
|
+
|
47
|
+
if path == nil
|
48
|
+
@path = Dir.pwd()
|
49
|
+
end
|
50
|
+
@remote_valid=runGit("remote -v | egrep '^#{VALID_REPO}' | grep fetch |
|
51
|
+
awk '{ print $2}' | sed -e 's/.*://' -e 's/\.git//'")
|
52
|
+
@remote_stable=runGit("remote -v | egrep '^#{STABLE_REPO}' | grep fetch |
|
53
|
+
awk '{ print $2}' | sed -e 's/.*://' -e 's/\.git//'")
|
54
|
+
@stable_base_patterns=
|
55
|
+
runGit("config --get-regexp stable-base | egrep '^stable-base\.' | "+
|
56
|
+
"sed -e 's/stable-base\.//' -e 's/---/\\//g'").split("\n").inject({}){ |m, x|
|
57
|
+
y=x.split(" ");
|
58
|
+
m[y[0]] = y[1]
|
59
|
+
m
|
60
|
+
}
|
61
|
+
end
|
62
|
+
attr_reader :path, :remote_valid, :remote_stable
|
63
|
+
|
64
|
+
def run(cmd)
|
65
|
+
return `cd #{@path} && #{cmd}`
|
66
|
+
end
|
67
|
+
def runSystem(cmd)
|
68
|
+
return system("cd #{@path} && #{cmd}")
|
69
|
+
end
|
70
|
+
def runGit(cmd)
|
71
|
+
if ENV["DEBUG"].to_s() != "" then
|
72
|
+
puts "Called from #{caller[1]}"
|
73
|
+
puts "Running git command '#{cmd}'"
|
74
|
+
end
|
75
|
+
return `git --work-tree=#{@path} #{cmd}`.chomp()
|
76
|
+
end
|
77
|
+
def runGitImap(cmd)
|
78
|
+
return `export GIT_ASKPASS=$(dirname $(dirname $(which git)))/lib/git-core/git-gui--askpass;
|
79
|
+
if [ ! -f $GIT_ASKPASS ]; then
|
80
|
+
export GIT_ASKPASS=$(dirname $(which git))/git-gui--askpass;
|
81
|
+
fi;
|
82
|
+
if [ ! -f $GIT_ASKPASS ]; then
|
83
|
+
export GIT_ASKPASS=/usr/lib/ssh/ssh-askpass;
|
84
|
+
fi; git --work-tree=#{@path} imap-send #{cmd}`
|
85
|
+
end
|
86
|
+
|
87
|
+
def stableUpdate()
|
88
|
+
puts "# Fetching stable updates..."
|
89
|
+
runGit("fetch #{STABLE_REPO}")
|
90
|
+
end
|
91
|
+
def getStableList(br_suff)
|
92
|
+
return @stable_list if @stable_list != nil
|
93
|
+
|
94
|
+
@stable_list=runGit("branch").split("\n").map(){|x|
|
95
|
+
x=~ /dev\/stable-v[0-9]+\/#{br_suff}/ ?
|
96
|
+
x.gsub(/\*?\s*dev\/stable-v([0-9]+)\/#{br_suff}\s*$/, '\1') :
|
97
|
+
nil}.compact().uniq()
|
98
|
+
|
99
|
+
return @stable_list
|
100
|
+
end
|
101
|
+
|
102
|
+
def getSuffixList()
|
103
|
+
return @suffix_list if @suffix_list != nil
|
104
|
+
|
105
|
+
@suffix_list = runGit("branch").split("\n").map(){|x|
|
106
|
+
x=~ /dev\/stable-v[0-9]+\/[a-zA-Z0-9_-]+/ ?
|
107
|
+
x.gsub(/\*?\s*dev\/stable-v[0-9]+\/([a-zA-Z0-9_-]+)\s*$/, '\1') :
|
108
|
+
nil}.compact().uniq()
|
109
|
+
|
110
|
+
return @suffix_list
|
111
|
+
end
|
112
|
+
|
113
|
+
def submitReleases()
|
114
|
+
remote_tags=runGit("ls-remote --tags #{STABLE_REPO} |
|
115
|
+
egrep 'refs/tags/v[0-9.]*$'").split("\n").map(){
|
116
|
+
|x| x.gsub(/.*refs\/tags\//, '')
|
117
|
+
}
|
118
|
+
local_tags =runGit("tag -l | egrep '^v[0-9.]*$'").split("\n")
|
119
|
+
|
120
|
+
new_tags = local_tags - remote_tags
|
121
|
+
if new_tags.empty? then
|
122
|
+
puts "All tags are already submitted."
|
123
|
+
# return
|
124
|
+
end
|
125
|
+
|
126
|
+
puts "This will officially release these tags: #{new_tags.join(", ")}"
|
127
|
+
rep = GitMaintain::confirm("release them")
|
128
|
+
if rep != 'y' then
|
129
|
+
raise "Aborting.."
|
130
|
+
end
|
131
|
+
|
132
|
+
mail_path=`mktemp`.chomp()
|
133
|
+
mail = File.open(mail_path, "w+")
|
134
|
+
mail.puts "From " + runGit("rev-parse HEAD") + " " + `date`.chomp()
|
135
|
+
mail.puts "From: " + runGit("config user.name") +
|
136
|
+
" <" + runGit("config user.email") +">"
|
137
|
+
mail.puts "To: " + runGit("config patch.target")
|
138
|
+
mail.puts "Date: " + `date -R`.chomp()
|
139
|
+
mail.puts "Subject: [ANNOUNCE] " + File.basename(@path) + " " +
|
140
|
+
(new_tags.length > 1 ?
|
141
|
+
(new_tags[0 .. -2].join(", ") + " and " + new_tags[-1]) :
|
142
|
+
new_tags.join(" ")) +
|
143
|
+
" has been tagged/released"
|
144
|
+
mail.puts ""
|
145
|
+
mail.puts "Here's the information from the tags:"
|
146
|
+
new_tags.sort().each(){|tag|
|
147
|
+
mail.puts `git show #{tag} --no-decorate -q | awk '!p;/^-----END PGP SIGNATURE-----/{p=1}'`
|
148
|
+
mail.puts ""
|
149
|
+
}
|
150
|
+
mail.puts "It's available at the normal places:"
|
151
|
+
mail.puts ""
|
152
|
+
mail.puts "git://github.com/#{@remote_stable}"
|
153
|
+
mail.puts "https://github.com/#{@remote_stable}/releases"
|
154
|
+
mail.close()
|
155
|
+
|
156
|
+
puts runGitImap("< #{mail_path}; rm -f #{mail_path}")
|
157
|
+
|
158
|
+
puts "Last chance to cancel before submitting"
|
159
|
+
rep= GitMaintain::confirm("submit these releases")
|
160
|
+
if rep != 'y' then
|
161
|
+
raise "Aborting.."
|
162
|
+
end
|
163
|
+
puts `#{SUBMIT_BINARY}`
|
164
|
+
end
|
165
|
+
def findStableBase(branch)
|
166
|
+
@stable_base_patterns.each(){|pattern, base|
|
167
|
+
return base if branch =~ /#{pattern}\//
|
168
|
+
}
|
169
|
+
raise("Could not a find a stable base for branch #{branch}")
|
170
|
+
end
|
171
|
+
|
172
|
+
def list_branches(opts)
|
173
|
+
puts getStableList(opts[:br_suff])
|
174
|
+
end
|
175
|
+
def list_suffixes(opts)
|
176
|
+
puts getSuffixList()
|
177
|
+
end
|
178
|
+
def submit_release(opts)
|
179
|
+
submitReleases()
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
data/lib/travis.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
module GitMaintain
|
2
|
+
class TravisChecker
|
3
|
+
TRAVIS_URL='https://api.travis-ci.org/'
|
4
|
+
|
5
|
+
def self.load(repo)
|
6
|
+
repo_name = File.basename(repo.path)
|
7
|
+
return GitMaintain::loadClass(TravisChecker, repo_name, repo)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(repo)
|
11
|
+
GitMaintain::checkDirectConstructor(self.class)
|
12
|
+
|
13
|
+
@repo = repo
|
14
|
+
@cachedJson={}
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def fetch(uri_str, limit = 10)
|
19
|
+
# You should choose a better exception.
|
20
|
+
raise ArgumentError, 'too many HTTP redirects' if limit == 0
|
21
|
+
|
22
|
+
response = Net::HTTP.get_response(URI(uri_str))
|
23
|
+
|
24
|
+
case response
|
25
|
+
when Net::HTTPSuccess then
|
26
|
+
response
|
27
|
+
when Net::HTTPRedirection then
|
28
|
+
location = response['location']
|
29
|
+
fetch(location, limit - 1)
|
30
|
+
else
|
31
|
+
response.value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
def getJson(query_label, query, json=true)
|
35
|
+
return @cachedJson[query_label] if @cachedJson[query_label] != nil
|
36
|
+
url = TRAVIS_URL + query
|
37
|
+
uri = URI(url)
|
38
|
+
puts "# Querying travis..."
|
39
|
+
puts "# #{url}" if ENV["DEBUG_TRAVIS"].to_s() != ""
|
40
|
+
response = fetch(uri)
|
41
|
+
raise("Travis request failed '#{url}'") if response.code.to_s() != '200'
|
42
|
+
|
43
|
+
if json == true
|
44
|
+
@cachedJson[query_label] = JSON.parse(response.body)
|
45
|
+
else
|
46
|
+
@cachedJson[query_label] = response.body
|
47
|
+
end
|
48
|
+
return @cachedJson[query_label]
|
49
|
+
end
|
50
|
+
def getState(sha1, resp)
|
51
|
+
br = findBranch(sha1, resp)
|
52
|
+
return "not found" if br == nil
|
53
|
+
|
54
|
+
return br["state"]
|
55
|
+
end
|
56
|
+
def getLog(sha1, resp)
|
57
|
+
br = findBranch(sha1, resp)
|
58
|
+
raise("Travis build not found") if br == nil
|
59
|
+
job_id = br["job_ids"].last().to_s()
|
60
|
+
return getJson("log_" + job_id, 'jobs/' + job_id + '/log', false)
|
61
|
+
end
|
62
|
+
def checkState(sha1, resp)
|
63
|
+
return getState(sha1, resp) == "passed"
|
64
|
+
end
|
65
|
+
|
66
|
+
def getBrValidJson()
|
67
|
+
return getJson(:br_valid, 'repos/' + @repo.remote_valid + '/branches')
|
68
|
+
end
|
69
|
+
def getBrStableJson()
|
70
|
+
return getJson(:br_stable, 'repos/' + @repo.remote_stable + '/branches')
|
71
|
+
end
|
72
|
+
def findBranch(sha1, resp)
|
73
|
+
puts "# Looking for build for #{sha1}" if ENV["DEBUG_TRAVIS"].to_s() != ""
|
74
|
+
resp["branches"].each(){|br|
|
75
|
+
commit=resp["commits"].select(){|e| e["id"] == br["commit_id"]}.first()
|
76
|
+
raise("Incomplete JSON received from Travis") if commit == nil
|
77
|
+
puts "# Found entry for sha #{commit["sha"]}" if ENV["DEBUG_TRAVIS"].to_s() != ""
|
78
|
+
next if commit["sha"] != sha1
|
79
|
+
return br
|
80
|
+
}
|
81
|
+
return nil
|
82
|
+
end
|
83
|
+
|
84
|
+
public
|
85
|
+
def getValidState(sha1)
|
86
|
+
return getState(sha1, getBrValidJson())
|
87
|
+
end
|
88
|
+
def checkValidState(sha1)
|
89
|
+
return checkState(sha1, getBrValidJson())
|
90
|
+
end
|
91
|
+
def getValidLog(sha1)
|
92
|
+
return getLog(sha1, getBrValidJson())
|
93
|
+
end
|
94
|
+
|
95
|
+
def getStableState(sha1)
|
96
|
+
return getState(sha1, getBrStableJson())
|
97
|
+
end
|
98
|
+
def checkStableState(sha1)
|
99
|
+
return checkState(sha1, getBrStableJson())
|
100
|
+
end
|
101
|
+
def getStableLog(sha1)
|
102
|
+
return getLog(sha1, getBrStableJson())
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: git-maintain
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nicolas Morey-Chaisemartin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-07-13 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |-
|
14
|
+
Be lazy and let git-maintain do all the heavy lifting for maintaining stable branches.
|
15
|
+
Leaves you only with the essential: reviewing the selected patches and decide where they should go.
|
16
|
+
email: nmoreychaisemartin@suse.de
|
17
|
+
executables:
|
18
|
+
- git-maintain
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- LICENSE
|
23
|
+
- README.md
|
24
|
+
- bin/git-maintain
|
25
|
+
- git-maintain-completion.sh
|
26
|
+
- lib/addons/RDMACore.rb
|
27
|
+
- lib/branch.rb
|
28
|
+
- lib/common.rb
|
29
|
+
- lib/repo.rb
|
30
|
+
- lib/travis.rb
|
31
|
+
homepage: https://github.com/nmorey/git-maintain
|
32
|
+
licenses:
|
33
|
+
- MIT
|
34
|
+
metadata: {}
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project:
|
51
|
+
rubygems_version: 2.7.3
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: Your ultimate script for maintaining stable branches.
|
55
|
+
test_files: []
|