-
Notifications
You must be signed in to change notification settings - Fork 0
/
git-manage-mine
executable file
·277 lines (220 loc) · 8.89 KB
/
git-manage-mine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# Manage repos that belong to my user.
#
# Looks for your system user name and git email username in remotes and ensures that
# the remote name is 'mine'. For any repo with a remote 'mine', syncs local
# input branch with mine (ex. master and mine/master).
import argparse
import os
import sys
import re
import subprocess
import pprint
pprint = pprint.PrettyPrinter().pprint
DEBUG_DISABLE_EDIT = False
#DEBUG_DISABLE_EDIT = True
# Functions {{{1
def _git(*git_args):
"""Execute git command and return a list of lines of output.
_git(list(str)) -> list(str)
Prints any errors to stderr.
"""
cmd = ["git"] + list(git_args)
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True, # treat as text
)
out, err = p.communicate()
if err:
print(err, file=sys.stderr)
return out.strip().split('\n')
git_query = _git
git_edit = _git
if DEBUG_DISABLE_EDIT:
git_edit = print
def git_graph(refs):
return git_query("log", "--graph", "--oneline", "--abbrev-commit", refs)
def get_history_ahead(branch):
return git_graph("mine/{br}..{br}".format(br=branch))
def get_history_behind(branch):
return git_graph("{br}..mine/{br}".format(br=branch))
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("branch", type=str, help="Branch to operate on.")
# When called from `git submodule foreach`, $name is provided, but also support
# calling directly and use generic "Repo".
p.add_argument("--name", type=str, default="Repo", help="Name of the module we're operating on (for output).")
p.add_argument("--quiet", default=False, action="store_true", help="Enable minimal output.")
p.add_argument("--yes-all", default=False, action="store_true", help="Assume yes for all prompts.")
p.add_argument("--yes-rename", default=False, action="store_true", help="Assume yes for rename prompts.")
p.add_argument("--yes-ff", default=False, action="store_true", help="Assume yes for fast-fwd prompts.")
args = p.parse_args()
if args.yes_all:
args.yes_rename = True
args.yes_ff = True
return args
filter_regex = re.compile('fetch\)$')
def find_remotes(user_regex): #{{{2
"""
find_remotes(re) -> list(str)
>>> import re
>>> user_regex = re.compile("hi|hello")
>>> matches = [r for r in ("hello world", "that guy says hi") if user_regex.match(r)]
>>> for m in matches: print(m)
hello world
>>> searches = [r for r in ("hello world", "that guy says hi") if user_regex.search(r)]
>>> for m in searches: print(m)
hello world
that guy says hi
"""
# Find remotes we're interested in looking at. I don't see a reason to look
# at fetch and push separately (since they're always the same for me).
remotes = git_query("remote", "-v")
remotes = [r for r in remotes if filter_regex.search(r)]
remotes = [r for r in remotes if user_regex.search(r)]
return remotes
def prettify_remotes(remotes):
return "\n".join(remotes)
def prettify_commits(commits):
return "\n".join(commits)
def query_yes_no(question):
"""Ask a yes/no question via raw_input() and return their answer.
query_yes_no(str) -> bool
"question" is a string that is presented to the user.
The "answer" return value is True for "yes" or False for "no".
via http://stackoverflow.com/questions/3041986/python-command-line-yes-no-input
"""
# If there's no tty to give us input, then assume no unless we're debugging
# and destructive actions are disabled.
if not sys.stdin.isatty():
print(question +'Assume '+ str(DEBUG_DISABLE_EDIT))
return DEBUG_DISABLE_EDIT
valid = {"yes": True, "y": True,
"no": False, "n": False
}
prompt = " [y/n/q] "
while True:
sys.stdout.write(question + prompt)
choice = input().lower()
if choice in valid:
return valid[choice]
if choice == 'q':
sys.exit()
else:
sys.stdout.write("Please respond with 'yes' or 'no' "
"(or 'y' or 'n').\n")
def build_username_regex():
"""Collect names to search for.
Use same name associated with git config.
"""
names = [os.environ.get(var) for var in ('USER', 'USERNAME')]
git_user_guess = git_query("config", "user.email")
try:
# Assume only one user.
git_user_guess = git_user_guess[0]
# Pull out username from email address.
at_sign = git_user_guess.index('@')
names.append(git_user_guess[:at_sign])
except IndexError:
pass
except ValueError:
pass
user_query = "|".join([n for n in names if n])
if user_query:
return re.compile(user_query)
return None
# Inputs {{{1
# Parse. {{{2
args = parse_args()
# Implementation {{{1
user_regex = build_username_regex()
if not user_regex:
print("Error: Don't know user to search for.")
sys.exit(-1)
# Process remotes. {{{2
my_remotes = find_remotes(user_regex)
# Have any remotes for my user?
if my_remotes:
remote_regex = re.compile('(\w\+)\t')
mine_regex = re.compile('(mine)\t')
# Is there no remote named mine?
mine_matches = [mine_regex.match(remote) for remote in my_remotes]
if not any(mine_matches):
remote_names = [remote.split('\t') for remote in my_remotes]
remote_names = [r[0] for r in remote_names if r]
if not remote_names:
print("Not really valid.")
sys.exit()
if len(remote_names) > 1:
print("Multiple possible remote names:")
print(prettify_remotes(my_remotes))
origin = remote_names[0]
print("{name} has my name but remote '{origin}' isn't mine:".format(name=args.name, origin=origin))
print(prettify_remotes(find_remotes(user_regex)))
print()
if not args.yes_rename and not query_yes_no("Rename remote '{origin}' to mine?".format(origin=origin)):
sys.exit()
git_edit("remote", "rename", origin, "mine")
# Renamed remote, refresh list.
my_remotes = find_remotes(user_regex)
print()
# Is there still no remote named mine?
mine_matches = [mine_regex.match(remote) for remote in my_remotes]
if not any(mine_matches):
print("Failed to find remote 'mine':")
print(prettify_remotes(find_remotes(user_regex)))
sys.exit(-1)
# We don't fetch before doing the following work because we presumably
# already fetched in git-sub-update or git pull.
git_edit("use-branch-if-already-there", args.branch)
# Are there *any* commits between HEAD and destination branch?
delta_cmd = "HEAD...{br}".format(br=args.branch)
delta = git_graph(delta_cmd)
if any(delta):
print("Error: HEAD is not at {br}!\n Commits uncommon in {commits}:".format(br=args.branch, commits=delta_cmd))
print(prettify_commits(delta))
print()
delta_cmd = "HEAD...mine/{br}".format(br=args.branch)
delta = git_graph(delta_cmd)
if any(delta):
# HEAD is somewhere else. Abort.
sys.exit(-1)
else:
# Our local master is behind.
#
# See if branch is behind HEAD (has nothing extra and there are new
# commits).
delta_cmd = "HEAD..{br}".format(br=args.branch)
delta = git_graph(delta_cmd)
something_left_behind = any(delta)
delta_cmd = "{br}..HEAD".format(br=args.branch)
delta = git_graph(delta_cmd)
if not something_left_behind and any(delta):
print("{br} is behind HEAD but HEAD is at mine/{br}.".format(br=args.branch))
print(" Commits between {commits}:".format(br=args.branch, commits=delta_cmd))
print(prettify_commits(delta))
print()
if query_yes_no("Force {br} forward to HEAD?".format(br=args.branch)):
git_edit("branch", "-f", args.branch, "HEAD")
git_edit("use-branch-if-already-there", args.branch)
ahead = get_history_ahead(args.branch)
behind = get_history_behind(args.branch)
if any(ahead):
print("{name} is mine, but not pushed.".format(name=args.name))
print(prettify_commits(ahead))
print()
if query_yes_no("Push {br} to mine?".format(br=args.branch)):
git_edit("push", "mine", "{br}".format(br=args.branch))
elif any(behind):
print("{name} is mine, but behind.".format(name=args.name))
print(prettify_commits(behind))
print()
if args.yes_ff or query_yes_no("Fast-forward to mine/{br}?".format(br=args.branch)):
git_edit("merge", "--ff-only", "mine/{br}".format(br=args.branch))
elif not args.quiet:
print("{name} is mine and wonderful.".format(name=args.name))
prettify_remotes(find_remotes(user_regex))
print()