From 0b88762379ad84037a01ffca77c9a0523f020645 Mon Sep 17 00:00:00 2001 From: "luqui@chromium.org" Date: Wed, 3 Sep 2014 02:31:03 +0000 Subject: [PATCH] Added git footers tool to parse conventional metadata from git commits BUG=407316 Review URL: https://codereview.chromium.org/521033002 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@291777 0039d316-1c4b-4281-b951-d872f2087c98 --- git-footers | 8 + git_footers.py | 131 ++++ man/html/git-footers.html | 878 +++++++++++++++++++++++++++ man/man1/git-footers.1 | 144 +++++ man/src/_git-footers_desc.helper.txt | 1 + man/src/git-footers.demo.1.sh | 17 + man/src/git-footers.txt | 71 +++ tests/git_footers_test.py | 70 +++ 8 files changed, 1320 insertions(+) create mode 100755 git-footers create mode 100755 git_footers.py create mode 100644 man/html/git-footers.html create mode 100644 man/man1/git-footers.1 create mode 100644 man/src/_git-footers_desc.helper.txt create mode 100755 man/src/git-footers.demo.1.sh create mode 100644 man/src/git-footers.txt create mode 100755 tests/git_footers_test.py diff --git a/git-footers b/git-footers new file mode 100755 index 000000000..635cd8f46 --- /dev/null +++ b/git-footers @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# git_footers.py -- Extract the conventional footers associated with a commit. + +. $(type -P python_git_runner.sh) diff --git a/git_footers.py b/git_footers.py new file mode 100755 index 000000000..b08d46484 --- /dev/null +++ b/git_footers.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# Copyright 2014 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import argparse +import re +import sys + +from collections import defaultdict + +import git_common as git + +FOOTER_PATTERN = re.compile(r'^\s*([\w-]+): (.*)$') +CHROME_COMMIT_POSITION_PATTERN = re.compile(r'^([\w/-]+)@{#(\d+)}$') +GIT_SVN_ID_PATTERN = re.compile('^([^\s@]+)@(\d+)') + +def normalize_name(header): + return '-'.join([ word.title() for word in header.strip().split('-') ]) + + +def parse_footer(line): + match = FOOTER_PATTERN.match(line) + if match: + return (match.group(1), match.group(2)) + else: + return None + + +def parse_footers(message): + """Parses a git commit message into a multimap of footers.""" + footer_lines = [] + for line in reversed(message.splitlines()): + if line == '' or line.isspace(): + break + footer_lines.append(line) + + footers = map(parse_footer, footer_lines) + if not all(footers): + return defaultdict(list) + + footer_map = defaultdict(list) + for (k, v) in footers: + footer_map[normalize_name(k)].append(v.strip()) + + return footer_map + + +def get_unique(footers, key): + key = normalize_name(key) + values = footers[key] + assert len(values) <= 1, 'Multiple %s footers' % key + if values: + return values[0] + else: + return None + + +def get_position(footers): + """Get the chrome commit position from a footer multimap using a heuristic. + + Returns: + A tuple of the branch and the position on that branch. For example, + + Cr-Commit-Position: refs/heads/master@{#292272} + + would give the return value ('refs/heads/master', 292272). If + Cr-Commit-Position is not defined, we try to infer the ref and position + from git-svn-id. The position number can be None if it was not inferrable. + """ + + position = get_unique(footers, 'Cr-Commit-Position') + if position: + match = CHROME_COMMIT_POSITION_PATTERN.match(position) + assert match, 'Invalid Cr-Commit-Position value: %s' % position + return (match.group(1), match.group(2)) + + svn_commit = get_unique(footers, 'git-svn-id') + if svn_commit: + match = GIT_SVN_ID_PATTERN.match(svn_commit) + assert match, 'Invalid git-svn-id value: %s' % svn_commit + if re.match('.*/chrome/trunk/src$', match.group(1)): + return ('refs/heads/master', match.group(2)) + branch_match = re.match('.*/chrome/branches/([\w/-]+)/src$', match.group(1)) + if branch_match: + # svn commit numbers do not map to branches. + return ('refs/branch-heads/%s' % branch_match.group(1), None) + + raise ValueError('Unable to infer commit position from footers') + + +def main(args): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument('ref') + + g = parser.add_mutually_exclusive_group() + g.add_argument('--key', metavar='KEY', + help='Get all values for the given footer name, one per ' + 'line (case insensitive)') + g.add_argument('--position', action='store_true') + g.add_argument('--position-ref', action='store_true') + g.add_argument('--position-num', action='store_true') + + + opts = parser.parse_args(args) + + message = git.run('log', '-1', '--format=%B', opts.ref) + footers = parse_footers(message) + + if opts.key: + for v in footers.get(normalize_name(opts.key), []): + print v + elif opts.position: + pos = get_position(footers) + print '%s@{#%s}' % (pos[0], pos[1] or '?') + elif opts.position_ref: + print get_position(footers)[0] + elif opts.position_num: + pos = get_position(footers) + assert pos[1], 'No valid position for commit' + print pos[1] + else: + for k in footers.keys(): + for v in footers[k]: + print '%s: %s' % (k, v) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/man/html/git-footers.html b/man/html/git-footers.html new file mode 100644 index 000000000..11147ff28 --- /dev/null +++ b/man/html/git-footers.html @@ -0,0 +1,878 @@ + + + + + +git-freeze(1) + + + + + +
+
+

