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
+
+
+
+
+
+
+
+
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()