Skip to content

Instantly share code, notes, and snippets.

@dduan
Last active September 18, 2018 05:24
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dduan/eea4aaace72cf9000b87db6272b94640 to your computer and use it in GitHub Desktop.
Save dduan/eea4aaace72cf9000b87db6272b94640 to your computer and use it in GitHub Desktop.
Generate commit for each staged file, such that each commit is a `--fixup` to the commit said file was last changed.
#!/usr/bin/env python
"""
Generate commit for each staged file, such that each commit is a `--fixup` to
the commit said file was last changed.
NOTE: this command will unstage all files. It also does not disninguish staged
and unstaged potion of the same file.
USAGE: stage files you want to commit, run this command. Interactive rebase with
autosquash: `git rebase -i --autosquash BASE`
WHY: sometimes you want to change a bunch of files and distribute changes to
each file to recent history of your branch. This happens when you have
a list of commit in a pull request you want to end up neatly in history,
but you need to make some last minutes changes.
"""
from subprocess import (call, check_output)
# list paths for cached files
cached_file_paths = check_output(['git', 'diff', '--cached', '--name-only'])
new_commits = []
for file_path in [p for p in cached_file_paths.split('\n') if p]:
# get SHA for the last commit this file is changed
latest_sha = check_output(['git', 'log', '-n', '1', '--pretty=format:%H', '--', file_path])
call(['git', 'reset', 'HEAD', file_path])
# ignore files that doesn't have a history
if not latest_sha:
continue
new_commits.append((file_path, latest_sha))
# unstage everything for safety measures
call(['git', 'reset', 'HEAD', '.'])
for file_path, sha in new_commits:
# stage file
call(['git', 'add', file_path])
# create a fixup commit
call(['git', 'commit', '--fixup', sha])
@pgherveou
Copy link

pgherveou commented Sep 18, 2018

@dduan super useful gist. Thanks for writing it!

If you use --name-status git diff option, then you can also make it work for file renames, by grouping both the old and new file name in the same --fixup

Here is how I did it by nodeifying your python script.

#!/usr/bin/node

const { execSync } = require('child_process')
const git = command => execSync(`git ${command}`, { encoding: 'utf8' }).trim()

const diffInfos = git('diff --cached --name-status')
  .split('\n')
  .map(diff => {
    const [_, ...files] = diff.split('\t')
    const latestSha = git(`log -n 1 --pretty=format:%H -- "${files[0]}"`)
    return { files, latestSha }
  })
  .filter(info => info.latestSha)

git('reset HEAD .')

diffInfos.forEach(({ files, latestSha }) => {
  files.forEach(file => git(`add "${file}"`))
  git(`commit --fixup ${latestSha}`)
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment