11

I have a directory with files like this

a.JPG
b.JPG
c.JPG

I would like to perform something like this

git mv a.JPG a.jpg

I tried using xargs and other tools but nothing seems to work.

Nick Vanderbilt
  • 36,724
  • 29
  • 83
  • 106
  • You mention in a comment below that you use OS X. This means that you are (probably) using a case-preserving case-insensitive file system. Hence the problem in the renaming: your filesystem sees the destination name as the same as the target name, and since Git works through the filesystem, it, too, has its casing blinkers on. wbyoung's approach below will solve the problem, by passing the files through intermediate names that are recognizably different. – Jeet Jun 06 '10 at 05:29
  • Note: git 2.0.1 solves the issue: http://stackoverflow.com/a/24979063/6309 – VonC Jul 27 '14 at 08:06

5 Answers5

9

The core of the solution will be to use a tool/method that will automate the bulk rename. You can either use mv in combination with git add or just git mv. In either case you may have to take extra steps if you are using a case insensitive filesystem. So before we tackle the bulk renaming, it may be useful to discuss how case is handled a bit.

Case Sensitivity

Some systems (or system+filesystem combinations—like the default variant of the HFS+ filesystem on Mac OS X*) are case preserving, but case insensitive. On such systems, you may need to be careful when making renames that involve only changing the case of a name. The usual workaround is to use a temporary name that differs by more than just case as a “bridge” between the two names that differ by case alone (e.g. mv foo.JPG tmp && mv tmp foo.jpg).

* It is possible to use case sensitive file systems on Mac OS X (including a case sensitive variant of HFS+).

From here on, I will assume a case insensitive filesystem.

The mv command on Mac OS X can handle case-change-only renames in a single step. It will give an “overwrite?” prompt if run with the -i option, and it will skip the rename if given the -n option. It is only succeeding through the “enough rope to hang your self” default operation of many parts of Unix-like systems.

The git mv command is a bit more paranoid about the situation. It refuses the operation (“destination exists” error) unless given -f/--force option.

# this will succeed, though it may fail/prompt if mv is aliased to use -n/-i
mv foo.JPG foo.jpg

# this will succeed
mv -f bar.JPG bar.jpg

# this will succeed but give a warning
git mv -f quux.JPG quux.jpg

Bulk Rename Options

Perl rename

The desired operation is simple enough to do with a bit of shell scripting, but you could get the Perl rename utility (the one Jordan Lewis mentions) if you needed to do something that was a lot more complicated. You could try the rename from Debian's perl package, or if you feel up to using CPAN, you could install File::Rename, which includes the rename program.

ksh, bash, zsh, dash

The -ef used below is not POSIX compatible. Likewise, while -e is specified in POSIX, it is not pure-Bourne compatible. Both of them are widely supported though.

for f in *.JPG; do
    ff="${f%.JPG}.jpg"
    test -e "$f" || continue        # possible when not using nullglob
    test "$f" != "$ff" || continue  # possible when using nocaseglob
    if test -e "$ff" &&
       ! test "$f" -ef "$ff"; then  # possible on a case sensitive filesystem
        echo "skipping <$f>: destination <$ff> exists and is distinct" 1>&2
        continue
    fi

    # "mv" with "git rm" and "git add"
    mv -f "$f" "$ff"     &&
    git rm --cached "$f" &&
    git add "$ff"
done

The last section (mv, git rm, git add) could be replaced with just git mv:

    # "git mv"
    git mv -f "$f" "$ff"

If you are very concerned with how the rename might fail on case insensitive systems, then you could use a temp name:

    # temp-based "mv" with "git rm" and "git add"
    t="$ff.tmp"; while test -e "$t"; do t="$t.tmp"; done
    mv -n "$f" "$t"      &&
    mv -n "$t" "$ff"     &&
    git rm --cached "$f" &&
    git add "$ff"

Or with git mv:

    # temp-based "git mv"
    t="$ff.tmp"; while test -e "$t"; do t="$t.tmp"; done
    git mv "$f" "$t"  &&
    git mv "$t" "$ff"

