xref: /llvm-project/llvm/utils/git/pre-push.py (revision c49770c60f26e449379447109f7d915bd8de0384)
1a61c73dbSMehdi Amini#!/usr/bin/env python3
2a61c73dbSMehdi Amini#
3a61c73dbSMehdi Amini# ======- pre-push - LLVM Git Help Integration ---------*- python -*--========#
4a61c73dbSMehdi Amini#
5a61c73dbSMehdi Amini# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6a61c73dbSMehdi Amini# See https://llvm.org/LICENSE.txt for license information.
7a61c73dbSMehdi Amini# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8a61c73dbSMehdi Amini#
9a61c73dbSMehdi Amini# ==------------------------------------------------------------------------==#
10a61c73dbSMehdi Amini
11a61c73dbSMehdi Amini"""
12a61c73dbSMehdi Aminipre-push git hook integration
13a61c73dbSMehdi Amini=============================
14a61c73dbSMehdi Amini
15a61c73dbSMehdi AminiThis script is intended to be setup as a pre-push hook, from the root of the
16a61c73dbSMehdi Aminirepo run:
17a61c73dbSMehdi Amini
18a61c73dbSMehdi Amini   ln -sf ../../llvm/utils/git/pre-push.py .git/hooks/pre-push
19a61c73dbSMehdi Amini
20a61c73dbSMehdi AminiFrom the git doc:
21a61c73dbSMehdi Amini
22a61c73dbSMehdi Amini  The pre-push hook runs during git push, after the remote refs have been
23a61c73dbSMehdi Amini  updated but before any objects have been transferred. It receives the name
24a61c73dbSMehdi Amini  and location of the remote as parameters, and a list of to-be-updated refs
25a61c73dbSMehdi Amini  through stdin. You can use it to validate a set of ref updates before a push
26a61c73dbSMehdi Amini  occurs (a non-zero exit code will abort the push).
27a61c73dbSMehdi Amini"""
28a61c73dbSMehdi Amini
29a61c73dbSMehdi Aminiimport argparse
30a61c73dbSMehdi Aminiimport shutil
31a61c73dbSMehdi Aminiimport subprocess
32a61c73dbSMehdi Aminiimport sys
33a61c73dbSMehdi Aminiimport time
34a61c73dbSMehdi Aminifrom shlex import quote
35a61c73dbSMehdi Amini
36a61c73dbSMehdi AminiVERBOSE = False
37a61c73dbSMehdi AminiQUIET = False
38a61c73dbSMehdi Aminidev_null_fd = None
39b71edfaaSTobias Hietaz40 = "0000000000000000000000000000000000000000"
40a61c73dbSMehdi Amini
41a61c73dbSMehdi Amini
42a61c73dbSMehdi Aminidef eprint(*args, **kwargs):
43a61c73dbSMehdi Amini    print(*args, file=sys.stderr, **kwargs)
44a61c73dbSMehdi Amini
45a61c73dbSMehdi Amini
46a61c73dbSMehdi Aminidef log(*args, **kwargs):
47a61c73dbSMehdi Amini    if QUIET:
48a61c73dbSMehdi Amini        return
49a61c73dbSMehdi Amini    print(*args, **kwargs)
50a61c73dbSMehdi Amini
51a61c73dbSMehdi Amini
52a61c73dbSMehdi Aminidef log_verbose(*args, **kwargs):
53a61c73dbSMehdi Amini    if not VERBOSE:
54a61c73dbSMehdi Amini        return
55a61c73dbSMehdi Amini    print(*args, **kwargs)
56a61c73dbSMehdi Amini
57a61c73dbSMehdi Amini
58a61c73dbSMehdi Aminidef die(msg):
59a61c73dbSMehdi Amini    eprint(msg)
60a61c73dbSMehdi Amini    sys.exit(1)
61a61c73dbSMehdi Amini
62a61c73dbSMehdi Amini
63a61c73dbSMehdi Aminidef ask_confirm(prompt):
64a61c73dbSMehdi Amini    while True:
65b71edfaaSTobias Hieta        query = input("%s (y/N): " % (prompt))
66b71edfaaSTobias Hieta        if query.lower() not in ["y", "n", ""]:
67b71edfaaSTobias Hieta            print("Expect y or n!")
68a61c73dbSMehdi Amini            continue
69b71edfaaSTobias Hieta        return query.lower() == "y"
70a61c73dbSMehdi Amini
71a61c73dbSMehdi Amini
72b71edfaaSTobias Hietadef shell(
73b71edfaaSTobias Hieta    cmd,
74b71edfaaSTobias Hieta    strip=True,
75b71edfaaSTobias Hieta    cwd=None,
76b71edfaaSTobias Hieta    stdin=None,
77b71edfaaSTobias Hieta    die_on_failure=True,
78b71edfaaSTobias Hieta    ignore_errors=False,
79b71edfaaSTobias Hieta    text=True,
80b71edfaaSTobias Hieta    print_raw_stderr=False,
81b71edfaaSTobias Hieta):
82a61c73dbSMehdi Amini    # Escape args when logging for easy repro.
83a61c73dbSMehdi Amini    quoted_cmd = [quote(arg) for arg in cmd]
84b71edfaaSTobias Hieta    cwd_msg = ""
85a61c73dbSMehdi Amini    if cwd:
86b71edfaaSTobias Hieta        cwd_msg = " in %s" % cwd
87b71edfaaSTobias Hieta    log_verbose("Running%s: %s" % (cwd_msg, " ".join(quoted_cmd)))
88a61c73dbSMehdi Amini
89a61c73dbSMehdi Amini    # Silence errors if requested.
90*c49770c6SNicolas van Kempen    err_pipe = subprocess.DEVNULL if ignore_errors else subprocess.PIPE
91a61c73dbSMehdi Amini
92a61c73dbSMehdi Amini    start = time.time()
93b71edfaaSTobias Hieta    p = subprocess.Popen(
94b71edfaaSTobias Hieta        cmd,
95b71edfaaSTobias Hieta        cwd=cwd,
96b71edfaaSTobias Hieta        stdout=subprocess.PIPE,
97b71edfaaSTobias Hieta        stderr=err_pipe,
98a61c73dbSMehdi Amini        stdin=subprocess.PIPE,
99b71edfaaSTobias Hieta        universal_newlines=text,
100b71edfaaSTobias Hieta    )
101a61c73dbSMehdi Amini    stdout, stderr = p.communicate(input=stdin)
102a61c73dbSMehdi Amini    elapsed = time.time() - start
103a61c73dbSMehdi Amini
104b71edfaaSTobias Hieta    log_verbose("Command took %0.1fs" % elapsed)
105a61c73dbSMehdi Amini
106a61c73dbSMehdi Amini    if p.returncode == 0 or ignore_errors:
107a61c73dbSMehdi Amini        if stderr and not ignore_errors:
108a61c73dbSMehdi Amini            if not print_raw_stderr:
109b71edfaaSTobias Hieta                eprint("`%s` printed to stderr:" % " ".join(quoted_cmd))
110a61c73dbSMehdi Amini            eprint(stderr.rstrip())
111a61c73dbSMehdi Amini        if strip:
112a61c73dbSMehdi Amini            if text:
113b71edfaaSTobias Hieta                stdout = stdout.rstrip("\r\n")
114a61c73dbSMehdi Amini            else:
115b71edfaaSTobias Hieta                stdout = stdout.rstrip(b"\r\n")
116a61c73dbSMehdi Amini        if VERBOSE:
117a61c73dbSMehdi Amini            for l in stdout.splitlines():
118b71edfaaSTobias Hieta                log_verbose("STDOUT: %s" % l)
119a61c73dbSMehdi Amini        return stdout
120b71edfaaSTobias Hieta    err_msg = "`%s` returned %s" % (" ".join(quoted_cmd), p.returncode)
121a61c73dbSMehdi Amini    eprint(err_msg)
122a61c73dbSMehdi Amini    if stderr:
123a61c73dbSMehdi Amini        eprint(stderr.rstrip())
124a61c73dbSMehdi Amini    if die_on_failure:
125a61c73dbSMehdi Amini        sys.exit(2)
126a61c73dbSMehdi Amini    raise RuntimeError(err_msg)
127a61c73dbSMehdi Amini
128a61c73dbSMehdi Amini
129a61c73dbSMehdi Aminidef git(*cmd, **kwargs):
130b71edfaaSTobias Hieta    return shell(["git"] + list(cmd), **kwargs)
131a61c73dbSMehdi Amini
132a61c73dbSMehdi Amini
133a61c73dbSMehdi Aminidef get_revs_to_push(range):
134b71edfaaSTobias Hieta    commits = git("rev-list", range).splitlines()
135a61c73dbSMehdi Amini    # Reverse the order so we print the oldest commit first
136a61c73dbSMehdi Amini    commits.reverse()
137a61c73dbSMehdi Amini    return commits
138a61c73dbSMehdi Amini
139a61c73dbSMehdi Amini
140a61c73dbSMehdi Aminidef handle_push(args, local_ref, local_sha, remote_ref, remote_sha):
141b71edfaaSTobias Hieta    """Check a single push request (which can include multiple revisions)"""
142b71edfaaSTobias Hieta    log_verbose(
143b71edfaaSTobias Hieta        "Handle push, reproduce with "
144b71edfaaSTobias Hieta        "`echo %s %s %s %s | pre-push.py %s %s"
145b71edfaaSTobias Hieta        % (local_ref, local_sha, remote_ref, remote_sha, args.remote, args.url)
146b71edfaaSTobias Hieta    )
147a61c73dbSMehdi Amini    # Handle request to delete
148a61c73dbSMehdi Amini    if local_sha == z40:
149b71edfaaSTobias Hieta        if not ask_confirm(
150b71edfaaSTobias Hieta            'Are you sure you want to delete "%s" on remote "%s"?'
151b71edfaaSTobias Hieta            % (remote_ref, args.url)
152b71edfaaSTobias Hieta        ):
153a61c73dbSMehdi Amini            die("Aborting")
154a61c73dbSMehdi Amini        return
155a61c73dbSMehdi Amini
156a61c73dbSMehdi Amini    # Push a new branch
157a61c73dbSMehdi Amini    if remote_sha == z40:
158b71edfaaSTobias Hieta        if not ask_confirm(
159b71edfaaSTobias Hieta            'Are you sure you want to push a new branch/tag "%s" on remote "%s"?'
160b71edfaaSTobias Hieta            % (remote_ref, args.url)
161b71edfaaSTobias Hieta        ):
162a61c73dbSMehdi Amini            die("Aborting")
163a61c73dbSMehdi Amini        range = local_sha
164a61c73dbSMehdi Amini        return
165a61c73dbSMehdi Amini    else:
166a61c73dbSMehdi Amini        # Update to existing branch, examine new commits
167b71edfaaSTobias Hieta        range = "%s..%s" % (remote_sha, local_sha)
168a61c73dbSMehdi Amini        # Check that the remote commit exists, otherwise let git proceed
169b71edfaaSTobias Hieta        if "commit" not in git("cat-file", "-t", remote_sha, ignore_errors=True):
170a61c73dbSMehdi Amini            return
171a61c73dbSMehdi Amini
172a61c73dbSMehdi Amini    revs = get_revs_to_push(range)
173a61c73dbSMehdi Amini    if not revs:
174a61c73dbSMehdi Amini        # This can happen if someone is force pushing an older revision to a branch
175a61c73dbSMehdi Amini        return
176a61c73dbSMehdi Amini
177a61c73dbSMehdi Amini    # Print the revision about to be pushed commits
178a61c73dbSMehdi Amini    print('Pushing to "%s" on remote "%s"' % (remote_ref, args.url))
179a61c73dbSMehdi Amini    for sha in revs:
180b71edfaaSTobias Hieta        print(" - " + git("show", "--oneline", "--quiet", sha))
181a61c73dbSMehdi Amini
182a61c73dbSMehdi Amini    if len(revs) > 1:
183b71edfaaSTobias Hieta        if not ask_confirm("Are you sure you want to push %d commits?" % len(revs)):
184b71edfaaSTobias Hieta            die("Aborting")
185a61c73dbSMehdi Amini
186a61c73dbSMehdi Amini    for sha in revs:
187b71edfaaSTobias Hieta        msg = git("log", "--format=%B", "-n1", sha)
188b71edfaaSTobias Hieta        if "Differential Revision" not in msg:
189a61c73dbSMehdi Amini            continue
190a61c73dbSMehdi Amini        for line in msg.splitlines():
191b71edfaaSTobias Hieta            for tag in ["Summary", "Reviewers", "Subscribers", "Tags"]:
192b71edfaaSTobias Hieta                if line.startswith(tag + ":"):
193b71edfaaSTobias Hieta                    eprint(
194b71edfaaSTobias Hieta                        'Please remove arcanist tags from the commit message (found "%s" tag in %s)'
195b71edfaaSTobias Hieta                        % (tag, sha[:12])
196b71edfaaSTobias Hieta                    )
197a61c73dbSMehdi Amini                    if len(revs) == 1:
198b71edfaaSTobias Hieta                        eprint("Try running: llvm/utils/git/arcfilter.sh")
199a61c73dbSMehdi Amini                    die('Aborting (force push by adding "--no-verify")')
200a61c73dbSMehdi Amini
201a61c73dbSMehdi Amini    return
202a61c73dbSMehdi Amini
203a61c73dbSMehdi Amini
204b71edfaaSTobias Hietaif __name__ == "__main__":
205b71edfaaSTobias Hieta    if not shutil.which("git"):
206b71edfaaSTobias Hieta        die("error: cannot find git command")
207a61c73dbSMehdi Amini
208a61c73dbSMehdi Amini    argv = sys.argv[1:]
209a61c73dbSMehdi Amini    p = argparse.ArgumentParser(
210b71edfaaSTobias Hieta        prog="pre-push",
211b71edfaaSTobias Hieta        formatter_class=argparse.RawDescriptionHelpFormatter,
212b71edfaaSTobias Hieta        description=__doc__,
213b71edfaaSTobias Hieta    )
214a61c73dbSMehdi Amini    verbosity_group = p.add_mutually_exclusive_group()
215b71edfaaSTobias Hieta    verbosity_group.add_argument(
216b71edfaaSTobias Hieta        "-q", "--quiet", action="store_true", help="print less information"
217b71edfaaSTobias Hieta    )
218b71edfaaSTobias Hieta    verbosity_group.add_argument(
219b71edfaaSTobias Hieta        "-v", "--verbose", action="store_true", help="print more information"
220b71edfaaSTobias Hieta    )
221a61c73dbSMehdi Amini
222b71edfaaSTobias Hieta    p.add_argument("remote", type=str, help="Name of the remote")
223b71edfaaSTobias Hieta    p.add_argument("url", type=str, help="URL for the remote")
224a61c73dbSMehdi Amini
225a61c73dbSMehdi Amini    args = p.parse_args(argv)
226a61c73dbSMehdi Amini    VERBOSE = args.verbose
227a61c73dbSMehdi Amini    QUIET = args.quiet
228a61c73dbSMehdi Amini
229a61c73dbSMehdi Amini    lines = sys.stdin.readlines()
230b71edfaaSTobias Hieta    sys.stdin = open("/dev/tty", "r")
231a61c73dbSMehdi Amini    for line in lines:
232a61c73dbSMehdi Amini        local_ref, local_sha, remote_ref, remote_sha = line.split()
233a61c73dbSMehdi Amini        handle_push(args, local_ref, local_sha, remote_ref, remote_sha)
234