1

I have a file in a repository branch I would like to have on its own repository to build a package with it.

/A
 -file1
 -file.code
 -file3
 -.git

And I want to have:

/B
 -file.code
 -.git #with all the previous history for such file
/A
 -file1
 -file3
 -.git

I would like to preserve the history of commits I have done with this file, and to be able to keep using it in the main project, so that I don't lost the functionality until I finish the package.

I think that this post is something similar to what I want to do, whoever some comments recommend using submodules as in this gist, with the problem of keeping all the original history.

I also found this other question which didn't work when I tried the main answers and I got assertion error and:

$git clone file.code ../folder
Cloning into '../folder'...
fatal: Invalid gitfile format: path/to/file.code
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

Another solution I found solves the problem for a directory and when moving to an existing repository (not that it is a trouble creating a new empty one) but it doesn't work for a single file.

Lastly I thought about "moving" from one repository to another, but

$git mv file.code ../B
fatal: ../B: '../B is outside repository

How can I move/clone/... an existing file to a new repository keeping all its own history?

Community
  • 1
  • 1
llrs
  • 3,308
  • 35
  • 68

1 Answers1

1

In Git, files don't have history.

Instead, commits have history—or more correctly, the commits are the history. And, commits have files. But files don't have history. Hence, if you want history, what you want are commits, and those will bring files along for the ride.

The filter-branch command will let you copy some or all commits within a repository. It starts by listing all the commits to copy. Then, for each such commit, it:

  1. extracts the commit;
  2. applies your filters; and
  3. makes a new commit from the filtered result.

Last, once it's filtered all your commits, it makes the filtered branch names (and optionally annotated-tags as well) point, not to the old commits (which are still in your repository), but to the new commmits instead.

If you filter all commits on all branches (and all tag names with --tag-name-filter cat), then throw away the original branches, the result is a new repository with whatever changes your filter(s) made. Typically one might use git filter-branch to remove all copies of some sensitive file (e.g., with passwords). That is, the filter says "if file dontcommit.txt exists, remove it".

If you write a filter that says "remove everything except file keep.txt", and use that to filter all commits in all branches and all tag names, the copied commits will have only the one file. Filter-branch can be told to discard "empty" commits, i.e., those that make no change after the filter has been applied, so by adding --prune-empty you can toss out all the commits that affect other files.

Assuming you have a Unix-like system with a good find command, this filter-branch should therefore do the trick. Note that filter-branch is very slow—extracting and re-making all the commits takes a significant amount of work in a big repository—and it should be done on a new clone of the repository (so that you cannot damage the original).

git filter-branch \
  --tree-filter \
    'find . -path . -o -path path/to/keep.txt -o -print0 | xargs -0 rm -f' \
  --prune-empty \
  --tag-name-filter cat \
  -- --all

Replace path/to/keep.txt with the one file you intend to keep, of course. (Note that this relies on the fact that Git does not save empty directories: the rm -f will quietly fail on directories but will remove all their contents, except of course the one "keep" path that we made sure not to print.)

(Another option is to copy it out of the way, then remove everything, then move it back into place:

cp path/to/keep.txt /tmp/keep.txt &&
  rm -rf .??* * &&
  mv /tmp/keep.txt .

which lets you change the location of the file within the temporary directory that git filter-branch uses to make each new commit. The .??* here is intended to remove files like .gitignore and .gitattributes: any dot-files at the top of the work-tree. Note that the filtering is done in a temporary directory that does not contain the .git repository itself.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • I tried your first solution but there seems to be a problem with the find instructions `find: unknown predicate '-keep.txt'`. And I didn't manage to correct it. Your second solution doesn't seem to preserve the folder and thus the .git with the commits history is lost if executed the cp, outside the git filter-branch and if executed instead of the find... then `No such file or directory tree filter failed` – llrs Dec 07 '16 at 11:42
  • I made it work changing the find command to: `find . -path . -o -path keep.txt -o -print0`, however due to some other issues with the history I got "duplicate parent" ignored, and `cannot remove ‘./C’: Is a directory` and setting `rm -f` to `rm -rf` deletes everything, including the file of interest – llrs Dec 07 '16 at 12:10
  • There's no `-keep` in either of my proposed tree-filters... your second comment here looks like my first proposed `find` :-) Note that the `rm` in it is `rm -f` which should silently pass over directories. If your rm errors out for some reason, you could add `-o -type d` to skip directories before `-print0`, or if you have no symlinks, make it `-type f -print0`. – torek Dec 07 '16 at 17:12