1# -*- coding: utf-8 -*-
2from __future__ import print_function
3
4COPYRIGHT = """\
5Copyright (C) 2011-2012 OpenStack LLC.
6
7Licensed under the Apache License, Version 2.0 (the "License");
8you may not use this file except in compliance with the License.
9You may obtain a copy of the License at
10
11   http://www.apache.org/licenses/LICENSE-2.0
12
13Unless required by applicable law or agreed to in writing, software
14distributed under the License is distributed on an "AS IS" BASIS,
15WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16implied.
17
18See the License for the specific language governing permissions and
19limitations under the License."""
20
21import argparse
22import datetime
23import getpass
24import json
25import os
26import re
27import shlex
28import six
29import subprocess
30import sys
31import textwrap
32
33import pkg_resources
34import requests
35from six.moves import configparser
36from six.moves import input as do_input
37from six.moves.urllib.parse import urlencode
38from six.moves.urllib.parse import urljoin
39from six.moves.urllib.parse import urlparse
40
41
42VERBOSE = False
43UPDATE = False
44LOCAL_MODE = 'GITREVIEW_LOCAL_MODE' in os.environ
45CONFIGDIR = os.path.expanduser("~/.config/git-review")
46GLOBAL_CONFIG = "/etc/git-review/git-review.conf"
47USER_CONFIG = os.path.join(CONFIGDIR, "git-review.conf")
48DEFAULTS = dict(scheme='ssh', hostname=False, port=None, project=False,
49                branch='master', remote="gerrit", rebase="1",
50                track="0", usepushurl="0")
51
52_branch_name = None
53_has_color = None
54_use_color = None
55_orig_head = None
56_rewrites = None
57_rewrites_push = None
58
59
60class colors(object):
61    yellow = '\033[33m'
62    green = '\033[92m'
63    reset = '\033[0m'
64    blue = '\033[36m'
65
66
67class GitReviewException(Exception):
68    EXIT_CODE = 1
69
70
71class CommandFailed(GitReviewException):
72
73    def __init__(self, *args):
74        Exception.__init__(self, *args)
75        (self.rc, self.output, self.argv, self.envp) = args
76        self.quickmsg = dict([
77            ("argv", " ".join(self.argv)),
78            ("rc", self.rc),
79            ("output", self.output)])
80
81    def __str__(self):
82        return self.__doc__ + """
83The following command failed with exit code %(rc)d
84    "%(argv)s"
85-----------------------
86%(output)s
87-----------------------""" % self.quickmsg
88
89
90class ChangeSetException(GitReviewException):
91
92    def __init__(self, e):
93        GitReviewException.__init__(self)
94        self.e = str(e)
95
96    def __str__(self):
97        return self.__doc__ % self.e
98
99
100def printwrap(unwrapped):
101    print('\n'.join(textwrap.wrap(unwrapped)))
102
103
104def warn(warning):
105    printwrap("WARNING: %s" % warning)
106
107
108def parse_review_number(review):
109    parts = review.split(',')
110    if len(parts) < 2:
111        parts.append(None)
112    return parts
113
114
115def build_review_number(review, patchset):
116    if patchset is not None:
117        return '%s,%s' % (review, patchset)
118    return review
119
120
121def run_command_status(*argv, **kwargs):
122    if VERBOSE:
123        print(datetime.datetime.now(), "Running:", " ".join(argv))
124    if len(argv) == 1:
125        # for python2 compatibility with shlex
126        if sys.version_info < (3,) and isinstance(argv[0], six.text_type):
127            argv = shlex.split(argv[0].encode('utf-8'))
128        else:
129            argv = shlex.split(str(argv[0]))
130    stdin = kwargs.pop('stdin', None)
131    newenv = os.environ.copy()
132    newenv['LANG'] = 'C'
133    newenv['LANGUAGE'] = 'C'
134    newenv.update(kwargs)
135    p = subprocess.Popen(argv,
136                         stdin=subprocess.PIPE if stdin else None,
137                         stdout=subprocess.PIPE,
138                         stderr=subprocess.STDOUT,
139                         env=newenv, universal_newlines=True)
140    (out, nothing) = p.communicate(stdin)
141    return (p.returncode, out.strip())
142
143
144def run_command(*argv, **kwargs):
145    (rc, output) = run_command_status(*argv, **kwargs)
146    return output
147
148
149def run_command_exc(klazz, *argv, **env):
150    """Run command *argv, on failure raise klazz
151
152    klazz should be derived from CommandFailed
153    """
154    (rc, output) = run_command_status(*argv, **env)
155    if rc:
156        raise klazz(rc, output, argv, env)
157    return output
158
159
160def git_credentials(url):
161    """Return credentials using git credential or None."""
162    cmd = 'git', 'credential', 'fill'
163    stdin = 'url=%s' % url
164    rc, out = run_command_status(*cmd, stdin=stdin.encode('utf-8'))
165    if rc:
166        return None
167    data = dict(l.split('=', 1) for l in out.splitlines())
168    return data['username'], data['password']
169
170
171def http_code_2_return_code(code):
172    """Tranform http status code to system return code."""
173    return (code - 301) % 255 + 1
174
175
176def run_http_exc(klazz, url, **env):
177    """Run http GET request url, on failure raise klazz
178
179    klazz should be derived from CommandFailed
180    """
181    if url.startswith("https://") and "verify" not in env:
182        if "GIT_SSL_NO_VERIFY" in os.environ:
183            env["verify"] = False
184        else:
185            verify = git_config_get_value("http", "sslVerify", as_bool=True)
186            env["verify"] = verify != 'false'
187
188    try:
189        res = requests.get(url, **env)
190        if res.status_code == 401:
191            creds = git_credentials(url)
192            if creds:
193                env['auth'] = creds
194                res = requests.get(url, **env)
195    except klazz:
196        raise
197    except Exception as err:
198        raise klazz(255, str(err), ('GET', url), env)
199    if not 200 <= res.status_code < 300:
200        raise klazz(http_code_2_return_code(res.status_code),
201                    res.text, ('GET', url), env)
202    return res
203
204
205def get_version():
206    requirement = pkg_resources.Requirement.parse('git-review')
207    provider = pkg_resources.get_provider(requirement)
208    return provider.version
209
210
211def git_directories():
212    """Determine (absolute git work directory path, .git subdirectory path)."""
213    cmd = ("git", "rev-parse", "--show-toplevel", "--git-dir")
214    out = run_command_exc(GitDirectoriesException, *cmd)
215    try:
216        return out.splitlines()
217    except ValueError:
218        raise GitDirectoriesException(0, out, cmd, {})
219
220
221class GitDirectoriesException(CommandFailed):
222    "Cannot determine where .git directory is."
223    EXIT_CODE = 70
224
225
226class CustomScriptException(CommandFailed):
227    """Custom script execution failed."""
228    EXIT_CODE = 71
229
230
231def run_custom_script(action):
232    """Get status and output of .git/hooks/$action-review or/and
233    ~/.config/hooks/$action-review if existing.
234    """
235    returns = []
236    script_file = "%s-review" % (action)
237    (top_dir, git_dir) = git_directories()
238    paths = [os.path.join(CONFIGDIR, "hooks", script_file),
239             os.path.join(git_dir, "hooks", script_file)]
240    for fpath in paths:
241        if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
242            status, output = run_command_status(fpath)
243            returns.append((status, output, fpath))
244
245    for (status, output, path) in returns:
246        if status:
247            raise CustomScriptException(status, output, [path], {})
248        elif output and VERBOSE:
249            print("script %s output is:" % (path))
250            print(output)
251
252
253def git_config_get_value(section, option, default=None, as_bool=False):
254    """Get config value for section/option."""
255    cmd = ["git", "config", "--get", "%s.%s" % (section, option)]
256    if as_bool:
257        cmd.insert(2, "--bool")
258    if LOCAL_MODE:
259        __, git_dir = git_directories()
260        cmd[2:2] = ['-f', os.path.join(git_dir, 'config')]
261    try:
262        result = run_command_exc(GitConfigException, *cmd).strip()
263        if VERBOSE:
264            print(datetime.datetime.now(), "... %s.%s = %s"
265                  % (section, option, result))
266        return result
267    except GitConfigException as exc:
268        if exc.rc == 1:
269            if VERBOSE and default is not None:
270                print(datetime.datetime.now(),
271                      "... nothing in git config, returning func parameter:",
272                      default)
273            return default
274        raise
275
276
277class Config(object):
278    """Expose as dictionary configuration options."""
279
280    def __init__(self, config_file=None):
281        self.config = DEFAULTS.copy()
282        filenames = [] if LOCAL_MODE else [GLOBAL_CONFIG, USER_CONFIG]
283        if config_file:
284            filenames.append(config_file)
285        for filename in filenames:
286            if os.path.exists(filename):
287                if filename != config_file:
288                    msg = ("Using global/system git-review config files (%s) "
289                           "is deprecated and will be removed in a future "
290                           "release")
291                    warn(msg % filename)
292                self.config.update(load_config_file(filename))
293
294    def __getitem__(self, key):
295        """Let 'git config --get' override every Config['key'] access"""
296        value = git_config_get_value('gitreview', key)
297        if value is None:
298            value = self.config[key]
299        # "--verbose" doesn't trace *early* invocations; for that you
300        # must change the value at the top of this file (*and* pass
301        # --verbose)
302        if VERBOSE:
303            print(datetime.datetime.now(),
304                  "Config['%s'] = %s " % (key, value))
305        return value
306
307
308class GitConfigException(CommandFailed):
309    """Git config value retrieval failed."""
310    EXIT_CODE = 128
311
312
313class CannotInstallHook(CommandFailed):
314    "Problems encountered installing commit-msg hook"
315    EXIT_CODE = 2
316
317
318def set_hooks_commit_msg(remote, target_file):
319    """Install the commit message hook if needed."""
320
321    # Create the hooks directory if it's not there already
322    hooks_dir = os.path.dirname(target_file)
323    if not os.path.isdir(hooks_dir):
324        os.mkdir(hooks_dir)
325
326    if not os.path.exists(target_file) or UPDATE:
327        remote_url = get_remote_url(remote)
328        if (remote_url.startswith('http://') or
329                remote_url.startswith('https://')):
330            hook_url = urljoin(remote_url, '/tools/hooks/commit-msg')
331            if VERBOSE:
332                print("Fetching commit hook from: %s" % hook_url)
333            res = run_http_exc(CannotInstallHook, hook_url, stream=True)
334            with open(target_file, 'wb') as f:
335                for x in res.iter_content(1024):
336                    f.write(x)
337        else:
338            (hostname, username, port, project_name) = \
339                parse_gerrit_ssh_params_from_git_url(remote_url)
340            if username:
341                userhost = "%s@%s" % (username, hostname)
342            else:
343                userhost = hostname
344            # OS independent target file
345            scp_target_file = target_file.replace(os.sep, "/")
346            cmd = ["scp", userhost + ":hooks/commit-msg", scp_target_file]
347            if port is not None:
348                cmd.insert(1, "-P%s" % port)
349
350            if VERBOSE:
351                hook_url = 'scp://%s%s/hooks/commit-msg' \
352                           % (userhost, (":%s" % port) if port else "")
353                print("Fetching commit hook from: %s" % hook_url)
354            run_command_exc(CannotInstallHook, *cmd)
355
356    if not os.access(target_file, os.X_OK):
357        os.chmod(target_file, os.path.stat.S_IREAD | os.path.stat.S_IEXEC)
358
359
360def test_remote_url(remote_url):
361    """Tests that a possible gerrit remote url works."""
362    status, description = run_command_status("git", "push", "--dry-run",
363                                             remote_url, "--all")
364    if status != 128:
365        if VERBOSE:
366            print("%s worked. Description: %s" % (remote_url, description))
367        return True
368    else:
369        print("%s did not work. Description: %s" % (remote_url, description))
370        return False
371
372
373def make_remote_url(scheme, username, hostname, port, project):
374    """Builds a gerrit remote URL."""
375    if port is None and scheme == 'ssh':
376        port = 29418
377    hostport = '%s:%s' % (hostname, port) if port else hostname
378    if username:
379        return "%s://%s@%s/%s" % (scheme, username, hostport, project)
380    else:
381        return "%s://%s/%s" % (scheme, hostport, project)
382
383
384def add_remote(scheme, hostname, port, project, remote, usepushurl):
385    """Adds a gerrit remote."""
386    asked_for_username = False
387
388    username = git_config_get_value("gitreview", "username")
389    if not username:
390        username = getpass.getuser()
391
392    remote_url = make_remote_url(scheme, username, hostname, port, project)
393    if VERBOSE:
394        print("No remote set, testing %s" % remote_url)
395    if not test_remote_url(remote_url):
396        print("Could not connect to gerrit.")
397        username = do_input("Enter your gerrit username: ")
398        remote_url = make_remote_url(scheme, username, hostname, port, project)
399        print("Trying again with %s" % remote_url)
400        if not test_remote_url(remote_url):
401            raise GerritConnectionException(
402                "Could not connect to gerrit at %s" % remote_url
403            )
404        asked_for_username = True
405
406    if usepushurl:
407        cmd = "git remote set-url --push %s %s" % (remote, remote_url)
408        print("Adding a git push url to '%s' that maps to:" % remote)
409    else:
410        cmd = "git remote add -f %s %s" % (remote, remote_url)
411        print("Creating a git remote called '%s' that maps to:" % remote)
412    print("\t%s" % remote_url)
413
414    (status, remote_output) = run_command_status(cmd)
415    if status:
416        raise CommandFailed(status, remote_output, cmd, {})
417
418    if asked_for_username:
419        print()
420        printwrap("This repository is now set up for use with git-review. "
421                  "You can set the default username for future repositories "
422                  "with:")
423        print('  git config --global --add gitreview.username "%s"' % username)
424        print()
425
426
427def populate_rewrites():
428    """Populate the global _rewrites and _rewrites_push maps based on the
429    output of "git-config".
430    """
431
432    cmd = ['git', 'config', '--list']
433    out = run_command_exc(CommandFailed, *cmd).strip()
434
435    global _rewrites, _rewrites_push
436    _rewrites = {}
437    _rewrites_push = {}
438
439    for entry in out.splitlines():
440        key, _, value = entry.partition('=')
441        key = key.lower()
442
443        if key.startswith('url.') and key.endswith('.insteadof'):
444            rewrite = key[len('url.'):-len('.insteadof')]
445            if rewrite:
446                _rewrites[value] = rewrite
447        elif key.startswith('url.') and key.endswith('.pushinsteadof'):
448            rewrite = key[len('url.'):-len('.pushinsteadof')]
449            if rewrite:
450                _rewrites_push[value] = rewrite
451
452
453def alias_url(url, rewrite_push):
454    """Expand a remote URL. Use the global map _rewrites to replace the
455    longest match with its equivalent. If rewrite_push is True, try
456    _rewrites_push before _rewrites.
457    """
458
459    if _rewrites is None:
460        populate_rewrites()
461
462    if rewrite_push:
463        maps = [_rewrites_push, _rewrites]
464    else:
465        maps = [_rewrites]
466
467    for rewrites in maps:
468        # If Git finds a pushInsteadOf alias, it uses that even if
469        # there is a longer insteadOf alias.
470        longest = None
471        for alias in rewrites:
472            if (url.startswith(alias)
473                    and (longest is None or len(longest) < len(alias))):
474                longest = alias
475
476        if longest:
477            return url.replace(longest, rewrites[longest])
478
479    return url
480
481
482def get_remote_url(remote):
483    """Retrieve the remote URL. Read the configuration to expand the URL of a
484    remote repository taking into account any "url.<base>.insteadOf" or
485    "url.<base>.pushInsteadOf" config setting.
486
487    TODO: Replace current code with something like "git ls-remote
488    --get-url" after Git grows a version of it that returns the push
489    URL rather than the fetch URL.
490    """
491
492    push_url = git_config_get_value('remote.%s' % remote, 'pushurl')
493    if push_url is not None:
494        # Git rewrites pushurl using insteadOf but not pushInsteadOf.
495        push_url = alias_url(push_url, False)
496    else:
497        url = git_config_get_value('remote.%s' % remote, 'url')
498        # Git rewrites url using pushInsteadOf or insteadOf.
499        push_url = alias_url(url, True)
500    if VERBOSE:
501        print("Found origin Push URL:", push_url)
502    return push_url
503
504
505def parse_gerrit_ssh_params_from_git_url(git_url):
506    """Parse a given Git "URL" into Gerrit parameters. Git "URLs" are either
507    real URLs or SCP-style addresses.
508    """
509
510    # The exact code for this in Git itself is a bit obtuse, so just do
511    # something sensible and pythonic here instead of copying the exact
512    # minutiae from Git.
513
514    # Handle real(ish) URLs
515    if "://" in git_url:
516        parsed_url = urlparse(git_url)
517        path = parsed_url.path
518
519        hostname = parsed_url.netloc
520        username = None
521        port = parsed_url.port
522
523        # Workaround bug in urlparse on OSX
524        if parsed_url.scheme == "ssh" and parsed_url.path[:2] == "//":
525            hostname = parsed_url.path[2:].split("/")[0]
526
527        if "@" in hostname:
528            (username, _, hostname) = hostname.rpartition("@")
529        if ":" in hostname:
530            (hostname, port) = hostname.split(":")
531
532        if port is not None:
533            port = str(port)
534
535    # Handle SCP-style addresses
536    else:
537        username = None
538        port = None
539        (hostname, path) = git_url.split(":", 1)
540        if "@" in hostname:
541            (username, hostname) = hostname.split("@", 1)
542
543    # Strip leading slash and trailing .git from the path to form the project
544    # name.
545    project_name = re.sub(r"^/|(\.git$)", "", path)
546
547    return (hostname, username, port, project_name)
548
549
550def query_reviews(remote_url, project=None, change=None,
551                  current_patch_set=True, exception=CommandFailed,
552                  parse_exc=Exception):
553    if remote_url.startswith('http://') or remote_url.startswith('https://'):
554        query = query_reviews_over_http
555    else:
556        query = query_reviews_over_ssh
557    return query(remote_url,
558                 project=project,
559                 change=change,
560                 current_patch_set=current_patch_set,
561                 exception=exception,
562                 parse_exc=parse_exc)
563
564
565def query_reviews_over_http(remote_url, project=None, change=None,
566                            current_patch_set=True, exception=CommandFailed,
567                            parse_exc=Exception):
568    if project:
569        # Remove any trailing .git suffixes for project to url comparison
570        clean_url = os.path.splitext(remote_url)[0]
571        clean_project = os.path.splitext(project)[0]
572        if clean_url.endswith(clean_project):
573            # Get the "root" url for gerrit by removing the project from the
574            # url. For example:
575            # https://example.com/foo/project.git gets truncated to
576            # https://example.com/foo/ regardless of whether or not none,
577            # either, or both of the remote_url or project strings end
578            # with .git.
579            remote_url = clean_url[:-len(clean_project)]
580    url = urljoin(remote_url, 'changes/')
581    if change:
582        if current_patch_set:
583            url += '?q=%s&o=CURRENT_REVISION' % change
584        else:
585            url += '?q=%s&o=ALL_REVISIONS' % change
586    else:
587        if project:
588            project_name = re.sub(r"^/|(\.git$)", "",
589                                  project)
590        else:
591            project_name = re.sub(r"^/|(\.git$)", "",
592                                  urlparse(remote_url).path)
593        params = urlencode({'q': 'project:%s status:open' % project_name})
594        url += '?' + params
595
596    if VERBOSE:
597        print("Query gerrit %s" % url)
598    request = run_http_exc(exception, url)
599    if VERBOSE:
600        print(request.text)
601    reviews = json.loads(request.text[4:])
602
603    # Reformat output to match ssh output
604    try:
605        for review in reviews:
606            review["number"] = str(review.pop("_number"))
607            if "revisions" not in review:
608                continue
609            patchsets = {}
610            for key, revision in review["revisions"].items():
611                fetch_value = list(revision["fetch"].values())[0]
612                patchset = {"number": str(revision["_number"]),
613                            "ref": fetch_value["ref"]}
614                patchsets[key] = patchset
615            review["patchSets"] = patchsets.values()
616            review["currentPatchSet"] = patchsets[review["current_revision"]]
617    except Exception as err:
618        raise parse_exc(err)
619
620    return reviews
621
622
623def query_reviews_over_ssh(remote_url, project=None, change=None,
624                           current_patch_set=True, exception=CommandFailed,
625                           parse_exc=Exception):
626    (hostname, username, port, project_name) = \
627        parse_gerrit_ssh_params_from_git_url(remote_url)
628
629    if change:
630        if current_patch_set:
631            query = "--current-patch-set change:%s" % change
632        else:
633            query = "--patch-sets change:%s" % change
634    else:
635        query = "project:%s status:open" % project_name
636
637    port_data = "p%s" % port if port is not None else ""
638    if username is None:
639        userhost = hostname
640    else:
641        userhost = "%s@%s" % (username, hostname)
642
643    if VERBOSE:
644        print("Query gerrit %s %s" % (remote_url, query))
645    output = run_command_exc(
646        exception,
647        "ssh", "-x" + port_data, userhost,
648        "gerrit", "query",
649        "--format=JSON %s" % query)
650    if VERBOSE:
651        print(output)
652
653    changes = []
654    try:
655        for line in output.split("\n"):
656            if line[0] == "{":
657                try:
658                    data = json.loads(line)
659                    if "type" not in data:
660                        changes.append(data)
661                except Exception:
662                    if VERBOSE:
663                        print(output)
664    except Exception as err:
665        raise parse_exc(err)
666    return changes
667
668
669def set_color_output(color="auto"):
670    global _use_color
671    if check_color_support():
672        if color == "auto":
673            check_use_color_output()
674        else:
675            _use_color = color == "always"
676
677
678def check_use_color_output():
679    global _use_color
680    if _use_color is None:
681        if check_color_support():
682            # we can support color, now check if we should use it
683            stdout = "true" if sys.stdout.isatty() else "false"
684            test_command = "git config --get-colorbool color.review " + stdout
685            color = run_command(test_command)
686            _use_color = color == "true"
687        else:
688            _use_color = False
689    return _use_color
690
691
692def check_color_support():
693    global _has_color
694    if _has_color is None:
695        test_command = "git log --color=never --oneline HEAD^1..HEAD"
696        (status, output) = run_command_status(test_command)
697        if status == 0:
698            _has_color = True
699        else:
700            _has_color = False
701    return _has_color
702
703
704def load_config_file(config_file):
705    """Load configuration options from a file."""
706    configParser = configparser.ConfigParser()
707    configParser.read(config_file)
708    options = {
709        'scheme': 'scheme',
710        'hostname': 'host',
711        'port': 'port',
712        'project': 'project',
713        'branch': 'defaultbranch',
714        'remote': 'defaultremote',
715        'rebase': 'defaultrebase',
716        'track': 'track',
717        'usepushurl': 'usepushurl',
718    }
719    config = {}
720    for config_key, option_name in options.items():
721        if configParser.has_option('gerrit', option_name):
722            config[config_key] = configParser.get('gerrit', option_name)
723    return config
724
725
726def update_remote(remote):
727    cmd = "git remote update %s" % remote
728    (status, output) = run_command_status(cmd)
729    if VERBOSE:
730        print(output)
731    if status != 0:
732        print("Problem running '%s'" % cmd)
733        if not VERBOSE:
734            print(output)
735        return False
736    return True
737
738
739def parse_tracking(ref=None):
740    """Return tracked (remote, branch) of current HEAD or other named
741       branch if tracking remote.
742    """
743    if ref is None:
744        ref = run_command_exc(
745            SymbolicRefFailed,
746            "git", "symbolic-ref", "-q", "HEAD")
747    tracked = run_command_exc(
748        ForEachRefFailed,
749        "git", "for-each-ref", "--format=%(upstream)", ref)
750
751    # Only on explicitly tracked remote branch do we diverge from default
752    if tracked and tracked.startswith('refs/remotes/'):
753        return tracked[13:].partition('/')[::2]
754
755    return None, None
756
757
758def resolve_tracking(remote, branch):
759    """Resolve tracked upstream remote/branch if current branch is tracked."""
760    tracked_remote, tracked_branch = parse_tracking()
761    # tracked_branch will be empty when tracking a local branch
762    if tracked_branch:
763        if VERBOSE:
764            print('Following tracked %s/%s rather than default %s/%s' % (
765                  tracked_remote, tracked_branch,
766                  remote, branch))
767        return tracked_remote, tracked_branch
768
769    return remote, branch
770
771
772def check_remote(branch, remote, scheme, hostname, port, project,
773                 usepushurl=False):
774    """Check that a Gerrit Git remote repo exists, if not, set one."""
775
776    if usepushurl:
777        push_url = git_config_get_value('remote.%s' % remote, 'pushurl', None)
778        if push_url:
779            return
780    else:
781        has_color = check_color_support()
782        if has_color:
783            color_never = "--color=never"
784        else:
785            color_never = ""
786
787        if remote in run_command("git remote").split("\n"):
788
789            remotes = run_command("git branch -a %s" % color_never).split("\n")
790            for current_remote in remotes:
791                remote_string = "remotes/%s/%s" % (remote, branch)
792                if (current_remote.strip() == remote_string and not UPDATE):
793                    return
794            # We have the remote, but aren't set up to fetch. Fix it
795            if VERBOSE:
796                print("Setting up gerrit branch tracking for better rebasing")
797            update_remote(remote)
798            return
799
800    if hostname is False or project is False:
801        # This means there was no .gitreview file
802        printwrap("No '.gitreview' file found in this repository. We don't "
803                  "know where your gerrit is.")
804        if usepushurl:
805            printwrap("Please set the push-url on your origin remote to the "
806                      "location of your gerrit server and try again")
807        else:
808            printwrap("Please manually create a remote named \"%s\" or "
809                      "rename the default one and try again." % remote)
810        sys.exit(1)
811
812    # Gerrit remote not present, try to add it
813    try:
814        add_remote(scheme, hostname, port, project, remote, usepushurl)
815    except Exception:
816        if usepushurl:
817            printwrap("We don't know where your gerrit is. Please manually"
818                      " add a push-url to the '%s' remote and try again."
819                      % remote)
820        else:
821            printwrap("We don't know where your gerrit is. Please manually"
822                      " create a remote named '%s' and try again." % remote)
823        raise
824
825
826def rebase_changes(branch, remote, interactive=True):
827
828    global _orig_head
829
830    remote_branch = "remotes/%s/%s" % (remote, branch)
831
832    if not update_remote(remote):
833        return False
834
835    # since the value of ORIG_HEAD may not be set by rebase as expected
836    # for use in undo_rebase, make sure to save it explicitly
837    cmd = "git rev-parse HEAD"
838    (status, output) = run_command_status(cmd)
839    if status != 0:
840        print("Errors running %s" % cmd)
841        if interactive:
842            print(output)
843        return False
844    _orig_head = output
845
846    cmd = "git show-ref --quiet --verify refs/%s" % remote_branch
847    (status, output) = run_command_status(cmd)
848    if status != 0:
849        printwrap("The branch '%s' does not exist on the given remote '%s'. "
850                  "If these changes are intended to start a new branch, "
851                  "re-run with the '-R' option enabled." % (branch, remote))
852        sys.exit(1)
853
854    # Determine git version to set rebase flags below.
855    output = run_command("git version")
856    rebase_flag = "--rebase-merges"
857    if "git version" in output:
858        try:
859            v = output.rsplit(None, 1)[1]
860            gitv = tuple(map(int, v.split('.')[:3]))
861            if gitv < (2, 18, 0):
862                rebase_flag = "--preserve-merges"
863        except Exception:
864            # We tried to determine the version and failed. Use current git
865            # flag as fallback.
866            warn("Could not determine git version. "
867                 "Using modern git rebase flags.")
868
869    interactive_flag = interactive and '-i' or ''
870
871    cmd = "git rebase %s %s %s" % \
872        (rebase_flag, interactive_flag, remote_branch)
873
874    (status, output) = run_command_status(cmd, GIT_EDITOR='true')
875    if status != 0:
876        print("Errors running %s" % cmd)
877        if interactive:
878            print(output)
879            printwrap("It is likely that your change has a merge conflict. "
880                      "You may resolve it in the working tree now as "
881                      "described above and then run 'git review' again, or "
882                      "if you do not want to resolve it yet (note that the "
883                      "change can not merge until the conflict is resolved) "
884                      "you may run 'git rebase --abort' then 'git review -R' "
885                      "to upload the change without rebasing.")
886        return False
887    return True
888
889
890def undo_rebase():
891    global _orig_head
892    if not _orig_head:
893        return True
894
895    cmd = "git reset --hard %s" % _orig_head
896    (status, output) = run_command_status(cmd)
897    if status != 0:
898        print("Errors running %s" % cmd)
899        print(output)
900        return False
901    return True
902
903
904def get_branch_name(target_branch):
905    global _branch_name
906    if _branch_name is not None:
907        return _branch_name
908    cmd = "git rev-parse --symbolic-full-name --abbrev-ref HEAD"
909    _branch_name = run_command(cmd)
910    if _branch_name == "HEAD":
911        # detached head or no branch found
912        _branch_name = target_branch
913    return _branch_name
914
915
916def assert_one_change(remote, branch, yes, have_hook):
917    if check_use_color_output():
918        use_color = "--color=always"
919    else:
920        use_color = "--color=never"
921    cmd = ("git log %s --decorate --oneline HEAD --not --remotes=%s" % (
922           use_color, remote))
923    (status, output) = run_command_status(cmd)
924    if status != 0:
925        print("Had trouble running %s" % cmd)
926        print(output)
927        sys.exit(1)
928    filtered = filter(None, output.split("\n"))
929    output_lines = sum(1 for s in filtered)
930    if output_lines == 1 and not have_hook:
931        printwrap("Your change was committed before the commit hook was "
932                  "installed. Amending the commit to add a gerrit change id.")
933        run_command("git commit --amend", GIT_EDITOR='true')
934    elif output_lines == 0:
935        printwrap("No changes between HEAD and %s/%s. Submitting for review "
936                  "would be pointless." % (remote, branch))
937        sys.exit(1)
938    elif output_lines > 1:
939        if not yes:
940            printwrap("You are about to submit multiple commits. This is "
941                      "expected if you are submitting a commit that is "
942                      "dependent on one or more in-review commits, or if you "
943                      "are submitting multiple self-contained but dependent "
944                      "changes. Otherwise you should consider squashing your "
945                      "changes into one commit before submitting (for "
946                      "indivisible changes) or submitting from separate "
947                      "branches (for independent changes).")
948            print("\nThe outstanding commits are:\n\n%s\n\n"
949                  "Do you really want to submit the above commits?" % output)
950            yes_no = do_input("Type 'yes' to confirm, other to cancel: ")
951            if yes_no.lower().strip() != "yes":
952                print("Aborting.")
953                sys.exit(1)
954
955
956def get_topic(target_branch):
957    branch_name = get_branch_name(target_branch)
958
959    branch_parts = branch_name.split("/")
960    if len(branch_parts) >= 3 and branch_parts[0] == "review":
961        # We don't want to set the review number as the topic
962        if branch_parts[2].isdigit():
963            return
964
965        topic = "/".join(branch_parts[2:])
966        if VERBOSE:
967            print("Using change number %s for the topic of the change "
968                  "submitted" % topic)
969        return topic
970
971    if VERBOSE:
972        print("Using local branch name %s for the topic of the change "
973              "submitted" % branch_name)
974    return branch_name
975
976
977class CannotQueryOpenChangesets(CommandFailed):
978    "Cannot fetch review information from gerrit"
979    EXIT_CODE = 32
980
981
982class CannotParseOpenChangesets(ChangeSetException):
983    "Cannot parse JSON review information from gerrit"
984    EXIT_CODE = 33
985
986
987class Review(dict):
988    _default_fields = ('branch', 'topic', 'subject')
989
990    def __init__(self, data):
991        if 'number' not in data:
992            raise TypeError("<Review> requires 'number' key in data")
993
994        super(Review, self).__init__(data)
995
996        # provide default values for some fields
997        for field in self._default_fields:
998            self[field] = self.get(field, '-')
999
1000
1001class ReviewsPrinter(object):
1002    def __init__(self, with_topic=False):
1003        if with_topic:
1004            self.fields = ('number', 'branch', 'topic', 'subject')
1005            # > is right justify, < is left, field indices for py26
1006            self.fields_format = [
1007                u"{0:>{1}}", u"{2:>{3}}", u"{4:>{5}}", u"{6:<{7}}"]
1008        else:
1009            self.fields = ('number', 'branch', 'subject')
1010            # > is right justify, < is left, field indices for py26
1011            self.fields_format = [u"{0:>{1}}", u"{2:>{3}}", u"{4:<{5}}"]
1012
1013        self.fields_colors = ("", "", "", "")
1014        self.color_reset = ""
1015        if check_use_color_output():
1016            self.fields_colors = (
1017                colors.yellow, colors.green, colors.blue, "")
1018            self.color_reset = colors.reset
1019
1020        self.reviews = []
1021
1022    @property
1023    def fields_width(self):
1024        return [
1025            max(len(str(review[field])) for review in self.reviews)
1026            for field in self.fields[:-1]
1027        ] + [1]
1028
1029    def _get_field_format_str(self, field):
1030        index = self.fields.index(field)
1031        return (
1032            self.fields_colors[index] +
1033            self.fields_format[index] +
1034            self.color_reset
1035        )
1036
1037    def add_review(self, review):
1038        self.reviews.append(review)
1039
1040    def _get_fields_format_str(self):
1041        return "  ".join([
1042            self._get_field_format_str(field)
1043            for field in self.fields])
1044
1045    def print_review(self, review):
1046        fields_format_str = self._get_fields_format_str()
1047
1048        formatted_fields = []
1049        for field, width in zip(self.fields, self.fields_width):
1050            formatted_fields.extend((
1051                review[field], width
1052            ))
1053
1054        print(fields_format_str.format(*formatted_fields))
1055
1056    def do_print(self, reviews):
1057
1058        total_reviews = len(reviews)
1059
1060        for review in reviews:
1061            self.print_review(review)
1062
1063        print("Found %d items for review" % total_reviews)
1064
1065
1066def list_reviews(remote, project, with_topic=False):
1067    remote_url = get_remote_url(remote)
1068
1069    reviews = []
1070    for r in query_reviews(remote_url,
1071                           project=project,
1072                           exception=CannotQueryOpenChangesets,
1073                           parse_exc=CannotParseOpenChangesets):
1074        reviews.append(Review(r))
1075
1076    if not reviews:
1077        print("No pending reviews")
1078        return
1079
1080    printer = ReviewsPrinter(with_topic=with_topic)
1081    for review in reviews:
1082        printer.add_review(review)
1083
1084    printer.do_print(reviews)
1085    return 0
1086
1087
1088class CannotQueryPatchSet(CommandFailed):
1089    "Cannot query patchset information"
1090    EXIT_CODE = 34
1091
1092
1093class ReviewInformationNotFound(ChangeSetException):
1094    "Could not fetch review information for change %s"
1095    EXIT_CODE = 35
1096
1097
1098class ReviewNotFound(ChangeSetException):
1099    "Gerrit review %s not found"
1100    EXIT_CODE = 36
1101
1102
1103class PatchSetGitFetchFailed(CommandFailed):
1104    """Cannot fetch patchset contents
1105
1106Does specified change number belong to this project?
1107"""
1108    EXIT_CODE = 37
1109
1110
1111class PatchSetNotFound(ChangeSetException):
1112    "Review patchset %s not found"
1113    EXIT_CODE = 38
1114
1115
1116class GerritConnectionException(GitReviewException):
1117    """Problem to establish connection to gerrit."""
1118    EXIT_CODE = 40
1119
1120
1121class CheckoutNewBranchFailed(CommandFailed):
1122    "Cannot checkout to new branch"
1123    EXIT_CODE = 64
1124
1125
1126class CheckoutExistingBranchFailed(CommandFailed):
1127    "Cannot checkout existing branch"
1128    EXIT_CODE = 65
1129
1130
1131class ResetHardFailed(CommandFailed):
1132    "Failed to hard reset downloaded branch"
1133    EXIT_CODE = 66
1134
1135
1136class SetUpstreamBranchFailed(CommandFailed):
1137    "Cannot set upstream to remote branch"
1138    EXIT_CODE = 67
1139
1140
1141class SymbolicRefFailed(CommandFailed):
1142    "Cannot find symbolic reference"
1143    EXIT_CODE = 68
1144
1145
1146class ForEachRefFailed(CommandFailed):
1147    "Cannot process symbolic reference"
1148    EXIT_CODE = 69
1149
1150
1151class BranchTrackingMismatch(GitReviewException):
1152    "Branch exists but is tracking unexpected branch"
1153    EXIT_CODE = 70
1154
1155
1156def fetch_review(review, masterbranch, remote, project):
1157    remote_url = get_remote_url(remote)
1158
1159    review_arg = review
1160    review, patchset_number = parse_review_number(review)
1161    current_patch_set = patchset_number is None
1162
1163    review_infos = query_reviews(remote_url,
1164                                 project=project,
1165                                 change=review,
1166                                 current_patch_set=current_patch_set,
1167                                 exception=CannotQueryPatchSet,
1168                                 parse_exc=ReviewInformationNotFound)
1169
1170    if not len(review_infos):
1171        raise ReviewInformationNotFound(review)
1172    for info in review_infos:
1173        if 'branch' in info and info['branch'] == masterbranch:
1174            if VERBOSE:
1175                print('Using review info from branch %s' % info['branch'])
1176            review_info = info
1177            break
1178    else:
1179        review_info = review_infos[0]
1180        if VERBOSE and 'branch' in review_info:
1181            print('Using default branch %s' % review_info['branch'])
1182
1183    try:
1184        if patchset_number is None:
1185            refspec = review_info['currentPatchSet']['ref']
1186        else:
1187            refspec = [ps for ps in review_info['patchSets']
1188                       if int(ps['number']) == int(patchset_number)][0]['ref']
1189    except IndexError:
1190        raise PatchSetNotFound(review_arg)
1191    except KeyError:
1192        raise ReviewNotFound(review)
1193
1194    try:
1195        topic = review_info['topic']
1196        if topic == masterbranch:
1197            topic = review
1198    except KeyError:
1199        topic = review
1200    try:
1201        author = re.sub('\W+', '_', review_info['owner']['name']).lower()
1202    except KeyError:
1203        author = 'unknown'
1204    remote_branch = review_info['branch']
1205
1206    if patchset_number is None:
1207        branch_name = "review/%s/%s" % (author, topic)
1208    else:
1209        branch_name = "review/%s/%s-patch%s" % (author, topic, patchset_number)
1210
1211    print("Downloading %s from gerrit" % refspec)
1212    run_command_exc(PatchSetGitFetchFailed,
1213                    "git", "fetch", remote_url, refspec)
1214    return branch_name, remote_branch
1215
1216
1217def checkout_review(branch_name, remote, remote_branch):
1218    """Checkout a newly fetched (FETCH_HEAD) change
1219       into a branch
1220    """
1221
1222    try:
1223        run_command_exc(CheckoutNewBranchFailed,
1224                        "git", "checkout", "-b",
1225                        branch_name, "FETCH_HEAD")
1226        # --set-upstream-to is supported starting in git 1.8
1227        if remote is not None:
1228            run_command_exc(SetUpstreamBranchFailed,
1229                            "git", "branch", "--set-upstream-to",
1230                            '%s/%s' % (remote, remote_branch),
1231                            branch_name)
1232
1233    except CheckoutNewBranchFailed as e:
1234        if re.search("already exists\.?", e.output):
1235            print("Branch %s already exists - reusing" % branch_name)
1236            track_remote, track_branch = parse_tracking(
1237                ref='refs/heads/' + branch_name)
1238            if track_remote and not (track_remote == remote and
1239                                     track_branch == remote_branch):
1240                print("Branch %s incorrectly tracking %s/%s instead of %s/%s"
1241                      % (branch_name,
1242                         track_remote, track_branch,
1243                         remote, remote_branch))
1244                raise BranchTrackingMismatch
1245            run_command_exc(CheckoutExistingBranchFailed,
1246                            "git", "checkout", branch_name)
1247            run_command_exc(ResetHardFailed,
1248                            "git", "reset", "--hard", "FETCH_HEAD")
1249        else:
1250            raise
1251
1252    print("Switched to branch \"%s\"" % branch_name)
1253
1254
1255class PatchSetGitCherrypickFailed(CommandFailed):
1256    "There was a problem applying changeset contents to the current branch."
1257    EXIT_CODE = 69
1258
1259
1260def cherrypick_review(option=None):
1261    cmd = ["git", "cherry-pick"]
1262    if option:
1263        cmd.append(option)
1264    cmd.append("FETCH_HEAD")
1265    print(run_command_exc(PatchSetGitCherrypickFailed, *cmd))
1266
1267
1268class CheckoutBackExistingBranchFailed(CommandFailed):
1269    "Cannot switch back to existing branch"
1270    EXIT_CODE = 67
1271
1272
1273class DeleteBranchFailed(CommandFailed):
1274    "Failed to delete branch"
1275    EXIT_CODE = 68
1276
1277
1278class InvalidPatchsetsToCompare(GitReviewException):
1279    def __init__(self, patchsetA, patchsetB):
1280        Exception.__init__(
1281            self,
1282            "Invalid patchsets for comparison specified (old=%s,new=%s)" % (
1283                patchsetA,
1284                patchsetB))
1285    EXIT_CODE = 39
1286
1287
1288def compare_review(review_spec, branch, remote, project, rebase=False):
1289    new_ps = None    # none means latest
1290
1291    if '-' in review_spec:
1292        review_spec, new_ps = review_spec.split('-')
1293    review, old_ps = parse_review_number(review_spec)
1294
1295    if old_ps is None or old_ps == new_ps:
1296        raise InvalidPatchsetsToCompare(old_ps, new_ps)
1297
1298    old_review = build_review_number(review, old_ps)
1299    new_review = build_review_number(review, new_ps)
1300
1301    old_branch, _ = fetch_review(old_review, branch, remote, project)
1302    checkout_review(old_branch, None, None)
1303
1304    if rebase:
1305        print('Rebasing %s' % old_branch)
1306        rebase = rebase_changes(branch, remote, False)
1307        if not rebase:
1308            print('Skipping rebase because of conflicts')
1309            run_command_exc(CommandFailed, 'git', 'rebase', '--abort')
1310
1311    new_branch, remote_branch = fetch_review(
1312        new_review,
1313        branch,
1314        remote,
1315        project)
1316    checkout_review(new_branch, remote, remote_branch)
1317
1318    if rebase:
1319        print('Rebasing also %s' % new_branch)
1320        if not rebase_changes(branch, remote, False):
1321            print("Rebasing of the new branch failed, "
1322                  "diff can be messed up (use -R to not rebase at all)!")
1323            run_command_exc(CommandFailed, 'git', 'rebase', '--abort')
1324
1325    subprocess.check_call(['git', 'diff', old_branch])
1326
1327
1328def finish_branch(target_branch):
1329    local_branch = get_branch_name(target_branch)
1330    if VERBOSE:
1331        print("Switching back to '%s' and deleting '%s'" % (target_branch,
1332                                                            local_branch))
1333    run_command_exc(CheckoutBackExistingBranchFailed,
1334                    "git", "checkout", target_branch)
1335    print("Switched to branch '%s'" % target_branch)
1336
1337    run_command_exc(DeleteBranchFailed,
1338                    "git", "branch", "-D", local_branch)
1339    print("Deleted branch '%s'" % local_branch)
1340
1341
1342def convert_bool(one_or_zero):
1343    "Return a bool on a one or zero string."
1344    return str(one_or_zero) in ["1", "true", "True"]
1345
1346
1347class MalformedInput(GitReviewException):
1348    EXIT_CODE = 3
1349
1350
1351def assert_valid_reviewers(reviewers):
1352    """Ensure no whitespace is found in reviewer names, as it will result
1353    in an invalid refspec.
1354    """
1355    for reviewer in reviewers:
1356        if re.search(r'\s', reviewer):
1357            raise MalformedInput(
1358                "Whitespace not allowed in reviewer: '%s'" % reviewer)
1359
1360
1361class _DownloadFlag(argparse.Action):
1362    """Special action for the various forms of downloading reviews.
1363
1364    Additional option parsing: store value in 'dest', but
1365    at the same time set one of the flag options to True
1366    """
1367    def __call__(self, parser, namespace, value, option_string=None):
1368        url = urlparse(value)
1369        # Turn URLs into change ids:
1370        #   https://review.openstack.org/423436
1371        # and
1372        #   https://review.openstack.org/423436/
1373        # and
1374        #   https://review.openstack.org/#/c/423436
1375        # and
1376        #   https://review.openstack.org/c/<project>/+/423436
1377        # become
1378        #   "423436"
1379        # while
1380        #   https://review.openstack.org/423436/1
1381        # and
1382        #   https://review.openstack.org/#/c/423436/1
1383        # and
1384        #   https://review.openstack.org/c/<project>/+/423436/1
1385        # become
1386        #   "423436,1".
1387        #
1388        # If there is a #, the rest of the path is stored in the
1389        # "fragment", otherwise that will be empty.
1390        base = url.fragment or url.path
1391        parts = base.rstrip('/').lstrip('/c').split('/')
1392        # PolyGerrit places the change after a '+' symbol in the url
1393        try:
1394            parts = parts[parts.index('+') + 1:]
1395        except ValueError:
1396            pass
1397        change = parts[0]
1398        if len(parts) > 1:
1399            change = '%s,%s' % (change, parts[1])
1400        setattr(namespace, self.dest, change)
1401        setattr(namespace, self.const, True)
1402
1403
1404def _main():
1405    usage = "git review [OPTIONS] ... [BRANCH]"
1406
1407    parser = argparse.ArgumentParser(usage=usage, description=COPYRIGHT)
1408
1409    topic_arg_group = parser.add_mutually_exclusive_group()
1410    topic_arg_group.add_argument("-t", "--topic", dest="topic",
1411                                 help="Topic to submit branch to")
1412    topic_arg_group.add_argument("-T", "--no-topic", dest="notopic",
1413                                 action="store_true",
1414                                 help="No topic except if explicitly provided")
1415
1416    parser.add_argument("--reviewers", nargs="+",
1417                        help="Add reviewers to uploaded patch sets.")
1418    parser.add_argument("-D", "--draft", dest="draft", action="store_true",
1419                        help="Submit review as a draft")
1420    parser.add_argument("-n", "--dry-run", dest="dry", action="store_true",
1421                        help="Don't actually submit the branch for review")
1422    parser.add_argument("-i", "--new-changeid", dest="regenerate",
1423                        action="store_true",
1424                        help="Regenerate Change-id before submitting")
1425    parser.add_argument("-r", "--remote", dest="remote",
1426                        help="git remote to use for gerrit")
1427    parser.add_argument("--use-pushurl", dest="usepushurl",
1428                        action="store_true",
1429                        help="Use remote push-url logic instead of separate"
1430                             " remotes")
1431
1432    rebase_group = parser.add_mutually_exclusive_group()
1433    rebase_group.add_argument("-R", "--no-rebase", dest="rebase",
1434                              action="store_false",
1435                              help="Don't rebase changes before submitting.")
1436    rebase_group.add_argument("-F", "--force-rebase", dest="force_rebase",
1437                              action="store_true",
1438                              help="Force rebase even when not needed.")
1439
1440    track_group = parser.add_mutually_exclusive_group()
1441    track_group.add_argument("--track", dest="track",
1442                             action="store_true",
1443                             help="Use tracked branch as default.")
1444    track_group.add_argument("--no-track", dest="track",
1445                             action="store_false",
1446                             help="Ignore tracked branch.")
1447
1448    fetch = parser.add_mutually_exclusive_group()
1449    fetch.set_defaults(download=False, compare=False, cherrypickcommit=False,
1450                       cherrypickindicate=False, cherrypickonly=False)
1451    fetch.add_argument("-d", "--download", dest="changeidentifier",
1452                       action=_DownloadFlag, metavar="CHANGE[,PS]",
1453                       const="download",
1454                       help="Download the contents of an existing gerrit "
1455                            "review into a branch. Include the patchset "
1456                            "number to download a specific version of the "
1457                            "change. The default is to take the most recent "
1458                            "version.")
1459    fetch.add_argument("-x", "--cherrypick", dest="changeidentifier",
1460                       action=_DownloadFlag, metavar="CHANGE",
1461                       const="cherrypickcommit",
1462                       help="Apply the contents of an existing gerrit "
1463                             "review onto the current branch and commit "
1464                             "(cherry pick; not recommended in most "
1465                             "situations)")
1466    fetch.add_argument("-X", "--cherrypickindicate", dest="changeidentifier",
1467                       action=_DownloadFlag, metavar="CHANGE",
1468                       const="cherrypickindicate",
1469                       help="Apply the contents of an existing gerrit "
1470                       "review onto the current branch and commit, "
1471                       "indicating its origin")
1472    fetch.add_argument("-N", "--cherrypickonly", dest="changeidentifier",
1473                       action=_DownloadFlag, metavar="CHANGE",
1474                       const="cherrypickonly",
1475                       help="Apply the contents of an existing gerrit "
1476                       "review to the working directory and prepare "
1477                       "for commit")
1478    fetch.add_argument("-m", "--compare", dest="changeidentifier",
1479                       action=_DownloadFlag, metavar="CHANGE,PS[-NEW_PS]",
1480                       const="compare",
1481                       help="Download specified and latest (or NEW_PS) "
1482                       "patchsets of an existing gerrit review into "
1483                       "a branches, rebase on master "
1484                       "(skipped on conflicts or when -R is specified) "
1485                       "and show their differences")
1486
1487    parser.add_argument("-u", "--update", dest="update", action="store_true",
1488                        help="Force updates from remote locations")
1489    parser.add_argument("-s", "--setup", dest="setup", action="store_true",
1490                        help="Just run the repo setup commands but don't "
1491                             "submit anything")
1492    parser.add_argument("-f", "--finish", dest="finish", action="store_true",
1493                        help="Close down this branch and switch back to "
1494                             "master on successful submission")
1495    parser.add_argument("-l", "--list", dest="list", action="count",
1496                        help="List available reviews for the current project, "
1497                        "if passed more than once, will show more information")
1498    parser.add_argument("-y", "--yes", dest="yes", action="store_true",
1499                        help="Indicate that you do, in fact, understand if "
1500                             "you are submitting more than one patch")
1501    parser.add_argument("-v", "--verbose", dest="verbose", action="store_true",
1502                        help="Output more information about what's going on")
1503
1504    wip_group = parser.add_mutually_exclusive_group()
1505    wip_group.add_argument("-w", "--work-in-progress", dest="wip",
1506                           action="store_true",
1507                           help="Send patch as work in progress for Gerrit "
1508                                "versions >= 2.15")
1509    wip_group.add_argument("-W", "--ready", dest="ready", action="store_true",
1510                           help="Send patch that is already work in progress"
1511                                " as ready for review. Gerrit versions >="
1512                                " 2.15")
1513
1514    private_group = parser.add_mutually_exclusive_group()
1515    private_group.add_argument("-p", "--private", dest="private",
1516                               action="store_true",
1517                               help="Send patch as a private patch ready for "
1518                                    "review. Gerrit versions >= 2.15")
1519    private_group.add_argument("-P", "--remove-private", dest="remove_private",
1520                               action="store_true",
1521                               help="Send patch which already in private state"
1522                                    " to normal patch."" Gerrit versions >= "
1523                                    "2.15")
1524
1525    parser.add_argument("--no-custom-script", dest="custom_script",
1526                        action="store_false", default=True,
1527                        help="Do not run custom scripts.")
1528    parser.add_argument("--color", dest="color", metavar="<when>",
1529                        nargs="?", choices=["always", "never", "auto"],
1530                        help="Show color output. --color (without [<when>]) "
1531                             "is the same as --color=always. <when> can be "
1532                             "one of %(choices)s. Behaviour can also be "
1533                             "controlled by the color.ui and color.review "
1534                             "configuration settings.")
1535    parser.add_argument("--no-color", dest="color", action="store_const",
1536                        const="never",
1537                        help="Turn off colored output. Can be used to "
1538                             "override configuration options. Same as "
1539                             "setting --color=never.")
1540    parser.add_argument("--license", dest="license", action="store_true",
1541                        help="Print the license and exit")
1542    parser.add_argument("--version", action="version",
1543                        version='%s version %s' %
1544                        (os.path.split(sys.argv[0])[-1], get_version()))
1545    parser.add_argument("branch", nargs="?")
1546
1547    parser.set_defaults(dry=False,
1548                        draft=False,
1549                        verbose=False,
1550                        update=False,
1551                        setup=False,
1552                        list=False,
1553                        yes=False,
1554                        wip=False,
1555                        ready=False,
1556                        private=False,
1557                        remove_private=False)
1558
1559    try:
1560        (top_dir, git_dir) = git_directories()
1561    except GitDirectoriesException as _no_git_dir:
1562        no_git_dir = _no_git_dir
1563    else:
1564        no_git_dir = False
1565        config = Config(os.path.join(top_dir, ".gitreview"))
1566        parser.set_defaults(rebase=convert_bool(config['rebase']),
1567                            track=convert_bool(config['track']),
1568                            remote=None,
1569                            usepushurl=convert_bool(config['usepushurl']))
1570    options = parser.parse_args()
1571
1572    if options.license:
1573        print(COPYRIGHT)
1574        sys.exit(0)
1575
1576    if no_git_dir:
1577        raise no_git_dir
1578
1579    if options.branch is None:
1580        branch = config['branch']
1581    else:
1582        # explicitly-specified branch on command line overrides options.track
1583        branch = options.branch
1584        options.track = False
1585
1586    global VERBOSE
1587    global UPDATE
1588    VERBOSE = options.verbose
1589    UPDATE = options.update
1590    remote = options.remote
1591    if not remote:
1592        if options.usepushurl:
1593            remote = 'origin'
1594        else:
1595            remote = config['remote']
1596    yes = options.yes
1597    status = 0
1598
1599    if options.track:
1600        remote, branch = resolve_tracking(remote, branch)
1601
1602    check_remote(branch, remote, config['scheme'],
1603                 config['hostname'], config['port'], config['project'],
1604                 usepushurl=options.usepushurl)
1605
1606    if options.color:
1607        set_color_output(options.color)
1608
1609    if options.changeidentifier:
1610        if options.compare:
1611            compare_review(options.changeidentifier,
1612                           branch, remote, config['project'],
1613                           options.rebase)
1614            return
1615        local_branch, remote_branch = fetch_review(options.changeidentifier,
1616                                                   branch, remote,
1617                                                   config['project'])
1618        if options.download:
1619            checkout_review(local_branch, remote, remote_branch)
1620        else:
1621            if options.cherrypickcommit:
1622                cherrypick_review()
1623            elif options.cherrypickonly:
1624                cherrypick_review("-n")
1625            if options.cherrypickindicate:
1626                cherrypick_review("-x")
1627        return
1628    elif options.list:
1629        with_topic = options.list > 1
1630        list_reviews(remote, config['project'], with_topic=with_topic)
1631        return
1632
1633    if options.custom_script:
1634        run_custom_script("pre")
1635
1636    hook_file = os.path.join(git_dir, "hooks", "commit-msg")
1637    have_hook = os.path.exists(hook_file) and os.access(hook_file, os.X_OK)
1638
1639    if not have_hook:
1640        set_hooks_commit_msg(remote, hook_file)
1641
1642    if options.setup:
1643        if options.finish and not options.dry:
1644            finish_branch(branch)
1645        return
1646
1647    if options.rebase or options.force_rebase:
1648        if not rebase_changes(branch, remote):
1649            sys.exit(1)
1650        if not options.force_rebase and not undo_rebase():
1651            sys.exit(1)
1652    assert_one_change(remote, branch, yes, have_hook)
1653
1654    ref = "for"
1655
1656    if options.draft:
1657        ref = "drafts"
1658        if options.custom_script:
1659            run_custom_script("draft")
1660
1661    cmd = "git push %s HEAD:refs/%s/%s" % (remote, ref, branch)
1662    push_options = []
1663    if options.topic is not None:
1664        topic = options.topic
1665    else:
1666        topic = None if options.notopic else get_topic(branch)
1667
1668    if topic and topic != branch:
1669        push_options.append("topic=%s" % topic)
1670
1671    if options.reviewers:
1672        assert_valid_reviewers(options.reviewers)
1673        push_options += ["r=%s" % r for r in options.reviewers]
1674
1675    if options.regenerate:
1676        print("Amending the commit to regenerate the change id\n")
1677        regenerate_cmd = "git commit --amend"
1678        if options.dry:
1679            print("\tGIT_EDITOR=\"sed -i -e '/^Change-Id:/d'\" %s\n" %
1680                  regenerate_cmd)
1681        else:
1682            run_command(regenerate_cmd,
1683                        GIT_EDITOR="sed -i -e "
1684                        "'/^Change-Id:/d'")
1685
1686    if options.wip:
1687        push_options.append('wip')
1688
1689    if options.ready:
1690        push_options.append('ready')
1691
1692    if options.private:
1693        push_options.append('private')
1694
1695    if options.remove_private:
1696        push_options.append('remove-private')
1697
1698    if push_options:
1699        cmd += "%" + ",".join(push_options)
1700    if options.dry:
1701        print("Please use the following command "
1702              "to send your commits to review:\n")
1703        print("\t%s\n" % cmd)
1704    else:
1705        (status, output) = run_command_status(cmd)
1706        print(output)
1707
1708    if options.finish and not options.dry and status == 0:
1709        finish_branch(branch)
1710        return
1711
1712    if options.custom_script:
1713        run_custom_script("post")
1714    sys.exit(status)
1715
1716
1717def main():
1718    # workaround for avoiding UnicodeEncodeError on print() with older python
1719    if sys.version_info[0] < 3:
1720        # without reload print would fail even if sys.stdin.encoding
1721        # would report utf-8
1722        # see: https://stackoverflow.com/a/23847316/99834
1723        stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
1724        reload(sys)
1725        sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
1726        sys.setdefaultencoding(os.environ.get('PYTHONIOENCODING', 'utf-8'))
1727
1728    try:
1729        _main()
1730    except GitReviewException as e:
1731        print(e)
1732        sys.exit(e.EXIT_CODE)
1733
1734
1735if __name__ == "__main__":
1736    main()
1737