SYNOPSIS

+
+
+
git footers [--key FOOTER] REF
+git footers [--position | --position-ref | --position-num] REF
+
+
+
+
+
+

DESCRIPTION

+
+

git footers extracts information included in commit messages as "footers", +which are roughly like HTTP headers except they are at the end. For example, a +commit might look like:

+
+
+
This is a fancy commit message.
+
+
+
+
Cr-Commit-Position: refs/heads/master@{#292272}
+Tech-Debt-Introduced: 17 nanoMSOffices
+
+

git footers knows how to extract this information.

+

Footers are order-independent and can appear more than once. Thus they are +treated as a multimap.

+
+
+
+

OPTIONS

+
+

If no options are given, all footers are printed, with their names +case-normalized.

+
+
+--key FOOTER +
+
+

+ Extract all the headers associated with the given key, and print one per + line. If there are no footers with this key, produces no output and exits + successfully. +

+
+
+--position +
+
+

+ Extract the Chrome commit position from the footers. This first attempts + to get the value of the Cr-Commit-Position footer. If that doesn’t exist + then it tries a heuristic based on Git-Svn-Id. Output is in one of the + following forms: +

+
+
+
refs/heads/master@{#292272}
+refs/branch-heads/branchname
+
+
+
+--position-num +
+
+

+ Extracts and prints the Chrome commit position number only (292272 in the + example above). Exits with an error if one cannot be found. +

+
+
+--position-ref +
+
+

+ Extracts and prints the Chrome commit position ref name only + (ref/heads/master or refs/branch-heads/branchname in the example above). +

+
+
+
+
+
+

EXAMPLE

+
+

$ git footers HEAD
+Tech-Debt-Introduced: -4 microMSOffices
+Tech-Debt-Introduced: 17 microMSOffices
+Cr-Commit-Position: refs/heads/master@{#292272}
+$ git footers --key Tech-Debt-Introduced HEAD
+-4 microMSOffices
+17 microMSOffices
+$ git footers --position HEAD
+refs/heads/master@{#292272}
+$ git footers --position-num HEAD
+292272
+$ git footers --position-ref HEAD
+refs/heads/master
+

+
+
+
+

SEE ALSO

+ +
+
+

CHROMIUM DEPOT_TOOLS

+
+

Part of the chromium depot_tools(7) suite. These tools are meant to +assist with the development of chromium and related projects. Download the tools +from here.

+
+
+
+

+ + + diff --git a/man/man1/git-footers.1 b/man/man1/git-footers.1 new file mode 100644 index 000000000..c76139958 --- /dev/null +++ b/man/man1/git-footers.1 @@ -0,0 +1,144 @@ +'\" t +.\" Title: git-freeze +.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author] +.\" Generator: DocBook XSL Stylesheets v1.76.1 +.\" Date: 09/02/2014 +.\" Manual: Chromium depot_tools Manual +.\" Source: depot_tools c9f28eb +.\" Language: English +.\" +.TH "GIT\-FREEZE" "1" "09/02/2014" "depot_tools c9f28eb" "Chromium depot_tools Manual" +.\" ----------------------------------------------------------------- +.\" * Define some portability stuff +.\" ----------------------------------------------------------------- +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.\" http://bugs.debian.org/507673 +.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" ----------------------------------------------------------------- +.\" * set default formatting +.\" ----------------------------------------------------------------- +.\" disable hyphenation +.nh +.\" disable justification (adjust text to left margin only) +.ad l +.\" ----------------------------------------------------------------- +.\" * MAIN CONTENT STARTS HERE * +.\" ----------------------------------------------------------------- +.SH "NAME" +git-footers \- Extract meta\-information expressed as footers in a commit message\&. +.SH "SYNOPSIS" +.sp +.nf +\fIgit footers\fR [\-\-key FOOTER] REF +\fIgit footers\fR [\-\-position | \-\-position\-ref | \-\-position\-num] REF +.fi +.sp +.SH "DESCRIPTION" +.sp +git footers extracts information included in commit messages as "footers", which are roughly like HTTP headers except they are at the end\&. For example, a commit might look like: +.sp +.if n \{\ +.RS 4 +.\} +.nf +This is a fancy commit message\&. +.fi +.if n \{\ +.RE +.\} +.sp +.if n \{\ +.RS 4 +.\} +.nf +Cr\-Commit\-Position: refs/heads/master@{#292272} +Tech\-Debt\-Introduced: 17 nanoMSOffices +.fi +.if n \{\ +.RE +.\} +.sp +git footers knows how to extract this information\&. +.sp +Footers are order\-independent and can appear more than once\&. Thus they are treated as a multimap\&. +.SH "OPTIONS" +.sp +If no options are given, all footers are printed, with their names case\-normalized\&. +.PP +\-\-key FOOTER +.RS 4 +Extract all the headers associated with the given key, and print one per line\&. If there are no footers with this key, produces no output and exits successfully\&. +.RE +.PP +\-\-position +.RS 4 +Extract the Chrome commit position from the footers\&. This first attempts to get the value of the +Cr\-Commit\-Position +footer\&. If that doesn\(cqt exist then it tries a heuristic based on +Git\-Svn\-Id\&. Output is in one of the following forms: +.sp +.if n \{\ +.RS 4 +.\} +.nf +refs/heads/master@{#292272} +refs/branch\-heads/branchname +.fi +.if n \{\ +.RE +.\} +.RE +.PP +\-\-position\-num +.RS 4 +Extracts and prints the Chrome commit position number only (292272 in the example above)\&. Exits with an error if one cannot be found\&. +.RE +.PP +\-\-position\-ref +.RS 4 +Extracts and prints the Chrome commit position ref name only (ref/heads/master +or +refs/branch\-heads/branchname +in the example above)\&. +.RE +.SH "EXAMPLE" +.sp + +.sp +.if n \{\ +.RS 4 +.\} +.nf +\fB$ git footers HEAD\fR +Tech\-Debt\-Introduced: \-4 microMSOffices +Tech\-Debt\-Introduced: 17 microMSOffices +Cr\-Commit\-Position: refs/heads/master@{#292272} +\fB$ git footers \-\-key Tech\-Debt\-Introduced HEAD\fR +\-4 microMSOffices +17 microMSOffices +\fB$ git footers \-\-position HEAD\fR +refs/heads/master@{#292272} +\fB$ git footers \-\-position\-num HEAD\fR +292272 +\fB$ git footers \-\-position\-ref HEAD\fR +refs/heads/master +.fi +.if n \{\ +.RE +.\} +.sp +.SH "SEE ALSO" +.sp +\fBgit-number\fR(1) +.SH "CHROMIUM DEPOT_TOOLS" +.sp +Part of the chromium \fBdepot_tools\fR(7) suite\&. These tools are meant to assist with the development of chromium and related projects\&. Download the tools from \m[blue]\fBhere\fR\m[]\&\s-2\u[1]\d\s+2\&. +.SH "NOTES" +.IP " 1." 4 +here +.RS 4 +\%https://chromium.googlesource.com/chromium/tools/depot_tools.git +.RE diff --git a/man/src/_git-footers_desc.helper.txt b/man/src/_git-footers_desc.helper.txt new file mode 100644 index 000000000..2966eb743 --- /dev/null +++ b/man/src/_git-footers_desc.helper.txt @@ -0,0 +1 @@ +Extract meta-information expressed as footers in a commit message. diff --git a/man/src/git-footers.demo.1.sh b/man/src/git-footers.demo.1.sh new file mode 100755 index 000000000..6825857cd --- /dev/null +++ b/man/src/git-footers.demo.1.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +. demo_repo.sh + +add deleted_file +add unstaged_deleted_file +add modified_file +c 'I commited this and am proud of it. + +Cr-Commit-Position: refs/heads/master@{#292272} +Tech-Debt-Introduced: 17 microMSOffices +Tech-Debt-Introduced: -4 microMSOffices' + +run git footers HEAD +run git footers --key Tech-Debt-Introduced HEAD +run git footers --position HEAD +run git footers --position-num HEAD +run git footers --position-ref HEAD diff --git a/man/src/git-footers.txt b/man/src/git-footers.txt new file mode 100644 index 000000000..2623581f2 --- /dev/null +++ b/man/src/git-footers.txt @@ -0,0 +1,71 @@ +git-freeze(1) +============= + +NAME +---- +git-footers - +include::_git-footers_desc.helper.txt[] + +SYNOPSIS +-------- +[verse] +'git footers' [--key FOOTER] REF +'git footers' [--position | --position-ref | --position-num] REF + +DESCRIPTION +----------- + +`git footers` extracts information included in commit messages as "footers", +which are roughly like HTTP headers except they are at the end. For example, a +commit might look like: + + This is a fancy commit message. + + Cr-Commit-Position: refs/heads/master@{#292272} + Tech-Debt-Introduced: 17 nanoMSOffices + +`git footers` knows how to extract this information. + +Footers are order-independent and can appear more than once. Thus they are +treated as a multimap. + +OPTIONS +------- + +If no options are given, all footers are printed, with their names +case-normalized. + +--key FOOTER:: + Extract all the headers associated with the given key, and print one per + line. If there are no footers with this key, produces no output and exits + successfully. + +--position:: + Extract the Chrome commit position from the footers. This first attempts + to get the value of the `Cr-Commit-Position` footer. If that doesn't exist + then it tries a heuristic based on `Git-Svn-Id`. Output is in one of the + following forms: + + refs/heads/master@{#292272} + refs/branch-heads/branchname + +--position-num:: + Extracts and prints the Chrome commit position number only (292272 in the + example above). Exits with an error if one cannot be found. + +--position-ref:: + Extracts and prints the Chrome commit position ref name only + (`ref/heads/master` or `refs/branch-heads/branchname` in the example above). + + +EXAMPLE +------- +demo:1[] + +SEE ALSO +-------- +linkgit:git-number[1] + +include::_footer.txt[] + +// vim: ft=asciidoc: diff --git a/tests/git_footers_test.py b/tests/git_footers_test.py new file mode 100755 index 000000000..5cf9ccc55 --- /dev/null +++ b/tests/git_footers_test.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +"""Tests for git_footers.""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import git_footers + +class GitFootersTest(unittest.TestCase): + _message = """ +This is my commit message. There are many like it, but this one is mine. + +My commit message is my best friend. It is my life. I must master it. + +""" + + _position = 'refs/heads/master@{#292272}' + + _position_footer = 'Cr-Commit-Position: %s\n' % _position + + _git_svn_id = ('svn://svn.chromium.org/chrome/trunk/src@290386' + ' 0039d316-1c4b-4281-b951-d872f2087c98') + + _git_svn_id_footer = 'git-svn-id: %s\n' % _git_svn_id + + _git_svn_id_branch = ( + 'svn://svn.chromium.org/chrome/branches/blabble/src@177288') + + _git_svn_id_footer_branch = 'git-svn-id: %s\n' % _git_svn_id_branch + + + def testFootersBasic(self): + self.assertEqual( + git_footers.parse_footers(self._message), {}) + self.assertEqual( + git_footers.parse_footers(self._message + self._position_footer), + { 'Cr-Commit-Position': [ self._position ] }) + self.assertEqual( + git_footers.parse_footers(self._message + self._git_svn_id_footer), + { 'Git-Svn-Id': [ self._git_svn_id ] }) + self.assertEqual( + git_footers.parse_footers(self._message + self._position_footer + + self._position_footer), + { 'Cr-Commit-Position': [ self._position, self._position ] }) + + def testTrunkHeuristic(self): + footers = git_footers.parse_footers(self._message + self._git_svn_id_footer) + self.assertEqual( + footers, + { 'Git-Svn-Id': [ self._git_svn_id ] }) + self.assertEqual( + git_footers.get_position(footers), + ('refs/heads/master', '290386')) + + def testBranchHeuristic(self): + footers = git_footers.parse_footers(self._message + + self._git_svn_id_footer_branch) + self.assertEqual( + footers, + { 'Git-Svn-Id': [ self._git_svn_id_branch ] }) + self.assertEqual( + git_footers.get_position(footers), + ('refs/branch-heads/blabble', None)) + +if __name__ == '__main__': + unittest.main()