{"thread":{"id":"9043","subject":"[PATCH] Teach fast-import to recursively copy files/directories","startedAt":"2007-07-15T05:45:15Z","lastAt":"2007-07-15T05:45:15Z","messageCount":1,"participants":["Shawn O. Pearce"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"47397","messageId":"20070715054515.GA32550@spearce.org","threadId":"9043","inReplyTo":null,"subject":"[PATCH] Teach fast-import to recursively copy files/directories","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2007-07-15T05:45:15Z","receivedAt":"2007-07-15T05:45:15Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"Some source material (e.g. Subversion dump files) perform directory\nrenames by telling us the directory was copied, then deleted in the\nsame revision.  This makes it difficult for a frontend to convert\nsuch data formats to a fast-import stream, as all the frontend has\non hand is \"Copy a/ to b/; Delete a/\" with no details about what\nfiles are in a/, unless the frontend also kept track of all files.\n\nThe new 'C' subcommand within a commit allows the frontend to make a\nrecursive copy of one path to another path within the branch, without\nneeding to keep track of the individual file paths.  The metadata\ncopy is performed in memory efficiently, but is implemented as a\ncopy-immediately operation, rather than copy-on-write.\n\nWith this new 'C' subcommand frontends could obviously implement an\n'R' (rename) on their own as a combination of 'C' and 'D' (delete),\nbut since we have already offered up 'R' in the past and it is a\ntrivial thing to keep implemented I'm not going to deprecate it.\n\nSigned-off-by: Shawn O. Pearce <spearce@spearce.org>\n---\n  \n    This and the following changes since commit 9d6f220cc8ffbd71b4c68765b52c3a7c41dd729b:\n      Josh Triplett (1):\n            Remove useless uses of cat, and replace with filename arguments\n    \n    are available in the git repository at:\n    \n      repo.or.cz:/srv/git/git/fastimport.git master\n    \n    Marius Storm-Olsen (1):\n          Fix git-p4 on Windows to not use the Posix sysconf function.\n    \n    Shawn O. Pearce (2):\n          Correct trivial typo in fast-import documentation\n          Teach fast-import to recursively copy files/directories\n    \n     Documentation/git-fast-import.txt |   40 +++++++++++++++--\n     contrib/fast-import/git-p4        |    6 ++-\n     fast-import.c                     |   81 ++++++++++++++++++++++++++++++++++--\n     t/t9300-fast-import.sh            |   83 +++++++++++++++++++++++++++++++++++++\n     4 files changed, 200 insertions(+), 10 deletions(-)\n\n Hopefully this stuff will make it into 1.5.3 final.  This should\n be the last fastimport update before then.\n\n --\n Documentation/git-fast-import.txt |   40 +++++++++++++++--\n fast-import.c                     |   81 ++++++++++++++++++++++++++++++++++--\n t/t9300-fast-import.sh            |   83 +++++++++++++++++++++++++++++++++++++\n 3 files changed, 195 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/git-fast-import.txt b/Documentation/git-fast-import.txt\nindex bf1ba67..30ee98d 100644\n--- a/Documentation/git-fast-import.txt\n+++ b/Documentation/git-fast-import.txt\n@@ -302,7 +302,7 @@ change to the project.\n \tdata\n \t('from' SP <committish> LF)?\n \t('merge' SP <committish> LF)?\n-\t(filemodify | filedelete | filerename | filedeleteall)*\n+\t(filemodify | filedelete | filecopy | filerename | filedeleteall)*\n \tLF\n ....\n \n@@ -325,13 +325,13 @@ commit message use a 0 length data.  Commit messages are free-form\n and are not interpreted by Git.  Currently they must be encoded in\n UTF-8, as fast-import does not permit other encodings to be specified.\n \n-Zero or more `filemodify`, `filedelete`, `filerename` and\n-`filedeleteall` commands\n+Zero or more `filemodify`, `filedelete`, `filecopy`, `filerename`\n+and `filedeleteall` commands\n may be included to update the contents of the branch prior to\n creating the commit.  These commands may be supplied in any order.\n However it is recommended that a `filedeleteall` command preceed\n-all `filemodify` and `filerename` commands in the same commit, as\n-`filedeleteall`\n+all `filemodify`, `filecopy` and `filerename` commands in the same\n+commit, as `filedeleteall`\n wipes the branch clean (see below).\n \n `author`\n@@ -497,6 +497,27 @@ here `<path>` is the complete path of the file or subdirectory to\n be removed from the branch.\n See `filemodify` above for a detailed description of `<path>`.\n \n+`filecopy`\n+^^^^^^^^^^^^\n+Recursively copies an existing file or subdirectory to a different\n+location within the branch.  The existing file or directory must\n+exist.  If the destination exists it will be completely replaced\n+by the content copied from the source.\n+\n+....\n+\t'C' SP <path> SP <path> LF\n+....\n+\n+here the first `<path>` is the source location and the second\n+`<path>` is the destination.  See `filemodify` above for a detailed\n+description of what `<path>` may look like.  To use a source path\n+that contains SP the path must be quoted.\n+\n+A `filecopy` command takes effect immediately.  Once the source\n+location has been copied to the destination any future commands\n+applied to the source location will not impact the destination of\n+the copy.\n+\n `filerename`\n ^^^^^^^^^^^^\n Renames an existing file or subdirectory to a different location\n@@ -517,6 +538,15 @@ location has been renamed to the destination any future commands\n applied to the source location will create new files there and not\n impact the destination of the rename.\n \n+Note that a `filerename` is the same as a `filecopy` followed by a\n+`filedelete` of the source location.  There is a slight performance\n+advantage to using `filerename`, but the advantage is so small\n+that it is never worth trying to convert a delete/add pair in\n+source material into a rename for fast-import.  This `filerename`\n+command is provided just to simplify frontends that already have\n+rename information and don't want bother with decomposing it into a\n+`filecopy` followed by a `filedelete`.\n+\n `filedeleteall`\n ^^^^^^^^^^^^^^^\n Included in a `commit` command to remove all files (and also all\ndiff --git a/fast-import.c b/fast-import.c\nindex a1cb13f..99a19d8 100644\n--- a/fast-import.c\n+++ b/fast-import.c\n@@ -26,10 +26,16 @@ Format of STDIN stream:\n     lf;\n   commit_msg ::= data;\n \n-  file_change ::= file_clr | file_del | file_rnm | file_obm | file_inm;\n+  file_change ::= file_clr\n+    | file_del\n+    | file_rnm\n+    | file_cpy\n+    | file_obm\n+    | file_inm;\n   file_clr ::= 'deleteall' lf;\n   file_del ::= 'D' sp path_str lf;\n   file_rnm ::= 'R' sp path_str sp path_str lf;\n+  file_cpy ::= 'C' sp path_str sp path_str lf;\n   file_obm ::= 'M' sp mode sp (hexsha1 | idnum) sp path_str lf;\n   file_inm ::= 'M' sp mode sp 'inline' sp path_str lf\n     data;\n@@ -623,6 +629,31 @@ static void release_tree_entry(struct tree_entry *e)\n \tavail_tree_entry = e;\n }\n \n+static struct tree_content *dup_tree_content(struct tree_content *s)\n+{\n+\tstruct tree_content *d;\n+\tstruct tree_entry *a, *b;\n+\tunsigned int i;\n+\n+\tif (!s)\n+\t\treturn NULL;\n+\td = new_tree_content(s->entry_count);\n+\tfor (i = 0; i < s->entry_count; i++) {\n+\t\ta = s->entries[i];\n+\t\tb = new_tree_entry();\n+\t\tmemcpy(b, a, sizeof(*a));\n+\t\tif (a->tree && is_null_sha1(b->versions[1].sha1))\n+\t\t\tb->tree = dup_tree_content(a->tree);\n+\t\telse\n+\t\t\tb->tree = NULL;\n+\t\td->entries[i] = b;\n+\t}\n+\td->entry_count = s->entry_count;\n+\td->delta_depth = s->delta_depth;\n+\n+\treturn d;\n+}\n+\n static void start_packfile(void)\n {\n \tstatic char tmpfile[PATH_MAX];\n@@ -1273,6 +1304,43 @@ del_entry:\n \treturn 1;\n }\n \n+static int tree_content_get(\n+\tstruct tree_entry *root,\n+\tconst char *p,\n+\tstruct tree_entry *leaf)\n+{\n+\tstruct tree_content *t = root->tree;\n+\tconst char *slash1;\n+\tunsigned int i, n;\n+\tstruct tree_entry *e;\n+\n+\tslash1 = strchr(p, '/');\n+\tif (slash1)\n+\t\tn = slash1 - p;\n+\telse\n+\t\tn = strlen(p);\n+\n+\tfor (i = 0; i < t->entry_count; i++) {\n+\t\te = t->entries[i];\n+\t\tif (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) {\n+\t\t\tif (!slash1) {\n+\t\t\t\tmemcpy(leaf, e, sizeof(*leaf));\n+\t\t\t\tif (e->tree && is_null_sha1(e->versions[1].sha1))\n+\t\t\t\t\tleaf->tree = dup_tree_content(e->tree);\n+\t\t\t\telse\n+\t\t\t\t\tleaf->tree = NULL;\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\tif (!S_ISDIR(e->versions[1].mode))\n+\t\t\t\treturn 0;\n+\t\t\tif (!e->tree)\n+\t\t\t\tload_tree(e);\n+\t\t\treturn tree_content_get(e, slash1 + 1, leaf);\n+\t\t}\n+\t}\n+\treturn 0;\n+}\n+\n static int update_branch(struct branch *b)\n {\n \tstatic const char *msg = \"fast-import\";\n@@ -1658,7 +1726,7 @@ static void file_change_d(struct branch *b)\n \tfree(p_uq);\n }\n \n-static void file_change_r(struct branch *b)\n+static void file_change_cr(struct branch *b, int rename)\n {\n \tconst char *s, *d;\n \tchar *s_uq, *d_uq;\n@@ -1694,7 +1762,10 @@ static void file_change_r(struct branch *b)\n \t}\n \n \tmemset(&leaf, 0, sizeof(leaf));\n-\ttree_content_remove(&b->branch_tree, s, &leaf);\n+\tif (rename)\n+\t\ttree_content_remove(&b->branch_tree, s, &leaf);\n+\telse\n+\t\ttree_content_get(&b->branch_tree, s, &leaf);\n \tif (!leaf.versions[1].mode)\n \t\tdie(\"Path %s not in branch\", s);\n \ttree_content_set(&b->branch_tree, d,\n@@ -1874,7 +1945,9 @@ static void cmd_new_commit(void)\n \t\telse if (!prefixcmp(command_buf.buf, \"D \"))\n \t\t\tfile_change_d(b);\n \t\telse if (!prefixcmp(command_buf.buf, \"R \"))\n-\t\t\tfile_change_r(b);\n+\t\t\tfile_change_cr(b, 1);\n+\t\telse if (!prefixcmp(command_buf.buf, \"C \"))\n+\t\t\tfile_change_cr(b, 0);\n \t\telse if (!strcmp(\"deleteall\", command_buf.buf))\n \t\t\tfile_change_deleteall(b);\n \t\telse\ndiff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh\nindex bf3720d..4b920be 100755\n--- a/t/t9300-fast-import.sh\n+++ b/t/t9300-fast-import.sh\n@@ -648,4 +648,87 @@ test_expect_success \\\n \t git diff-tree -M -r M3^ M3 >actual &&\n \t compare_diff_raw expect actual'\n \n+###\n+### series N\n+###\n+\n+test_tick\n+cat >input <<INPUT_END\n+commit refs/heads/N1\n+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE\n+data <<COMMIT\n+file copy\n+COMMIT\n+\n+from refs/heads/branch^0\n+C file2/newf file2/n.e.w.f\n+\n+INPUT_END\n+\n+cat >expect <<EOF\n+:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc C100\tfile2/newf\tfile2/n.e.w.f\n+EOF\n+test_expect_success \\\n+\t'N: copy file in same subdirectory' \\\n+\t'git-fast-import <input &&\n+\t git diff-tree -C --find-copies-harder -r N1^ N1 >actual &&\n+\t compare_diff_raw expect actual'\n+\n+cat >input <<INPUT_END\n+commit refs/heads/N2\n+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE\n+data <<COMMIT\n+clean directory copy\n+COMMIT\n+\n+from refs/heads/branch^0\n+C file2 file3\n+\n+commit refs/heads/N2\n+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE\n+data <<COMMIT\n+modify directory copy\n+COMMIT\n+\n+M 644 inline file3/file5\n+data <<EOF\n+$file5_data\n+EOF\n+\n+INPUT_END\n+\n+cat >expect <<EOF\n+:100644 100644 fcf778cda181eaa1cbc9e9ce3a2e15ee9f9fe791 fcf778cda181eaa1cbc9e9ce3a2e15ee9f9fe791 C100\tnewdir/interesting\tfile3/file5\n+:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc C100\tfile2/newf\tfile3/newf\n+:100644 100644 7123f7f44e39be127c5eb701e5968176ee9d78b1 7123f7f44e39be127c5eb701e5968176ee9d78b1 C100\tfile2/oldf\tfile3/oldf\n+EOF\n+test_expect_success \\\n+\t'N: copy then modify subdirectory' \\\n+\t'git-fast-import <input &&\n+\t git diff-tree -C --find-copies-harder -r N2^^ N2 >actual &&\n+\t compare_diff_raw expect actual'\n+\n+cat >input <<INPUT_END\n+commit refs/heads/N3\n+committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE\n+data <<COMMIT\n+dirty directory copy\n+COMMIT\n+\n+from refs/heads/branch^0\n+M 644 inline file2/file5\n+data <<EOF\n+$file5_data\n+EOF\n+\n+C file2 file3\n+D file2/file5\n+\n+INPUT_END\n+\n+test_expect_success \\\n+\t'N: copy dirty subdirectory' \\\n+\t'git-fast-import <input &&\n+\t test `git-rev-parse N2^{tree}` = `git-rev-parse N3^{tree}`'\n+\n test_done\n-- \n1.5.3.rc0.900.ge7568\n"}]}