-
-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add setup-fork
command, reimplement clone
command as its child
#293
base: v2.x.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -782,24 +782,21 @@ class SetupCmd (object): | |||||
', '.join(users)) | ||||||
return users[0].encode('UTF8') | ||||||
|
||||||
# `git hub setup-fork` command implementation | ||||||
class SetupForkCmd (object): | ||||||
|
||||||
# `git hub clone` command implementation | ||||||
class CloneCmd (object): | ||||||
|
||||||
cmd_name = "setup-fork" | ||||||
cmd_required_config = ['username', 'oauthtoken'] | ||||||
cmd_help = 'clone a GitHub repository (and fork as needed)' | ||||||
cmd_usage = '%(prog)s [OPTIONS] [GIT CLONE OPTIONS] REPO [DEST]' | ||||||
cmd_help = 'fork a GitHub repository' | ||||||
cmd_usage = '%(prog)s [OPTIONS] [REPO]' | ||||||
|
||||||
@classmethod | ||||||
def setup_parser(cls, parser): | ||||||
parser.add_argument('repository', metavar='REPO', | ||||||
parser.add_argument('repository', metavar='REPO', nargs='?', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The help here should probably mention what happens if this argument is omitted. |
||||||
help="name of the repository to fork; in " | ||||||
"<owner>/<project> format is the upstream repository, " | ||||||
"if only <project> is specified, the <owner> part is " | ||||||
"taken from hub.username") | ||||||
parser.add_argument('dest', metavar='DEST', nargs='?', | ||||||
help="destination directory where to put the new " | ||||||
"cloned repository") | ||||||
parser.add_argument('-U', '--upstreamremote', metavar='NAME', | ||||||
default=config.upstreamremote, | ||||||
help="use NAME as the upstream remote repository name " | ||||||
|
@@ -816,61 +813,113 @@ class CloneCmd (object): | |||||
parser.add_argument('--no-triangular', action="store_false", | ||||||
dest='triangular', | ||||||
help="do not use Git 'triangular workflow' setup") | ||||||
return True # we need to get unknown arguments | ||||||
|
||||||
@classmethod | ||||||
def run(cls, parser, args): | ||||||
(urltype, proj) = cls.parse_repo(args.repository) | ||||||
(repo, upstream, forked) = cls.setup_repo(proj) | ||||||
dest = args.dest or repo['name'] | ||||||
if args.repository is not None: | ||||||
(urltype, proj) = cls.parse_repo(args.repository) | ||||||
else: | ||||||
git_remotes = git('remote', 'show', '-n').split('\n') | ||||||
remote = args.upstreamremote | ||||||
if remote not in git_remotes: | ||||||
die("No REPO specified, nor does `{}` remote exist", remote) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This will use the parse to report the usage too. |
||||||
git_url = git('remote', 'get-url', '--', remote) | ||||||
(urltype, proj) = cls.parse_repo(git_url) | ||||||
(repo, upstream, forked) = cls.maybe_fork(proj) | ||||||
cls.setup_dest(parser, args, urltype, proj, repo, upstream, forked, None) | ||||||
|
||||||
@classmethod | ||||||
def setup_dest(cls, parser, args, urltype, proj, repo, upstream, forked, dest): | ||||||
personal = False | ||||||
if upstream is None: | ||||||
if args.upstreamremote != args.forkremote: | ||||||
die('You are setting up with a personal repository as upstream, ' | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
'`--upstreamremote` and `--forkremote` must match') | ||||||
upstream = proj | ||||||
personal = True | ||||||
triangular = cls.check_triangular(args.triangular | ||||||
if args.triangular is not None else config.triangular) | ||||||
if triangular and not upstream: | ||||||
parser.error("Can't use triangular workflow without " | ||||||
"an upstream repo") | ||||||
url = repo['parent'][urltype] if triangular else repo[urltype] | ||||||
if personal and triangular: | ||||||
warnf('You are setting up with a personal repository as upstream, ' | ||||||
'forcing `--no-triangular` mode') | ||||||
triangular = False | ||||||
if triangular: | ||||||
url = repo['parent'][urltype] | ||||||
remote = args.upstreamremote | ||||||
else: | ||||||
url = repo[urltype] | ||||||
remote = args.forkremote | ||||||
validate_url(url, urltype) | ||||||
remote = args.upstreamremote if triangular else args.forkremote | ||||||
# It's complicated to allow the user to use the --origin option, there | ||||||
# is enough complexity with --upstreamremote and --forkremote, so ask | ||||||
# the user to use those instead | ||||||
for a in args.unknown_args: | ||||||
if a in ('-o', '--origin') or a.startswith('-o', '--origin='): | ||||||
die("Please use --forkremote or --upstreamremote to name your " | ||||||
"remotes instead of using Git's `{}` option!", a) | ||||||
# If we just forked the repo, GitHub might still be doing the actual | ||||||
# fork, so cloning could fail temporarily. See | ||||||
# https://github.com/sociomantic-tsunami/git-hub/issues/214 | ||||||
cls.git_retry_if(not args.triangular and forked, | ||||||
'clone', | ||||||
args.unknown_args + ['--origin', remote, '--', url, dest], | ||||||
'Cloning {} to {}'.format(url, dest)) | ||||||
if not upstream: | ||||||
# Not a forked repository, nothing else to do | ||||||
return | ||||||
# Complete the repository setup | ||||||
os.chdir(dest) | ||||||
fetchremote = args.forkremote if triangular else args.upstreamremote | ||||||
remote_url = repo['parent'][urltype] | ||||||
if dest is not None: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code will only ever used when invoking this function from the If not, wouldn't it make sense to rename In general I'm not a fan of such a large method, so If you find any way to factor out some parts, it would be greatly appreciated. Maybe just remove some small parts as independent methods and then write slightly longer |
||||||
# Cloning to dest | ||||||
# | ||||||
# It's complicated to allow the user to use the --origin option, there | ||||||
# is enough complexity with --upstreamremote and --forkremote, so ask | ||||||
# the user to use those instead | ||||||
for a in args.unknown_args: | ||||||
if a in ('-o', '--origin') or a.startswith('-o', '--origin='): | ||||||
die("Please use --forkremote or --upstreamremote to name your " | ||||||
"remotes instead of using Git's `{}` option!", a) | ||||||
# If we just forked the repo, GitHub might still be doing the actual | ||||||
# fork, so cloning could fail temporarily. See | ||||||
# https://github.com/sociomantic-tsunami/git-hub/issues/214 | ||||||
cls.git_retry_if(not args.triangular and forked, | ||||||
'clone', | ||||||
args.unknown_args + ['--origin', remote, '--', url, dest], | ||||||
'Cloning {} to {}'.format(url, dest)) | ||||||
# Complete the repository setup | ||||||
os.chdir(dest) | ||||||
else: | ||||||
# We are inside working directory of a pre-cloned repository, just | ||||||
# setup a remote and fetch it | ||||||
added=cls.git_add_remote(remote, url) | ||||||
if added: | ||||||
cls.git_retry_if(not args.triangular and forked, | ||||||
'fetch', ['--', remote], | ||||||
'Fetching from {} ({})'.format(remote, url)) | ||||||
|
||||||
if triangular: | ||||||
fetchremote = args.forkremote | ||||||
remote_url = repo[urltype] | ||||||
git_config('remote.pushdefault', prefix='', value=fetchremote) | ||||||
git_config('upstreamremote', value=args.upstreamremote) | ||||||
git_config('forkremote', value=args.forkremote) | ||||||
else: | ||||||
fetchremote = args.upstreamremote | ||||||
remote_url = repo['parent'][urltype] if not personal else repo[urltype] | ||||||
validate_url(remote_url, urltype) | ||||||
added=cls.git_add_remote(fetchremote, remote_url) | ||||||
if added: | ||||||
# We also need to retry in here, although is less likely since we | ||||||
# already spent some time doing the previous clone | ||||||
cls.git_retry_if(args.triangular and forked, | ||||||
'fetch', ['--', fetchremote], | ||||||
'Fetching from {} ({})'.format(fetchremote, remote_url)) | ||||||
git_config('urltype', value=urltype) | ||||||
git_config('forkremote', value=args.forkremote) | ||||||
git_config('upstreamremote', value=args.upstreamremote) | ||||||
git_config('upstream', value=upstream) | ||||||
validate_url(remote_url, urltype) | ||||||
git('remote', 'add', '--', fetchremote, remote_url) | ||||||
# We also need to retry in here, although is less likely since we | ||||||
# already spent some time doing the previous clone | ||||||
cls.git_retry_if(args.triangular and forked, | ||||||
'fetch', ['--', fetchremote], | ||||||
'Fetching from {} ({})'.format(fetchremote, remote_url)) | ||||||
run_hookscript('postclone', env=dict( | ||||||
git_config('triangular', value='true' if triangular else 'false') | ||||||
run_hookscript('post-setup-fork', env=dict( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes a lot of sense, but is a bit unfortunate that this PR comes right after v2.1.0 was released, because it now constitute a breaking change :( (and we are quite strict about following semver). Making it a non-breaking change should be easy enough though. We can just deprecate postclone and accept both until the next major is released and we can get rid of the old name. So if you can accept postclone as a hook name too and emit a warning saying the name is deprecated and will be removed in the future, everything should be fine to put this change in v2.2.0. You can remove the old name from the documentation, don't worry about that. |
||||||
fetchremote=fetchremote, | ||||||
triangular=triangular, | ||||||
)) | ||||||
|
||||||
@classmethod | ||||||
def git_add_remote(cls, remote, url): | ||||||
git_remotes = git('remote', 'show', '-n').split('\n') | ||||||
if remote in git_remotes: | ||||||
git_url = git('remote', 'get-url', '--', remote) | ||||||
|
||||||
repo = cls.parse_repo(url) | ||||||
git_repo = cls.parse_repo(git_url) | ||||||
if repo[1] != git_repo[1]: | ||||||
die("Remote {} already exists and is set to {} instead of {}", remote, git_url, url) | ||||||
else: | ||||||
infof("Nothing to do, remote {} already exists and is already set to {}", remote, git_url) | ||||||
return False | ||||||
else: | ||||||
git('remote', 'add', '--', remote, url) | ||||||
return True | ||||||
|
||||||
@classmethod | ||||||
def git_retry_if(cls, condition, cmd, args, progress_msg): | ||||||
# If we are not retrying, just do it once | ||||||
|
@@ -924,18 +973,15 @@ class CloneCmd (object): | |||||
return (urltype, proj) | ||||||
|
||||||
@classmethod | ||||||
def setup_repo(cls, proj): | ||||||
def maybe_fork(cls, proj): | ||||||
forked = False | ||||||
# Own repo | ||||||
if proj.split('/')[0] == config.username: | ||||||
# Our own repository | ||||||
repo = req.get('/repos/' + proj) | ||||||
if repo['fork']: | ||||||
upstream = repo['parent']['full_name'] | ||||||
else: | ||||||
upstream = None | ||||||
warnf('Repository {} is not a fork, just ' | ||||||
'cloning, upstream will not be set', | ||||||
repo['full_name']) | ||||||
else: | ||||||
upstream = proj | ||||||
# Try to fork, if a fork already exists, we'll get the | ||||||
|
@@ -944,9 +990,9 @@ class CloneCmd (object): | |||||
# API docs, but it seems to work as of Sep 2016. | ||||||
# See https://github.com/sociomantic-tsunami/git-hub/pull/193 | ||||||
# for more details. | ||||||
infof('Checking for existing fork / forking...') | ||||||
infof('Checking for existing fork / forking {}...', upstream) | ||||||
repo = req.post('/repos/' + upstream + '/forks') | ||||||
infof('Fork at {}', repo['html_url']) | ||||||
infof('Fork exists / created at {}', repo['html_url']) | ||||||
forked = True | ||||||
return (repo, upstream, forked) | ||||||
|
||||||
|
@@ -974,6 +1020,48 @@ class CloneCmd (object): | |||||
return False | ||||||
return True | ||||||
|
||||||
# `git hub clone` command implementation | ||||||
class CloneCmd (SetupForkCmd): | ||||||
|
||||||
cmd_name = "clone" | ||||||
cmd_required_config = ['username', 'oauthtoken'] | ||||||
cmd_help = 'clone a GitHub repository (and fork as needed)' | ||||||
cmd_usage = '%(prog)s [OPTIONS] [GIT CLONE OPTIONS] REPO [DEST]' | ||||||
|
||||||
@classmethod | ||||||
def setup_parser(cls, parser): | ||||||
parser.add_argument('repository', metavar='REPO', | ||||||
help="name of the repository to fork; in " | ||||||
"<owner>/<project> format is the upstream repository, " | ||||||
"if only <project> is specified, the <owner> part is " | ||||||
"taken from hub.username") | ||||||
parser.add_argument('dest', metavar='DEST', nargs='?', | ||||||
help="destination directory where to put the new " | ||||||
"cloned repository") | ||||||
parser.add_argument('-U', '--upstreamremote', metavar='NAME', | ||||||
default=config.upstreamremote, | ||||||
help="use NAME as the upstream remote repository name " | ||||||
"instead of the default '{}'".format(config.upstreamremote)) | ||||||
parser.add_argument('-F', '--forkremote', metavar='NAME', | ||||||
default=config.forkremote, | ||||||
help="use NAME as the fork remote repository name " | ||||||
"instead of the default '{}'".format(config.forkremote)) | ||||||
parser.add_argument('-t', '--triangular', action="store_true", | ||||||
default=None, | ||||||
help="use Git 'triangular workflow' setup, so you can " | ||||||
"push by default to your fork but pull by default " | ||||||
"from 'upstream'") | ||||||
parser.add_argument('--no-triangular', action="store_false", | ||||||
dest='triangular', | ||||||
help="do not use Git 'triangular workflow' setup") | ||||||
return True # we need to get unknown arguments | ||||||
|
||||||
@classmethod | ||||||
def run(cls, parser, args): | ||||||
(urltype, proj) = cls.parse_repo(args.repository) | ||||||
(repo, upstream, forked) = cls.maybe_fork(proj) | ||||||
dest = args.dest or repo['name'] | ||||||
cls.setup_dest(parser, args, urltype, proj, repo, upstream, forked, dest) | ||||||
|
||||||
# Utility class that groups common functionality used by the multiple | ||||||
# `git hub issue` (and `git hub pull`) subcommands. | ||||||
|
@@ -2205,6 +2293,7 @@ class HubCmd (CmdGroup): | |||||
cmd_title = "subcommands" | ||||||
cmd_help = "git command line interface to GitHub" | ||||||
SetupCmd = SetupCmd | ||||||
SetupForkCmd = SetupForkCmd | ||||||
CloneCmd = CloneCmd | ||||||
IssueCmd = IssueCmd | ||||||
PullCmd = PullCmd | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still a maybe right? If I understood correctly, if the fork already exists it will just set up the local repo. If so, it might be better to make this more explicit here.