zsh/zmv

This one needs -f for both zmv and git mv.

zsh -c 'autoload zmv && $0 $@' zmv -fp git -o 'mv -f' '(*).JPG' '$1 x.jpg'

Now that you have them all renamed and updated in Git's index you can commit them.

But will other Git users using case sensitive filesystems be able to check them out?

git checkout After a Case-Only Rename

If there are other users of your history, they will probably still have the JPG files and when they eventually checkout (a descendent of) your commit with the jpg files. What will happen for them?

No matter what happens, there is no need for “rename to temp, commit, rename to final, commit”. git checkout does not apply commits in sequence when moving between commits. It really works by “merging” the index and working tree from HEAD to the new commit. This effectively means that it “jumps” directly to the new commit while dragging along non-conflicting changes found between HEAD and the index/working-tree.

Internally, Git views renames as a deletion and an addition. I did not find any documentation that described the behavior of git checkout with respect to the order of deletions and additions, so I looked at the source code. git checkout processes all deletions before any updates/additions (cmd_checkout -> switch_branches -> merge_working_tree (-> reset_tree) -> unpack_trees -> check_updates).

You can test this out right after your rename commit:

git checkout HEAD~ # note: detached HEAD
# verify that the original names are back in place
git checkout -     # back to your branch
# verify that the new names are in place again

The git blame on the file seemed to indicate a likely commit: Make unpack-tree update removed files before any updated files, which was first released in Git 1.5.6-rc0 (2008-06-18). So, though undocumented(?), this behavior was implemented specifically to support case insensitive filesystems.

Thanks, Linus!

Chris Johnsen
  • 214,407
  • 26
  • 209
  • 186
5

Whether or not you can just change the case of the file will depend on your filesystem. Even if it works on your filesystem, you could cause problems for other people updating. You'll be best renaming them, committing them, then renaming them back. Change everything to *.tmp with the following bash script:

for i in *.JPG; do mv $i ${i%.JPG}.tmp; done

Then move them all in git. You can use a similar command, but I would recommend checking out guess-renames which will help with the move.

Then rename them all back to *.jpg with a similar process.

wbyoung
  • 22,383
  • 2
  • 34
  • 40
  • I think I'd recommend `*.JPG` over `\`ls\`` – Dustin Jun 05 '10 at 20:00
  • 1
    When moving between commits Git does not replay each intermediate changeset. Moving to a new commit updates the files that have changed between the current HEAD/index and the target commit. Commiting the rename in multiple stages will not help users that have machines that can not directly handle `mv foo.JPG foo.jpg` (unless they manually “visit” the “.tmp” commit whenever they move to a commit that is “on the other side” of the rename). Git does not need *guess-renames*, `git add -u && git add ` will pickup “renames” made without `git mv` (or, just use `git mv` instead of `mv`). – Chris Johnsen Jun 05 '10 at 20:10
3

To rename file extensions from .JPG to lowercase. This uses git mv, so that git history is preserved.

find . -name "*.JPG" | xargs -L1 bash -c 'git mv $0 ${0/JPG/jpg}'
Damien
  • 1,140
  • 15
  • 22
2

Use the standard Linux rename(1) utility. Once you've renamed the files, git add them.

Jordan Lewis
  • 16,900
  • 4
  • 29
  • 46
  • use "rename 's/JPG/jpg/' *.JPG" GIT is content-addressed, so that renaming files is automatically detected (as long as the contents remain the same). – tucuxi Jun 05 '10 at 17:12
  • 1
    I am using mac so I don't have rename. – Nick Vanderbilt Jun 05 '10 at 17:16
  • You will need to use `git add -A .` if you want git to store the commit as a rename, and not as a file destroy and a file creation. -A means check deletions(if tracked), not only file creations. – ivanxuu Jan 29 '15 at 11:42
  • This would result in losing the git log history of the files in question. Better to use git-mv. – Damien Nov 20 '20 at 11:59
1

Maybe rename them to *.somethingelse and then rename back to *.jpg

Amien
  • 954
  • 2
  • 11
  • 18