Published 2024-12-12.
Last modified 2024-12-14.
Time to read: 2 minutes.
git
collection.
Ever have git push
choke on a large file, for example, a file over 2 GB?
Wouldn't it be nice for the names of overly large files to automagically be added to .gitignore
instead of having to look up how git undo
works again?
I have maintained a series of streamlined commit
scripts for CVS, SVN and git for
19 years.
I removed the CVS and SVN support several years ago, so only git is supported now.
Originally written in bash, the current version is written in Ruby,
and uses the rugged
gem.
The Ruby language binding to libgit2
that rugged
provides
allows for more advanced features than
git porcelain commands can offer.
High-Level Functionality
The commit
scripts perform the following:
git add
git commit
git push
The Ruby version examines the file sizes about to be added to the commit,
and ensures that large files are added to .gitignore
instead of causing git push
to fail.
A message is displayed, so the user is informed when large files are ignored.
Ruby Version
This is the latest version, and is the only version with the ability to handle large files. Using this version means you no longer have to worry about getting tangled up in undoing commits that failed because they included a large file.
This version requires that Ruby is set up properly on your computer. If you do not want to set up Ruby, you can run the older bash version of commit instead, but that version does not include the large file handling feature.
Installation
- Follow the directions in Setting Up a Ruby Development Environment.
-
Install the
rugged
gem. -
Install the other dependencies:
Shell$ gem install activesupport
$ gem install colorator
$ gem install optparse -
Download the
commit
script to a directory on yourPATH
, for example,/usr/
, or the standard Ubuntu directory for user scriptslocal/ bin/ ~/.local/
bin/ -
Make the
commit
script executable:
Shell$ chmod a+x /usr/local/bin/commit
Help Message
$ commit -h Runs git commit without prompting for a message. Files larger than 2 GB are added to .gitignore instead of being committed. Usage: commit [options] [file...] Where options are: -a "tag message" -m "commit message" -v 0 # Minimum verbosity -v 1 # Default verbosity -v 2 # Maximum verbosity Examples: commit # The default commit message is just a single dash (-) commit -v 0 commit -m "This is a commit message" commit -v 0 -m "This is a commit message" commit -a 0.1.2
Source Code
The source code for the Ruby version of commit
follows.
The Walk Through Git’s Dirty Files section of the
Working With Git Repos Using Ruby's Rugged Gem article discusses how the code works.
#!/usr/bin/env ruby require 'active_support' require 'active_support/core_ext/numeric/conversions' require 'colorator' require 'optparse' require 'rugged' # Originally written in bash by Mike Slinn 2005-09-05 # Converted to Ruby and added MAX_SIZE 2024-12-11 # See https://docs.github.com/en/repositories/working-with-files/managing-large-files/configuring-git-large-file-storage # See https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github class GitCommit KB = 1024 MB = KB * KB GB = KB * MB # File size limits for git commits depend on the type of your GitHub account: GITHUB_FREE = 100 * MB GITHUB_PRO = 2 * GB GITHUB_TEAM = 4 * GB GITHUB_ENTERPRISE_CLOUD = 5 * GB GIT_LFS_ENABLED = false MAX_SIZE = GIT_LFS_ENABLED ? GITHUB_FREE : 100 * MB # Adjust this to suit your GitHub account ActiveSupport::NumberHelper.alias_method :to_human, :number_to_human_size def initialize(default_branch: 'master') @branch = default_branch @gitignore_dirty = false @nh = ActiveSupport::NumberHelper @commit_size = 0 @repo = Rugged::Repository.new '.' end # Needs absolute path or the path relative to the current directory, not just the name of the directory def add_recursively(name) Dir.entries(name).each do |entry| path = "#{name}/#{entry}" if File.directory(entry) scan_directory path else file_add path end end end def discover_branch if @repo.branches.entries.empty? # puts "\nYour git repository is empty. Please add at least one file before committing.".red # exit 4 run "git branch -M #{@branch}" else @branch = @repo.head.name.sub(/^refs\/heads\//, '') end end # @param filename [String] Must be a path relative to the git root def file_add(filename) file_size = File.exist?(filename) ? File.size(filename) : 0 if large_file?(filename) msg = <<~MESSAGE Not adding '#{filename}' because the git file size limit is #{@nh.to_human MAX_SIZE}, however the file is #{@nh.to_human file_size}. The file will be added to .gitignore. MESSAGE puts msg.yellow unless @options[:verbosity].zero? newline = needs_newline('.gitignore') ? "\n" : '' File.write('.gitignore', "#{newline}#{filename}\n", mode: 'a') @gitignore_dirty = true elsif filename == '.gitignore' @gitignore_dirty = true else commit_push("A portion of the files to be committed is being pushed now because they are large.") if @commit_size + file_size >= MAX_SIZE / 2.0 puts "Adding #{filename}".green unless @options[:verbosity].zero? run "git add '#{escape filename}'", verbose: @options[:verbosity] >= 2 end @change_count += 1 @commit_size += file_size end # Handles single quotes in filename def escape(string) string.gsub "'", "\\\\'" end def git_project? run 'git rev-parse 2> /dev/null', verbose: false end def help(msg = nil) printf "Error: #{msg}\n\n".yellow unless msg.nil? msg = <<~HELP Runs git commit without prompting for a message. Files larger than #{@nh.to_human MAX_SIZE} are added to .gitignore instead of being committed. Usage: commit [options] [file...] Where options are: -a "tag message" -m "commit message" -v 0 # Minimum verbosity -v 1 # Default verbosity -v 2 # Maximum verbosity Examples: commit # The default commit message is just a single dash (-) commit -v 0 commit -m "This is a commit message" commit -v 0 -m "This is a commit message" commit -a 0.1.2 HELP puts msg.yellow exit 1 end # Assumes .gitignore is never large def large_file?(filename) File.size(filename) > MAX_SIZE if File.exist?(filename) end def large_files large = [] @repo.status do |path, flags| puts "#{path} #{flags}" if @options[:verbosity].positive? if File(path).dir? scan_dir path elsif large_file?(filename) large << path end end large end def commit_push(msg = nil) puts msg.yellow if msg msg = @options[:message] if @options[:message] discover_branch run("git commit -m '#{msg}' 2>&1 | sed -e '/^X11/d' -e '/^Warning:/d'", verbose: false) # @repo.push 'origin', ['refs/heads/master'] # Needs a callback to handle authentication puts "Pushing to origin #{@branch}".green unless @options[:verbosity].zero? run("git push origin #{@branch} --tags 3>&1 1>&2 2>&3 | sed -e '/^X11/d' -e '/^Warning:/d'", verbose: false) @change_count = 0 @commit_size = 0 end def main @options = parse_options if git_project? process_tag # Exits if a tag was created recursive_add commit_push if @commit_size.positive? else puts "Error: '#{Dir.pwd}' is not a git project".red exit 3 end end def needs_newline(filename) return false unless File.exist? filename file_contents = File.read filename file_contents.nil? || file_contents.empty? || !file_contents.end_with?("\n") end # Sets class variables @options def parse_options options = { message: '-', verbosity: 1 } OptionParser.new do |parser| parser.program_name = File.basename __FILE__ @parser = parser parser.on('-m', '--message MESSAGE', 'Specify commit message') parser.on('-v', '--verbosity VERBOSITY', Integer, 'Verbosity (0..2)') parser.on_tail('-h', '--help', 'Show this message') do help end end.order!(into: options) help "Invalid verbosity value (#{options[:verbosity]})." if options[:verbosity].negative? || options[:verbosity] > 2 options end def process_tag tag = @options[:tag] if tag run("git tag -a #{tag} -m 'v#{tag}'", verbose: false) run("git push origin --tags", verbose: false) exit end end # Exclude big files, git add all others def recursive_add @change_count = 0 @repo.status do |path, flags| next if flags.include? :ignored if File.directory? path scan_directory path else file_add path end end if @gitignore_dirty puts "Changing .gitignore".green unless @options[:verbosity].zero? run "git add .gitignore", verbose: false @change_count += 1 end if @change_count.zero? puts "No changes were detected to this git repository.".green if @options[:verbosity].positive? exit end end def run(command, verbose: true, do_not_execute: false) puts command if verbose `#{command}`.chomp unless do_not_execute end def scan_directory(path) Dir.children(path) do |name| child_path = "#{path}/#{name}" if File.directory? child_path scan_directory child_path else file_add child_path end end end end if __FILE__ == $PROGRAM_NAME git_commit = GitCommit.new git_commit.main end
Sample Usage
$ commit Adding my_new_file.txt Not adding 'video.mp4' because the git file size limit is 2 GB, however the file is 2.12 GB. The file will be added to .gitignore.
Pushing to origin master
$ commit No changes were detected to this git repository.
Older Bash Version
The bash version installs more easily than the Ruby version,
but it cannot prevent big files from causing git push
to choke and fail.
Installation
-
Download the bash script to a directory on your
PATH
, for example,/usr/local/bin/
, the standard Ubuntu directory for user scripts~/.local/bin/
-
Make the script executable:
Shell$ chmod a+x /usr/local/bin/commit
Help Message
$ commit -h Runs git commit without prompting for a message. Usage: commit [options] [file...] Where options are: -a "tag message" -d # enables debug mode -m "commit message" Examples: commit # The default commit message is just a single dash (-) commit -m "This is a commit message" commit -a 0.1.2
Source Code
#!/bin/bash # Originally written 2005-09-05 by Mike Slinn for CVS, then SVN, then git. function help { echo "Runs git commit without prompting for a message." echo "Usage: commit [options] [file...]" echo " Where options are:" echo " -a \"tag message\"" echo " -d # enables debug mode" echo " -m \"commit message\"" echo "Examples:" echo " commit # The default commit message is just a single dash (-)" echo " commit -m \"This is a commit message\"" echo " commit -a 0.1.2" exit 1 } function isGitProject { cd "$( git rev-parse --git-dir )/.." || exit 2 [ -d .git ] } BRANCH="$(git rev-parse --abbrev-ref HEAD)" MSG="" while getopts "a:dhm:?" opt; do case $opt in a ) TAG="$OPTARG" git tag -a "$TAG" -m "v$TAG" git push origin --tags exit ;; d ) set -xv ;; m ) MSG="$OPTARG" ;; h ) help ;; \?) help ;; esac done shift "$((OPTIND-1))" for o in "$@"; do if [ "$o" == "-m" ]; then unset MSG; fi done if isGitProject; then if [ "$@" ]; then git add -A "$@" else git add -A . fi shift if [ "$MSG" == "" ]; then MSG="-"; fi git commit -m "$MSG" "$@" 3>&1 1>&2 2>&3 | sed -e '/^X11/d' | sed -e '/^Warning:/d' git push origin "$BRANCH" --tags 3>&1 1>&2 2>&3 | sed -e '/^X11/d' | sed -e '/^Warning:/d' else echo "Error: '$( pwd )' is not a git project" fi if [ -f 0 ]; then rm -f 0; fi